Converting app into a library WIP
@@ -1,6 +0,0 @@
|
||||
VITE_GRAPHQL_SUPPORT_API=https://example.com/graphql
|
||||
VITE_PAYMENT_API=https://example.com/payment/api
|
||||
VITE_GRAPHQL_SUPPORT_SUBSCRIPTIONS_API=https://example.com/graphql
|
||||
VITE_GRAPHQL_API=https://example.com/graphql
|
||||
VITE_STRIPE_PUBLIC_KEY=STRIPE_PUBLIC_KEY
|
||||
VITE_CLOUDINARY_URL=CLOUDINARY_URL
|
||||
@@ -1 +0,0 @@
|
||||
.eslintrc.js
|
||||
@@ -1,65 +0,0 @@
|
||||
module.exports = {
|
||||
extends: [
|
||||
'airbnb-typescript',
|
||||
'airbnb/hooks',
|
||||
'plugin:jest/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:import/recommended'
|
||||
],
|
||||
plugins: ['react', '@typescript-eslint', 'jest', 'prettier'],
|
||||
env: {
|
||||
browser: true,
|
||||
es6: true,
|
||||
jest: true,
|
||||
},
|
||||
globals: {
|
||||
Atomics: 'readonly',
|
||||
SharedArrayBuffer: 'readonly',
|
||||
},
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
ecmaVersion: 2018,
|
||||
sourceType: 'module',
|
||||
project: './tsconfig.json',
|
||||
},
|
||||
rules: {
|
||||
'no-console': 0,
|
||||
'no-nested-ternary': 'off',
|
||||
'no-unused-expressions': 'off',
|
||||
'@typescript-eslint/no-unused-expressions': [
|
||||
'warn',
|
||||
{ allowShortCircuit: true, allowTernary: true },
|
||||
],
|
||||
'@typescript-eslint/ban-ts-comment': 0,
|
||||
'react-hooks/exhaustive-deps': 1,
|
||||
'import/no-cycle': 'off',
|
||||
'react/jsx-props-no-spreading': 'off',
|
||||
'react/require-default-props': 0,
|
||||
'import/prefer-default-export': 'off',
|
||||
'import/no-extraneous-dependencies': 0,
|
||||
'no-prototype-builtins': 'off',
|
||||
'no-plusplus': 'off',
|
||||
'@typescript-eslint/no-use-before-define': 'off',
|
||||
'@typescript-eslint/no-non-null-asserted-optional-chain': 0,
|
||||
'@typescript-eslint/no-non-null-assertion': 0,
|
||||
'react/prop-types': 'off',
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'react/self-closing-comp': 0,
|
||||
'react/no-array-index-key': 0,
|
||||
'@typescript-eslint/camelcase': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 0,
|
||||
'@typescript-eslint/explicit-module-boundary-types': 0,
|
||||
'@typescript-eslint/no-explicit-any': 0,
|
||||
'linebreak-style': 'off',
|
||||
'prettier/prettier': [
|
||||
'error',
|
||||
{
|
||||
endOfLine: 'auto',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -17,11 +17,9 @@ codegen-*
|
||||
# misc
|
||||
.DS_Store
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.*
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
yarn-error.log*
|
||||
*storybook.log
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { StorybookConfig } from '@storybook/react-vite';
|
||||
|
||||
const config: StorybookConfig = {
|
||||
"stories": [
|
||||
"../src/**/*.mdx",
|
||||
"../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"
|
||||
],
|
||||
"addons": [
|
||||
"@storybook/addon-essentials",
|
||||
"@storybook/addon-onboarding",
|
||||
"@chromatic-com/storybook",
|
||||
"@storybook/addon-interactions",
|
||||
"@storybook/addon-themes"
|
||||
],
|
||||
"framework": {
|
||||
"name": "@storybook/react-vite",
|
||||
"options": {}
|
||||
}
|
||||
};
|
||||
export default config;
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { Preview } from '@storybook/react'
|
||||
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import { withThemeFromJSXProvider } from '@storybook/addon-themes';
|
||||
|
||||
import { theme } from '../src/themes';
|
||||
|
||||
import GlobalStyles from '../src/components/GlobalStyles';
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
decorators: [withThemeFromJSXProvider({
|
||||
themes: {
|
||||
theme,
|
||||
},
|
||||
defaultTheme: 'theme',
|
||||
Provider: ThemeProvider,
|
||||
GlobalStyles,
|
||||
})]
|
||||
};
|
||||
|
||||
export default preview;
|
||||
@@ -1,30 +0,0 @@
|
||||
# Access Accounts for The Demo
|
||||
1. Client
|
||||
```
|
||||
- Email: client@client.cc
|
||||
- Password: 123456
|
||||
```
|
||||
2. Product Owner
|
||||
```
|
||||
- Email: po@po.cc
|
||||
- Password: 123456
|
||||
```
|
||||
3. Developer
|
||||
```
|
||||
- Email: dev@dev.cc
|
||||
- Password: 123456
|
||||
```
|
||||
4. Admin
|
||||
```
|
||||
- Email: admin@admin.cc
|
||||
- Password: 123456
|
||||
```
|
||||
|
||||
# Installation
|
||||
Check the `.env.example` for the list of APIs to use then clone and run
|
||||
|
||||
```
|
||||
yarn
|
||||
```
|
||||
|
||||
For the backend configuration check [here](https://github.com/MedAmineFouzai/astrobuild-api)
|
||||
@@ -0,0 +1,35 @@
|
||||
import globals from 'globals';
|
||||
import pluginJs from '@eslint/js';
|
||||
import tseslint from 'typescript-eslint';
|
||||
import pluginReact from 'eslint-plugin-react';
|
||||
import hooksPlugin from 'eslint-plugin-react-hooks';
|
||||
import prettier from 'eslint-plugin-prettier';
|
||||
|
||||
// TODO: Better understand and improve this config along with tsconfig.
|
||||
/** @type {import('eslint').Linter.Config[]} */
|
||||
export default [
|
||||
{ files: ['**/*.{js,mjs,cjs,ts,jsx,tsx}'] },
|
||||
{ languageOptions: { globals: globals.browser } },
|
||||
pluginJs.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
pluginReact.configs.flat.recommended,
|
||||
{
|
||||
plugins: {
|
||||
prettier,
|
||||
},
|
||||
rules: {
|
||||
'react/react-in-jsx-scope': 'off'
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
plugins: {
|
||||
'react-hooks': hooksPlugin,
|
||||
},
|
||||
rules: hooksPlugin.configs.recommended.rules,
|
||||
}
|
||||
];
|
||||
@@ -1,27 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
|
||||
<link rel="icon" href="/favicon.ico" type="image/x-icon" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using vite"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="/logo192.png" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@100;300;400;500;600;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<title>Astrobuild</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,45 +1,14 @@
|
||||
{
|
||||
"name": "astrobuild",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.7.10",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"@types/jest": "^29.5.0",
|
||||
"@types/jwt-decode": "^3.1.0",
|
||||
"@types/node": "^18.15.7",
|
||||
"@types/react": "^18.0.29",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/styled-components": "^5.1.26",
|
||||
"formik": "^2.2.9",
|
||||
"graphql": "^16.6.0",
|
||||
"graphql-ws": "^5.13.1",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"localforage": "^1.10.0",
|
||||
"match-sorter": "^6.3.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-elastic-carousel": "^0.11.5",
|
||||
"react-router-dom": "^6.9.0",
|
||||
"react-to-print": "^2.14.12",
|
||||
"reactflow": "^11.7.0",
|
||||
"sort-by": "^1.2.0",
|
||||
"styled-components": "^5.3.10",
|
||||
"typescript": "^5.0.2",
|
||||
"vite-plugin-svgr": "^2.4.0",
|
||||
"web-vitals": "^3.3.0",
|
||||
"yup": "^1.0.2"
|
||||
},
|
||||
"version": "1.0.0",
|
||||
"private": false,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
"build": "vite build",
|
||||
"generate:main": "graphql-codegen --config codegen-main.yml",
|
||||
"lint": "yarn run eslint src --ext .ts,.tsx",
|
||||
"fix": "yarn lint --fix",
|
||||
"generate:support": "graphql-codegen --config codegen-support.ts"
|
||||
"lint": "eslint src",
|
||||
"format": "prettier -w src",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
@@ -54,24 +23,38 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "3.3.1",
|
||||
"@graphql-codegen/client-preset": "3.0.1",
|
||||
"@graphql-codegen/introspection": "3.0.1",
|
||||
"@graphql-codegen/typescript": "^3.0.2",
|
||||
"@graphql-codegen/typescript-operations": "^3.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.56.0",
|
||||
"@typescript-eslint/parser": "^5.56.0",
|
||||
"@chromatic-com/storybook": "^3",
|
||||
"@eslint/js": "^9.22.0",
|
||||
"@storybook/addon-essentials": "^8.6.7",
|
||||
"@storybook/addon-interactions": "^8.6.7",
|
||||
"@storybook/addon-onboarding": "^8.6.7",
|
||||
"@storybook/addon-themes": "^8.6.7",
|
||||
"@storybook/blocks": "^8.6.7",
|
||||
"@storybook/react": "^8.6.7",
|
||||
"@storybook/react-vite": "^8.6.7",
|
||||
"@storybook/test": "^8.6.7",
|
||||
"@types/node": "^22.13.10",
|
||||
"@types/react": "^19.0.10",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@types/styled-components": "^5.1.34",
|
||||
"@typescript-eslint/eslint-plugin": "^8.26.1",
|
||||
"@typescript-eslint/parser": "^8.26.1",
|
||||
"@vitejs/plugin-react": "^4.0.0",
|
||||
"eslint-config-airbnb": "19.0.4",
|
||||
"eslint-config-airbnb-typescript": "^17.0.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"eslint-plugin-jest": "^27.2.1",
|
||||
"eslint-plugin-jsx-a11y": "6.7.1",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-react": "7.32.2",
|
||||
"eslint-plugin-react-hooks": "4.6.0",
|
||||
"prettier": "^2.8.7",
|
||||
"vite": "^4.2.1"
|
||||
"eslint": "^9.22.0",
|
||||
"eslint-config-prettier": "^10.1.1",
|
||||
"eslint-plugin-prettier": "^5.2.3",
|
||||
"eslint-plugin-react": "7.37.4",
|
||||
"eslint-plugin-react-hooks": "5.2.0",
|
||||
"eslint-plugin-storybook": "^0.11.5",
|
||||
"globals": "^16.0.0",
|
||||
"prettier": "^3.5.3",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"storybook": "^8.6.7",
|
||||
"styled-components": "^6.1.15",
|
||||
"typescript": "^5.8.2",
|
||||
"typescript-eslint": "^8.26.1",
|
||||
"vite": "^6.2.2",
|
||||
"vite-plugin-svgr": "^4.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 9.4 KiB |
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
|
Before Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 159 KiB |
|
Before Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 178 KiB |
@@ -1,352 +0,0 @@
|
||||
import jwtDecode from 'jwt-decode';
|
||||
import { useEffect } from 'react';
|
||||
import { Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { useLazyQuery, useReactiveVar } from '@apollo/client';
|
||||
import { Protected, Public, Navbar, Sidebar, Spinner } from './components';
|
||||
import { roleVar, tokenVar, userVar } from './graphql/state';
|
||||
import {
|
||||
AdditionalInfo,
|
||||
ForgotPassword,
|
||||
Login,
|
||||
RecoverAccount,
|
||||
Signup,
|
||||
Project,
|
||||
Users,
|
||||
Settings,
|
||||
UserSettings,
|
||||
CreateUser,
|
||||
Template,
|
||||
Feature,
|
||||
Category,
|
||||
Prototype,
|
||||
AddCategory,
|
||||
AddFeature,
|
||||
AddTemplate,
|
||||
CategorySettings,
|
||||
FeatureSettings,
|
||||
TemplateSettings,
|
||||
AddProject,
|
||||
UpdateProject,
|
||||
Support,
|
||||
} from './pages';
|
||||
import { GetUserByIdQuery, GetUserByIdQueryVariables } from './graphql/types';
|
||||
import { GET_USER_BY_ID } from './graphql/auth.api';
|
||||
|
||||
const App = () => {
|
||||
const token = useReactiveVar(tokenVar);
|
||||
const role = useReactiveVar(roleVar);
|
||||
const currentUser = useReactiveVar(userVar);
|
||||
|
||||
const [getUserById, { loading }] = useLazyQuery<
|
||||
GetUserByIdQuery,
|
||||
GetUserByIdQueryVariables
|
||||
>(GET_USER_BY_ID, {
|
||||
onCompleted({ getUserById: user }) {
|
||||
userVar(user);
|
||||
switch (user.role) {
|
||||
case 'Client':
|
||||
roleVar('client');
|
||||
break;
|
||||
case 'ProductOwner':
|
||||
roleVar('productOwner');
|
||||
break;
|
||||
case 'Developer':
|
||||
roleVar('developer');
|
||||
break;
|
||||
case 'Admin':
|
||||
roleVar('admin');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const localStorageToken = localStorage.getItem('token');
|
||||
|
||||
if (localStorageToken) {
|
||||
const { id } = jwtDecode<{ id: string; role: string }>(localStorageToken);
|
||||
|
||||
getUserById({ variables: { id } });
|
||||
tokenVar(localStorageToken);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return !loading ? (
|
||||
<>
|
||||
{token && currentUser?.firstName && (
|
||||
<>
|
||||
<Navbar />
|
||||
<Sidebar />
|
||||
</>
|
||||
)}
|
||||
<Routes>
|
||||
<Route
|
||||
path='/'
|
||||
element={
|
||||
<Protected>
|
||||
{role !== 'admin' ? (
|
||||
<Navigate to='/project' />
|
||||
) : (
|
||||
<Navigate to='/clients' />
|
||||
)}
|
||||
</Protected>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/project'
|
||||
element={
|
||||
<Protected>
|
||||
<Project />
|
||||
</Protected>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/project/:id'
|
||||
element={
|
||||
<Protected>
|
||||
<Project />
|
||||
</Protected>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/add-project'
|
||||
element={
|
||||
<Protected>
|
||||
<AddProject />
|
||||
</Protected>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/project-settings/:id'
|
||||
element={
|
||||
<Protected>
|
||||
<UpdateProject />
|
||||
</Protected>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/support/:projectId'
|
||||
element={
|
||||
<Protected>
|
||||
<Support />
|
||||
</Protected>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/support/:projectId/:threadId'
|
||||
element={
|
||||
<Protected>
|
||||
<Support />
|
||||
</Protected>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/template'
|
||||
element={
|
||||
<Protected>
|
||||
<Template />
|
||||
</Protected>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/template/:id'
|
||||
element={
|
||||
<Protected>
|
||||
<Template />
|
||||
</Protected>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/add-template'
|
||||
element={
|
||||
<Protected>
|
||||
<AddTemplate />
|
||||
</Protected>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/template-settings/:id'
|
||||
element={
|
||||
<Protected>
|
||||
<TemplateSettings />
|
||||
</Protected>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/add-template'
|
||||
element={
|
||||
<Protected>
|
||||
<AddTemplate />
|
||||
</Protected>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/feature'
|
||||
element={
|
||||
<Protected>
|
||||
<Feature />
|
||||
</Protected>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/feature/:id'
|
||||
element={
|
||||
<Protected>
|
||||
<Feature />
|
||||
</Protected>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/add-feature'
|
||||
element={
|
||||
<Protected>
|
||||
<AddFeature />
|
||||
</Protected>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/feature-settings/:id'
|
||||
element={
|
||||
<Protected>
|
||||
<FeatureSettings />
|
||||
</Protected>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/category'
|
||||
element={
|
||||
<Protected>
|
||||
<Category />
|
||||
</Protected>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/category/:id'
|
||||
element={
|
||||
<Protected>
|
||||
<Category />
|
||||
</Protected>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/add-category'
|
||||
element={
|
||||
<Protected>
|
||||
<AddCategory />
|
||||
</Protected>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/category-settings/:id'
|
||||
element={
|
||||
<Protected>
|
||||
<CategorySettings />
|
||||
</Protected>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/prototype/:id'
|
||||
element={
|
||||
<Protected>
|
||||
<Prototype />
|
||||
</Protected>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/clients'
|
||||
element={
|
||||
<Protected>
|
||||
<Users />
|
||||
</Protected>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/product-owners'
|
||||
element={
|
||||
<Protected>
|
||||
<Users />
|
||||
</Protected>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/developers'
|
||||
element={
|
||||
<Protected>
|
||||
<Users />
|
||||
</Protected>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/create-user/:role'
|
||||
element={
|
||||
<Protected>
|
||||
<CreateUser />
|
||||
</Protected>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/user-settings/:id'
|
||||
element={
|
||||
<Protected>
|
||||
<UserSettings />
|
||||
</Protected>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/settings'
|
||||
element={
|
||||
<Protected>
|
||||
<Settings />
|
||||
</Protected>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/login'
|
||||
element={
|
||||
<Public>
|
||||
<Login />
|
||||
</Public>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/signup'
|
||||
element={
|
||||
<Public>
|
||||
<Signup />
|
||||
</Public>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/additional-info'
|
||||
element={
|
||||
<Protected>
|
||||
<AdditionalInfo />
|
||||
</Protected>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/forgot-password'
|
||||
element={
|
||||
<Public>
|
||||
<ForgotPassword />
|
||||
</Public>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/recover-account'
|
||||
element={
|
||||
<Public>
|
||||
<RecoverAccount />
|
||||
</Public>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</>
|
||||
) : (
|
||||
<Spinner fullScreen color={role || 'client'} />
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
@@ -1,39 +1,39 @@
|
||||
import { ReactComponent as Add } from './icons/add.svg';
|
||||
import { ReactComponent as Upload } from './icons/upload.svg';
|
||||
import { ReactComponent as ChevronDown } from './icons/chevron-down.svg';
|
||||
import { ReactComponent as ChevronLeft } from './icons/chevron-left.svg';
|
||||
import { ReactComponent as ChevronRight } from './icons/chevron-right.svg';
|
||||
import { ReactComponent as ArrowLeft } from './icons/arrow-left.svg';
|
||||
import { ReactComponent as ArrowRight } from './icons/arrow-right.svg';
|
||||
import { ReactComponent as Search } from './icons/search.svg';
|
||||
import { ReactComponent as Check } from './icons/check.svg';
|
||||
import { ReactComponent as CheckCircle } from './icons/check-circle.svg';
|
||||
import { ReactComponent as Google } from './icons/google.svg';
|
||||
import { ReactComponent as Settings } from './icons/settings.svg';
|
||||
import { ReactComponent as Logout } from './icons/logout.svg';
|
||||
import { ReactComponent as Logo } from './icons/logo.svg';
|
||||
import { ReactComponent as Profile } from './icons/profile.svg';
|
||||
import { ReactComponent as Security } from './icons/security.svg';
|
||||
import { ReactComponent as Edit } from './icons/edit.svg';
|
||||
import { ReactComponent as Delete } from './icons/delete.svg';
|
||||
import { ReactComponent as General } from './icons/general.svg';
|
||||
import { ReactComponent as Design } from './icons/design.svg';
|
||||
import { ReactComponent as FullBuild } from './icons/full-build.svg';
|
||||
import { ReactComponent as MVP } from './icons/mvp.svg';
|
||||
import { ReactComponent as Specification } from './icons/specification.svg';
|
||||
import { ReactComponent as Features } from './icons/features.svg';
|
||||
import { ReactComponent as Frontend } from './icons/frontend.svg';
|
||||
import { ReactComponent as Backend } from './icons/backend.svg';
|
||||
import { ReactComponent as Close } from './icons/close.svg';
|
||||
import { ReactComponent as Payment } from './icons/payment.svg';
|
||||
import { ReactComponent as Messaging } from './icons/messaging.svg';
|
||||
import { ReactComponent as Send } from './icons/send.svg';
|
||||
import { ReactComponent as Attachment } from './icons/attachment.svg';
|
||||
import { ReactComponent as Login } from './images/login.svg';
|
||||
import { ReactComponent as Signup } from './images/signup.svg';
|
||||
import { ReactComponent as Empty } from './images/empty.svg';
|
||||
import { ReactComponent as ThreadClient } from './images/thread-client.svg';
|
||||
import { ReactComponent as ThreadProductOwner } from './images/thread-po.svg';
|
||||
import Add from './icons/add.svg?react';
|
||||
import Upload from './icons/upload.svg?react';
|
||||
import ChevronDown from './icons/chevron-down.svg?react';
|
||||
import ChevronLeft from './icons/chevron-left.svg?react';
|
||||
import ChevronRight from './icons/chevron-right.svg?react';
|
||||
import ArrowLeft from './icons/arrow-left.svg?react';
|
||||
import ArrowRight from './icons/arrow-right.svg?react';
|
||||
import Search from './icons/search.svg?react';
|
||||
import Check from './icons/check.svg?react';
|
||||
import CheckCircle from './icons/check-circle.svg?react';
|
||||
import Google from './icons/google.svg?react';
|
||||
import Settings from './icons/settings.svg?react';
|
||||
import Logout from './icons/logout.svg?react';
|
||||
import Logo from './icons/logo.svg?react';
|
||||
import Profile from './icons/profile.svg?react';
|
||||
import Security from './icons/security.svg?react';
|
||||
import Edit from './icons/edit.svg?react';
|
||||
import Delete from './icons/delete.svg?react';
|
||||
import General from './icons/general.svg?react';
|
||||
import Design from './icons/design.svg?react';
|
||||
import FullBuild from './icons/full-build.svg?react';
|
||||
import MVP from './icons/mvp.svg?react';
|
||||
import Specification from './icons/specification.svg?react';
|
||||
import Features from './icons/features.svg?react';
|
||||
import Frontend from './icons/frontend.svg?react';
|
||||
import Backend from './icons/backend.svg?react';
|
||||
import Close from './icons/close.svg?react';
|
||||
import Payment from './icons/payment.svg?react';
|
||||
import Messaging from './icons/messaging.svg?react';
|
||||
import Send from './icons/send.svg?react';
|
||||
import Attachment from './icons/attachment.svg?react';
|
||||
import Login from './images/login.svg?react';
|
||||
import Signup from './images/signup.svg?react';
|
||||
import Empty from './images/empty.svg?react';
|
||||
import ThreadClient from './images/thread-client.svg?react';
|
||||
import ThreadProductOwner from './images/thread-po.svg?react';
|
||||
|
||||
export {
|
||||
Add,
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import Alert from '.';
|
||||
|
||||
const meta = {
|
||||
title: 'Alert',
|
||||
component: Alert,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
text: { control: 'text' },
|
||||
color: { control: 'color' },
|
||||
},
|
||||
} satisfies Meta<typeof Alert>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Primary: Story = {
|
||||
args: {
|
||||
text: 'Alert',
|
||||
color: 'client'
|
||||
},
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
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;
|
||||
@@ -1,19 +0,0 @@
|
||||
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;
|
||||
@@ -1,204 +0,0 @@
|
||||
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;
|
||||
@@ -1,5 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
padding: 1rem;
|
||||
`;
|
||||
@@ -1,65 +0,0 @@
|
||||
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,
|
||||
};
|
||||
@@ -1,67 +0,0 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export const GET_ALL_USERS = gql`
|
||||
query GetAllUsers {
|
||||
getAllUsers {
|
||||
id
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
phone {
|
||||
prefix
|
||||
number
|
||||
}
|
||||
address {
|
||||
place
|
||||
city
|
||||
country
|
||||
zip
|
||||
}
|
||||
role
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_USER_BY_ID = gql`
|
||||
query GetUserById($id: String!) {
|
||||
getUserById(id: $id) {
|
||||
id
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
phone {
|
||||
prefix
|
||||
number
|
||||
}
|
||||
address {
|
||||
place
|
||||
city
|
||||
country
|
||||
zip
|
||||
}
|
||||
role
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const CREATE_USER = gql`
|
||||
mutation CreateUser($user: UserInput!) {
|
||||
createUser(user: $user) {
|
||||
id
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
phone {
|
||||
prefix
|
||||
number
|
||||
}
|
||||
address {
|
||||
place
|
||||
city
|
||||
country
|
||||
zip
|
||||
}
|
||||
role
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -1,192 +0,0 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export const SIGNUP = gql`
|
||||
mutation Signup($email: String!, $password: String!) {
|
||||
signup(email: $email, password: $password) {
|
||||
user {
|
||||
id
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
phone {
|
||||
prefix
|
||||
number
|
||||
}
|
||||
address {
|
||||
place
|
||||
city
|
||||
country
|
||||
zip
|
||||
}
|
||||
role
|
||||
}
|
||||
token
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const LOGIN = gql`
|
||||
mutation Login($email: String!, $password: String!) {
|
||||
login(email: $email, password: $password) {
|
||||
user {
|
||||
id
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
phone {
|
||||
prefix
|
||||
number
|
||||
}
|
||||
address {
|
||||
place
|
||||
city
|
||||
country
|
||||
zip
|
||||
}
|
||||
role
|
||||
}
|
||||
token
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const RESET_PASSWORD = gql`
|
||||
mutation ResetPassword($email: String!) {
|
||||
resetUserPassword(email: $email) {
|
||||
id
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
phone {
|
||||
prefix
|
||||
number
|
||||
}
|
||||
address {
|
||||
place
|
||||
city
|
||||
country
|
||||
zip
|
||||
}
|
||||
role
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const CONFIRM_USER_RESET_PASSWORD = gql`
|
||||
mutation ConfirmUserResetPassword($id: String!, $password: String!) {
|
||||
confirmUserResetPassword(id: $id, password: $password) {
|
||||
id
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
phone {
|
||||
prefix
|
||||
number
|
||||
}
|
||||
address {
|
||||
place
|
||||
city
|
||||
country
|
||||
zip
|
||||
}
|
||||
role
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const UPDATE_USER_INFO = gql`
|
||||
mutation UpdateUserInfo($user: UpdateUserInput!) {
|
||||
updateUserInfo(user: $user) {
|
||||
id
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
phone {
|
||||
prefix
|
||||
number
|
||||
}
|
||||
address {
|
||||
place
|
||||
city
|
||||
country
|
||||
zip
|
||||
}
|
||||
role
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const UPDATE_USER_PASSWORD = gql`
|
||||
mutation UpdateUserPassword($id: String!, $password: PasswordInput!) {
|
||||
updateUserPassword(id: $id, password: $password) {
|
||||
id
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
phone {
|
||||
prefix
|
||||
number
|
||||
}
|
||||
address {
|
||||
place
|
||||
city
|
||||
country
|
||||
zip
|
||||
}
|
||||
role
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const DELETE_USER = gql`
|
||||
mutation DeleteUser($id: String!, $password: String!) {
|
||||
deleteUser(id: $id, password: $password) {
|
||||
id
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
phone {
|
||||
prefix
|
||||
number
|
||||
}
|
||||
address {
|
||||
place
|
||||
city
|
||||
country
|
||||
zip
|
||||
}
|
||||
role
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_USER_BY_ID = gql`
|
||||
query GetUserById($id: String!) {
|
||||
getUserById(id: $id) {
|
||||
id
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
phone {
|
||||
prefix
|
||||
number
|
||||
}
|
||||
address {
|
||||
place
|
||||
city
|
||||
country
|
||||
zip
|
||||
}
|
||||
role
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_COUNTRY_CODES = gql`
|
||||
query GetCountryCodes {
|
||||
getCountryCode {
|
||||
prefix
|
||||
country
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -1,71 +0,0 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export const GET_ALL_CATEGORIES = gql`
|
||||
query GetAllCategories {
|
||||
getAllCategories {
|
||||
id
|
||||
name
|
||||
description
|
||||
image {
|
||||
name
|
||||
src
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_CATEGORY_BY_ID = gql`
|
||||
query GetCategoryById($id: String!) {
|
||||
getCategoryById(id: $id) {
|
||||
id
|
||||
name
|
||||
description
|
||||
image {
|
||||
name
|
||||
src
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const ADD_CATEGORY = gql`
|
||||
mutation AddCategory($category: CategoryInput!) {
|
||||
addCategory(category: $category) {
|
||||
id
|
||||
name
|
||||
description
|
||||
image {
|
||||
name
|
||||
src
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const UPDATE_CATEGORY = gql`
|
||||
mutation UpdateCategory($id: String!, $category: CategoryInput!) {
|
||||
updateCategory(id: $id, category: $category) {
|
||||
id
|
||||
name
|
||||
description
|
||||
image {
|
||||
name
|
||||
src
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const DELETE_CATEGORY = gql`
|
||||
mutation DeleteCategory($id: String!) {
|
||||
deleteCategory(id: $id) {
|
||||
id
|
||||
name
|
||||
description
|
||||
image {
|
||||
name
|
||||
src
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -1,76 +0,0 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export const GET_PROJECT_THREADS = gql`
|
||||
query GetProjectThreads($projectId: String!) {
|
||||
threads(projectId: $projectId) {
|
||||
id
|
||||
title
|
||||
threadDescription
|
||||
userMessages {
|
||||
id
|
||||
username
|
||||
text
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_THREAD_BY_ID = gql`
|
||||
query GetThreadById($threadId: String!) {
|
||||
thread(threadId: $threadId) {
|
||||
id
|
||||
title
|
||||
threadDescription
|
||||
userMessages {
|
||||
id
|
||||
username
|
||||
text
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const MESSAGES = gql`
|
||||
query Messages($threadId: String!) {
|
||||
messages(threadId: $threadId) {
|
||||
username
|
||||
text
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const CREATE_THREAD = gql`
|
||||
mutation CreateThread(
|
||||
$projectId: String!
|
||||
$title: String!
|
||||
$threadDescription: String!
|
||||
) {
|
||||
createThread(
|
||||
projectId: $projectId
|
||||
title: $title
|
||||
threadDescription: $threadDescription
|
||||
) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const SEND_MSG = gql`
|
||||
mutation SendMessage($threadId: String!, $username: String!, $text: String!) {
|
||||
sendMessage(threadId: $threadId, username: $username, text: $text)
|
||||
}
|
||||
`;
|
||||
|
||||
export const MESSAGES_SUBSCRIPTION = gql`
|
||||
subscription messagesSubscription {
|
||||
messages {
|
||||
mutationType
|
||||
id
|
||||
userMessages {
|
||||
id
|
||||
username
|
||||
text
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -1,155 +0,0 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export const GET_ALL_FEATURES = gql`
|
||||
query GetAllFeatures {
|
||||
getAllFeatures {
|
||||
id
|
||||
name
|
||||
description
|
||||
featureType
|
||||
image {
|
||||
name
|
||||
src
|
||||
}
|
||||
wireframes {
|
||||
id
|
||||
name
|
||||
src
|
||||
}
|
||||
price
|
||||
repo
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_FEATURE_BY_ID = gql`
|
||||
query GetFeatureById($id: String!) {
|
||||
getFeatureById(id: $id) {
|
||||
id
|
||||
name
|
||||
description
|
||||
featureType
|
||||
image {
|
||||
name
|
||||
src
|
||||
}
|
||||
wireframes {
|
||||
id
|
||||
name
|
||||
src
|
||||
}
|
||||
price
|
||||
repo
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const ADD_FEATURE = gql`
|
||||
mutation AddFeature($feature: FeatureInput!) {
|
||||
addFeature(feature: $feature) {
|
||||
id
|
||||
name
|
||||
description
|
||||
featureType
|
||||
image {
|
||||
name
|
||||
src
|
||||
}
|
||||
wireframes {
|
||||
id
|
||||
name
|
||||
src
|
||||
}
|
||||
price
|
||||
repo
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const UPDATE_FEATURE = gql`
|
||||
mutation UpdateFeature($id: String!, $feature: FeatureInput!) {
|
||||
updateFeature(id: $id, feature: $feature) {
|
||||
id
|
||||
name
|
||||
description
|
||||
featureType
|
||||
image {
|
||||
name
|
||||
src
|
||||
}
|
||||
wireframes {
|
||||
id
|
||||
name
|
||||
src
|
||||
}
|
||||
price
|
||||
repo
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const DELETE_FEATURE = gql`
|
||||
mutation DeleteFeature($id: String!) {
|
||||
deleteFeature(id: $id) {
|
||||
id
|
||||
name
|
||||
description
|
||||
featureType
|
||||
image {
|
||||
name
|
||||
src
|
||||
}
|
||||
wireframes {
|
||||
id
|
||||
name
|
||||
src
|
||||
}
|
||||
price
|
||||
repo
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const ADD_FEATURE_WIREFRAMES = gql`
|
||||
mutation AddFeatureWireframes($id: String!, $wireframes: [InputFile!]!) {
|
||||
addFeatureWireframes(id: $id, wireframes: $wireframes) {
|
||||
id
|
||||
name
|
||||
description
|
||||
featureType
|
||||
image {
|
||||
name
|
||||
src
|
||||
}
|
||||
wireframes {
|
||||
id
|
||||
name
|
||||
src
|
||||
}
|
||||
price
|
||||
repo
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const DELETE_FEATURE_WIREFRAME = gql`
|
||||
mutation DeleteFeatureWireframe($id: String!) {
|
||||
deleteFeatureWireframe(id: $id) {
|
||||
id
|
||||
name
|
||||
description
|
||||
featureType
|
||||
image {
|
||||
name
|
||||
src
|
||||
}
|
||||
wireframes {
|
||||
id
|
||||
name
|
||||
src
|
||||
}
|
||||
price
|
||||
repo
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -1,106 +0,0 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export const GET_PROTOTYPE_BY_ID = gql`
|
||||
query GetPrototypeById($id: String!) {
|
||||
getPrototypeById(id: $id) {
|
||||
id
|
||||
template
|
||||
prototype {
|
||||
feature {
|
||||
id
|
||||
name
|
||||
description
|
||||
featureType
|
||||
image {
|
||||
name
|
||||
src
|
||||
}
|
||||
wireframes {
|
||||
id
|
||||
name
|
||||
src
|
||||
}
|
||||
price
|
||||
repo
|
||||
}
|
||||
connections {
|
||||
to
|
||||
releations {
|
||||
back
|
||||
forword
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const ADD_PROTOTYPE = gql`
|
||||
mutation AddPrototype($prototype: TemplateProtoTypeInput!) {
|
||||
addPrototype(prototype: $prototype) {
|
||||
id
|
||||
template
|
||||
prototype {
|
||||
feature {
|
||||
id
|
||||
name
|
||||
description
|
||||
featureType
|
||||
image {
|
||||
name
|
||||
src
|
||||
}
|
||||
wireframes {
|
||||
id
|
||||
name
|
||||
src
|
||||
}
|
||||
price
|
||||
repo
|
||||
}
|
||||
connections {
|
||||
to
|
||||
releations {
|
||||
back
|
||||
forword
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const UPDATE_PROTOTYPE = gql`
|
||||
mutation UpdatePrototype($prototype: TemplateProtoTypeInput!) {
|
||||
updatePrototype(prototype: $prototype) {
|
||||
id
|
||||
template
|
||||
prototype {
|
||||
feature {
|
||||
id
|
||||
name
|
||||
description
|
||||
featureType
|
||||
image {
|
||||
name
|
||||
src
|
||||
}
|
||||
wireframes {
|
||||
id
|
||||
name
|
||||
src
|
||||
}
|
||||
price
|
||||
repo
|
||||
}
|
||||
connections {
|
||||
to
|
||||
releations {
|
||||
back
|
||||
forword
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -1,10 +0,0 @@
|
||||
import { makeVar } from '@apollo/client';
|
||||
import { UserOutput } from './types';
|
||||
|
||||
export const tokenVar = makeVar<string | undefined>(undefined);
|
||||
|
||||
export const roleVar = makeVar<
|
||||
'client' | 'productOwner' | 'developer' | 'admin' | undefined
|
||||
>(undefined);
|
||||
|
||||
export const userVar = makeVar<UserOutput | undefined>(undefined);
|
||||
@@ -1,460 +0,0 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export const GET_ALL_TEMPLATES = gql`
|
||||
query GetAllTemplates {
|
||||
getAllTemplates {
|
||||
id
|
||||
name
|
||||
description
|
||||
category
|
||||
features {
|
||||
id
|
||||
name
|
||||
description
|
||||
featureType
|
||||
image {
|
||||
name
|
||||
src
|
||||
}
|
||||
wireframes {
|
||||
id
|
||||
name
|
||||
src
|
||||
}
|
||||
price
|
||||
repo
|
||||
}
|
||||
image {
|
||||
name
|
||||
src
|
||||
}
|
||||
specification {
|
||||
introduction {
|
||||
purpose
|
||||
documentConventions
|
||||
intendedAudience
|
||||
projectScope
|
||||
}
|
||||
overallDescription {
|
||||
perspective
|
||||
userCharacteristics
|
||||
operatingEnvironment
|
||||
designImplementationConstraints
|
||||
userDocumentation
|
||||
assemptionsDependencies
|
||||
}
|
||||
nonFunctionalRequirements {
|
||||
performanceRequirements
|
||||
safetyRequirements
|
||||
securityRequirements
|
||||
softwareQualityAttributes
|
||||
}
|
||||
otherRequirements
|
||||
glossary
|
||||
analysisModels
|
||||
issuesList
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_TEMPLATE_BY_ID = gql`
|
||||
query GetTemplateById($id: String!) {
|
||||
getTemplateById(id: $id) {
|
||||
id
|
||||
name
|
||||
description
|
||||
category
|
||||
features {
|
||||
id
|
||||
name
|
||||
description
|
||||
featureType
|
||||
image {
|
||||
name
|
||||
src
|
||||
}
|
||||
wireframes {
|
||||
id
|
||||
name
|
||||
src
|
||||
}
|
||||
price
|
||||
repo
|
||||
}
|
||||
image {
|
||||
name
|
||||
src
|
||||
}
|
||||
specification {
|
||||
introduction {
|
||||
purpose
|
||||
documentConventions
|
||||
intendedAudience
|
||||
projectScope
|
||||
}
|
||||
overallDescription {
|
||||
perspective
|
||||
userCharacteristics
|
||||
operatingEnvironment
|
||||
designImplementationConstraints
|
||||
userDocumentation
|
||||
assemptionsDependencies
|
||||
}
|
||||
nonFunctionalRequirements {
|
||||
performanceRequirements
|
||||
safetyRequirements
|
||||
securityRequirements
|
||||
softwareQualityAttributes
|
||||
}
|
||||
otherRequirements
|
||||
glossary
|
||||
analysisModels
|
||||
issuesList
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_ALL_TEMPLATES_BY_CATEGORIES_ID = gql`
|
||||
query GetAllTemplatesByCategoriesId($categories: [String!]!) {
|
||||
getAllTemplatesByCategoriesId(categories: $categories) {
|
||||
id
|
||||
name
|
||||
description
|
||||
category
|
||||
features {
|
||||
id
|
||||
name
|
||||
description
|
||||
featureType
|
||||
image {
|
||||
name
|
||||
src
|
||||
}
|
||||
wireframes {
|
||||
id
|
||||
name
|
||||
src
|
||||
}
|
||||
price
|
||||
repo
|
||||
}
|
||||
image {
|
||||
name
|
||||
src
|
||||
}
|
||||
specification {
|
||||
introduction {
|
||||
purpose
|
||||
documentConventions
|
||||
intendedAudience
|
||||
projectScope
|
||||
}
|
||||
overallDescription {
|
||||
perspective
|
||||
userCharacteristics
|
||||
operatingEnvironment
|
||||
designImplementationConstraints
|
||||
userDocumentation
|
||||
assemptionsDependencies
|
||||
}
|
||||
nonFunctionalRequirements {
|
||||
performanceRequirements
|
||||
safetyRequirements
|
||||
securityRequirements
|
||||
softwareQualityAttributes
|
||||
}
|
||||
otherRequirements
|
||||
glossary
|
||||
analysisModels
|
||||
issuesList
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const ADD_TEMPLATE = gql`
|
||||
mutation AddTemplate($template: TemplateInput!) {
|
||||
addTemplate(template: $template) {
|
||||
id
|
||||
name
|
||||
description
|
||||
category
|
||||
features {
|
||||
id
|
||||
name
|
||||
description
|
||||
featureType
|
||||
image {
|
||||
name
|
||||
src
|
||||
}
|
||||
wireframes {
|
||||
id
|
||||
name
|
||||
src
|
||||
}
|
||||
price
|
||||
repo
|
||||
}
|
||||
image {
|
||||
name
|
||||
src
|
||||
}
|
||||
specification {
|
||||
introduction {
|
||||
purpose
|
||||
documentConventions
|
||||
intendedAudience
|
||||
projectScope
|
||||
}
|
||||
overallDescription {
|
||||
perspective
|
||||
userCharacteristics
|
||||
operatingEnvironment
|
||||
designImplementationConstraints
|
||||
userDocumentation
|
||||
assemptionsDependencies
|
||||
}
|
||||
nonFunctionalRequirements {
|
||||
performanceRequirements
|
||||
safetyRequirements
|
||||
securityRequirements
|
||||
softwareQualityAttributes
|
||||
}
|
||||
otherRequirements
|
||||
glossary
|
||||
analysisModels
|
||||
issuesList
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const UPDATE_TEMPLATE = gql`
|
||||
mutation UpdateTemplate(
|
||||
$id: String!
|
||||
$template: TemplateUpdateInput!
|
||||
$specification: SpecificationInput
|
||||
) {
|
||||
updateTemplate(
|
||||
id: $id
|
||||
template: $template
|
||||
specification: $specification
|
||||
) {
|
||||
id
|
||||
name
|
||||
description
|
||||
category
|
||||
features {
|
||||
id
|
||||
name
|
||||
description
|
||||
featureType
|
||||
image {
|
||||
name
|
||||
src
|
||||
}
|
||||
wireframes {
|
||||
id
|
||||
name
|
||||
src
|
||||
}
|
||||
price
|
||||
repo
|
||||
}
|
||||
image {
|
||||
name
|
||||
src
|
||||
}
|
||||
specification {
|
||||
introduction {
|
||||
purpose
|
||||
documentConventions
|
||||
intendedAudience
|
||||
projectScope
|
||||
}
|
||||
overallDescription {
|
||||
perspective
|
||||
userCharacteristics
|
||||
operatingEnvironment
|
||||
designImplementationConstraints
|
||||
userDocumentation
|
||||
assemptionsDependencies
|
||||
}
|
||||
nonFunctionalRequirements {
|
||||
performanceRequirements
|
||||
safetyRequirements
|
||||
securityRequirements
|
||||
softwareQualityAttributes
|
||||
}
|
||||
otherRequirements
|
||||
glossary
|
||||
analysisModels
|
||||
issuesList
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const UPDATE_TEMPLATE_FEATURES = gql`
|
||||
mutation UpdateTemplateFeatures($id: String!, $featuresId: [String!]!) {
|
||||
updateTemplateFeatures(id: $id, featuresId: $featuresId) {
|
||||
id
|
||||
name
|
||||
description
|
||||
category
|
||||
features {
|
||||
id
|
||||
name
|
||||
description
|
||||
featureType
|
||||
image {
|
||||
name
|
||||
src
|
||||
}
|
||||
wireframes {
|
||||
id
|
||||
name
|
||||
src
|
||||
}
|
||||
price
|
||||
repo
|
||||
}
|
||||
image {
|
||||
name
|
||||
src
|
||||
}
|
||||
specification {
|
||||
introduction {
|
||||
purpose
|
||||
documentConventions
|
||||
intendedAudience
|
||||
projectScope
|
||||
}
|
||||
overallDescription {
|
||||
perspective
|
||||
userCharacteristics
|
||||
operatingEnvironment
|
||||
designImplementationConstraints
|
||||
userDocumentation
|
||||
assemptionsDependencies
|
||||
}
|
||||
nonFunctionalRequirements {
|
||||
performanceRequirements
|
||||
safetyRequirements
|
||||
securityRequirements
|
||||
softwareQualityAttributes
|
||||
}
|
||||
otherRequirements
|
||||
glossary
|
||||
analysisModels
|
||||
issuesList
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const ADD_TEMPLATE_SPECIFICATION = gql`
|
||||
mutation AddTemplateSpecification(
|
||||
$id: String!
|
||||
$specification: SpecificationInput!
|
||||
) {
|
||||
addTemplateSpecification(id: $id, specification: $specification) {
|
||||
id
|
||||
name
|
||||
description
|
||||
category
|
||||
features {
|
||||
id
|
||||
name
|
||||
description
|
||||
featureType
|
||||
image {
|
||||
name
|
||||
src
|
||||
}
|
||||
wireframes {
|
||||
id
|
||||
name
|
||||
src
|
||||
}
|
||||
price
|
||||
repo
|
||||
}
|
||||
image {
|
||||
name
|
||||
src
|
||||
}
|
||||
specification {
|
||||
introduction {
|
||||
purpose
|
||||
documentConventions
|
||||
intendedAudience
|
||||
projectScope
|
||||
}
|
||||
overallDescription {
|
||||
perspective
|
||||
userCharacteristics
|
||||
operatingEnvironment
|
||||
designImplementationConstraints
|
||||
userDocumentation
|
||||
assemptionsDependencies
|
||||
}
|
||||
nonFunctionalRequirements {
|
||||
performanceRequirements
|
||||
safetyRequirements
|
||||
securityRequirements
|
||||
softwareQualityAttributes
|
||||
}
|
||||
otherRequirements
|
||||
glossary
|
||||
analysisModels
|
||||
issuesList
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const DELETE_TEMPLATE = gql`
|
||||
mutation DeleteTemplate($id: String!) {
|
||||
deleteTemplate(id: $id) {
|
||||
id
|
||||
name
|
||||
description
|
||||
category
|
||||
features
|
||||
image {
|
||||
name
|
||||
src
|
||||
}
|
||||
specification {
|
||||
introduction {
|
||||
purpose
|
||||
documentConventions
|
||||
intendedAudience
|
||||
projectScope
|
||||
}
|
||||
overallDescription {
|
||||
perspective
|
||||
userCharacteristics
|
||||
operatingEnvironment
|
||||
designImplementationConstraints
|
||||
userDocumentation
|
||||
assemptionsDependencies
|
||||
}
|
||||
nonFunctionalRequirements {
|
||||
performanceRequirements
|
||||
safetyRequirements
|
||||
securityRequirements
|
||||
softwareQualityAttributes
|
||||
}
|
||||
otherRequirements
|
||||
glossary
|
||||
analysisModels
|
||||
issuesList
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -1,180 +0,0 @@
|
||||
/* eslint-disable */
|
||||
export type Maybe<T> = T | null;
|
||||
export type InputMaybe<T> = Maybe<T>;
|
||||
export type Exact<T extends { [key: string]: unknown }> = {
|
||||
[K in keyof T]: T[K];
|
||||
};
|
||||
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & {
|
||||
[SubKey in K]?: Maybe<T[SubKey]>;
|
||||
};
|
||||
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & {
|
||||
[SubKey in K]: Maybe<T[SubKey]>;
|
||||
};
|
||||
/** All built-in and custom scalars, mapped to their actual values */
|
||||
export type Scalars = {
|
||||
ID: string;
|
||||
String: string;
|
||||
Boolean: boolean;
|
||||
Int: number;
|
||||
Float: number;
|
||||
};
|
||||
|
||||
export type MutationRoot = {
|
||||
__typename?: 'MutationRoot';
|
||||
createThread: Support;
|
||||
deleteThread?: Maybe<Support>;
|
||||
sendMessage: Scalars['ID'];
|
||||
};
|
||||
|
||||
export type MutationRootCreateThreadArgs = {
|
||||
projectId: Scalars['String'];
|
||||
threadDescription: Scalars['String'];
|
||||
title: Scalars['String'];
|
||||
};
|
||||
|
||||
export type MutationRootDeleteThreadArgs = {
|
||||
threadId: Scalars['String'];
|
||||
};
|
||||
|
||||
export type MutationRootSendMessageArgs = {
|
||||
text: Scalars['String'];
|
||||
threadId: Scalars['String'];
|
||||
username: Scalars['String'];
|
||||
};
|
||||
|
||||
export enum MutationType {
|
||||
Created = 'CREATED',
|
||||
}
|
||||
|
||||
export type QueryRoot = {
|
||||
__typename?: 'QueryRoot';
|
||||
messages?: Maybe<Array<UserMessages>>;
|
||||
thread?: Maybe<Support>;
|
||||
threads?: Maybe<Array<Support>>;
|
||||
};
|
||||
|
||||
export type QueryRootMessagesArgs = {
|
||||
threadId: Scalars['ID'];
|
||||
};
|
||||
|
||||
export type QueryRootThreadArgs = {
|
||||
threadId: Scalars['ID'];
|
||||
};
|
||||
|
||||
export type QueryRootThreadsArgs = {
|
||||
projectId: Scalars['ID'];
|
||||
};
|
||||
|
||||
export type StreamChanged = {
|
||||
__typename?: 'StreamChanged';
|
||||
id: Scalars['ID'];
|
||||
mutationType: MutationType;
|
||||
userMessages?: Maybe<UserMessages>;
|
||||
};
|
||||
|
||||
export type SubscriptionRoot = {
|
||||
__typename?: 'SubscriptionRoot';
|
||||
interval: Scalars['Int'];
|
||||
messages: StreamChanged;
|
||||
};
|
||||
|
||||
export type SubscriptionRootIntervalArgs = {
|
||||
n?: Scalars['Int'];
|
||||
};
|
||||
|
||||
export type Support = {
|
||||
__typename?: 'Support';
|
||||
id: Scalars['ID'];
|
||||
projectId: Scalars['ID'];
|
||||
threadDescription: Scalars['String'];
|
||||
title: Scalars['String'];
|
||||
userMessages: Array<UserMessages>;
|
||||
};
|
||||
|
||||
export type UserMessages = {
|
||||
__typename?: 'UserMessages';
|
||||
id: Scalars['String'];
|
||||
username: Scalars['String'];
|
||||
text: Scalars['String'];
|
||||
};
|
||||
|
||||
export type GetProjectThreadsQueryVariables = Exact<{
|
||||
projectId: Scalars['String'];
|
||||
}>;
|
||||
|
||||
export type GetProjectThreadsQuery = { __typename?: 'QueryRoot' } & {
|
||||
threads: Array<
|
||||
{ __typename?: 'Support' } & Pick<
|
||||
Support,
|
||||
'id' | 'title' | 'projectId' | 'threadDescription' | 'userMessages'
|
||||
> & {
|
||||
userMessages: Array<
|
||||
{ __typename?: 'UserMessage' } & Pick<
|
||||
UserMessages,
|
||||
'id' | 'username' | 'text'
|
||||
>
|
||||
>;
|
||||
}
|
||||
>;
|
||||
};
|
||||
|
||||
export type GetThreadByIdQueryVariables = Exact<{
|
||||
threadId: Scalars['String'];
|
||||
}>;
|
||||
|
||||
export type GetThreadByIdQuery = { __typename?: 'QueryRoot' } & {
|
||||
thread: { __typename?: 'Support' } & Pick<
|
||||
Support,
|
||||
'id' | 'title' | 'projectId' | 'threadDescription' | 'userMessages'
|
||||
> & {
|
||||
userMessages: Array<
|
||||
{ __typename?: 'UserMessages' } & Pick<UserMessages, 'id' | 'username' | 'text'>
|
||||
>;
|
||||
};
|
||||
};
|
||||
|
||||
export type MessagesQueryVariables = Exact<{
|
||||
threadId: Scalars['String'];
|
||||
}>;
|
||||
|
||||
export type MessagesQuery = { __typename?: 'QueryRoot' } & {
|
||||
messages: Array<
|
||||
{ __typename?: 'UserMessages' } & Pick<UserMessages, 'username' | 'text' | 'id'>
|
||||
>;
|
||||
};
|
||||
|
||||
export type CreateThreadMutationVariables = Exact<{
|
||||
projectId: Scalars['String'];
|
||||
title: Scalars['String'];
|
||||
threadDescription: Scalars['String'];
|
||||
}>;
|
||||
|
||||
export type CreateThreadMutation = { __typename?: 'MutationRoot' } & Pick<
|
||||
MutationRoot,
|
||||
'createThread'
|
||||
>;
|
||||
|
||||
export type SendMsgMutationVariables = Exact<{
|
||||
threadId: Scalars['String'];
|
||||
username: Scalars['String'];
|
||||
text: Scalars['String'];
|
||||
}>;
|
||||
|
||||
export type SendMsgMutation = { __typename?: 'MutationRoot' } & Pick<
|
||||
MutationRoot,
|
||||
'sendMessage'
|
||||
>;
|
||||
|
||||
export type MessagesSubscription = { __typename?: 'SubscriptionRoot' } & {
|
||||
messages: { __typename?: 'StreamChanged' } & Pick<
|
||||
StreamChanged,
|
||||
'mutationType'
|
||||
> & {
|
||||
userMessages?: Maybe<
|
||||
{ __typename?: 'UserMessages' } & Pick<
|
||||
UserMessages,
|
||||
'id' | 'username' | 'text'
|
||||
>
|
||||
>;
|
||||
};
|
||||
};
|
||||
@@ -1,108 +1,61 @@
|
||||
import React from 'react';
|
||||
import * as ReactDOMClient from 'react-dom/client';
|
||||
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
|
||||
import { createClient } from 'graphql-ws';
|
||||
import {
|
||||
ApolloClient,
|
||||
InMemoryCache,
|
||||
ApolloProvider,
|
||||
split,
|
||||
HttpLink,
|
||||
} from '@apollo/client';
|
||||
import { getMainDefinition } from '@apollo/client/utilities';
|
||||
import { setContext } from '@apollo/client/link/context';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { theme } from './themes';
|
||||
import App from './App';
|
||||
import GlobalStyles from './GlobalStyles';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
import Button from './components/Button';
|
||||
import IconButton from './components/IconButton';
|
||||
import Box from './components/Box';
|
||||
import Text from './components/Text';
|
||||
import Link from './components/Link';
|
||||
import Input from './components/Input';
|
||||
import TextArea from './components/TextArea';
|
||||
import Select from './components/Select';
|
||||
import Search from './components/Search';
|
||||
import Avatar from './components/Avatar';
|
||||
import ContextMenu from './components/ContextMenu';
|
||||
import Spinner from './components/Spinner';
|
||||
import Alert from './components/Alert';
|
||||
import CheckBox from './components/CheckBox';
|
||||
import Menu from './components/Menu';
|
||||
import Navbar from './components/Navbar';
|
||||
import Sidebar from './components/Sidebar';
|
||||
import SectionSelector from './components/SectionSelector';
|
||||
import Modal from './components/Modal';
|
||||
import SidebarItem from './components/SidebarItem';
|
||||
import ImagePreview from './components/ImagePreview';
|
||||
import FeatureCard from './components/FeatureCard';
|
||||
import FrontendFeatureCard from './components/FrontendFeatureCard';
|
||||
import BackendFeatureCard from './components/BackendFeatureCard';
|
||||
import Chip from './components/Chip';
|
||||
import CategoryCard from './components/CategoryCard';
|
||||
import TemplateCard from './components/TemplateCard';
|
||||
import SupportSidebar from './components/SupportSidebar';
|
||||
import GlobalStyles from './components/GlobalStyles';
|
||||
|
||||
const httpLinkMain = new HttpLink({
|
||||
uri: import.meta.env.VITE_GRAPHQL_API,
|
||||
});
|
||||
|
||||
const httpLinkSupport = new HttpLink({
|
||||
uri: import.meta.env.VITE_GRAPHQL_SUPPORT_API,
|
||||
});
|
||||
|
||||
const wsLink = new GraphQLWsLink(
|
||||
createClient({
|
||||
url: `${import.meta.env.VITE_GRAPHQL_SUPPORT_SUBSCRIPTIONS_API}`,
|
||||
})
|
||||
);
|
||||
|
||||
const splitLink = split(
|
||||
({ query }) => {
|
||||
const definition = getMainDefinition(query);
|
||||
return (
|
||||
definition.kind === 'OperationDefinition' &&
|
||||
definition.operation === 'subscription'
|
||||
);
|
||||
},
|
||||
wsLink,
|
||||
httpLinkSupport
|
||||
);
|
||||
|
||||
const authLink = setContext((_, { headers }) => {
|
||||
const token = localStorage.getItem('token');
|
||||
|
||||
return {
|
||||
headers: {
|
||||
...headers,
|
||||
authorization: token || '',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const clientMain = new ApolloClient({
|
||||
link: authLink.concat(httpLinkMain),
|
||||
cache: new InMemoryCache(),
|
||||
defaultOptions: {
|
||||
watchQuery: {
|
||||
fetchPolicy: 'network-only',
|
||||
nextFetchPolicy: 'cache-and-network',
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
export const clientSupport = new ApolloClient({
|
||||
link: authLink.concat(splitLink),
|
||||
cache: new InMemoryCache(),
|
||||
defaultOptions: {
|
||||
watchQuery: {
|
||||
fetchPolicy: 'network-only',
|
||||
nextFetchPolicy: 'cache-and-network',
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
let root: ReactDOMClient.Root | null = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (!root) {
|
||||
root = ReactDOMClient.createRoot(
|
||||
document.querySelector('#app') as HTMLElement
|
||||
);
|
||||
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<ApolloProvider client={clientMain}>
|
||||
{/* @ts-ignore */}
|
||||
<ThemeProvider theme={theme}>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
{/* @ts-ignore */}
|
||||
<GlobalStyles />
|
||||
</BrowserRouter>
|
||||
</ThemeProvider>
|
||||
</ApolloProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
reportWebVitals();
|
||||
export {
|
||||
Button,
|
||||
IconButton,
|
||||
Box,
|
||||
Text,
|
||||
Link,
|
||||
Input,
|
||||
TextArea,
|
||||
Select,
|
||||
Search,
|
||||
Avatar,
|
||||
ContextMenu,
|
||||
Menu,
|
||||
Spinner,
|
||||
Alert,
|
||||
CheckBox,
|
||||
Navbar,
|
||||
Sidebar,
|
||||
SectionSelector,
|
||||
Modal,
|
||||
SidebarItem,
|
||||
ImagePreview,
|
||||
FeatureCard,
|
||||
FrontendFeatureCard,
|
||||
BackendFeatureCard,
|
||||
Chip,
|
||||
CategoryCard,
|
||||
TemplateCard,
|
||||
SupportSidebar,
|
||||
GlobalStyles,
|
||||
};
|
||||
|
||||
@@ -1,201 +0,0 @@
|
||||
import * as Yup from 'yup';
|
||||
import { useFormik } from 'formik';
|
||||
import { Navigate, useNavigate } from 'react-router';
|
||||
import { useMutation, useReactiveVar } from '@apollo/client';
|
||||
import React, { useState } from 'react';
|
||||
import { roleVar } from '../../graphql/state';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Text,
|
||||
SectionSelector,
|
||||
Input,
|
||||
Alert,
|
||||
TextArea,
|
||||
} from '../../components';
|
||||
import { Wrapper } from './styles';
|
||||
import { ArrowLeft, General } from '../../assets';
|
||||
import {
|
||||
AddCategoryMutation,
|
||||
AddCategoryMutationVariables,
|
||||
} from '../../graphql/types';
|
||||
import { ADD_CATEGORY } from '../../graphql/category.api';
|
||||
|
||||
const AddCategory = () => {
|
||||
const navigate = useNavigate();
|
||||
const role = useReactiveVar(roleVar);
|
||||
|
||||
const [error, setError] = useState<string>('');
|
||||
|
||||
const [addCategory, { loading }] = useMutation<
|
||||
AddCategoryMutation,
|
||||
AddCategoryMutationVariables
|
||||
>(ADD_CATEGORY, {
|
||||
onCompleted({ addCategory: { id } }) {
|
||||
navigate(`/category/${id}`);
|
||||
},
|
||||
onError({ graphQLErrors }) {
|
||||
setError(graphQLErrors[0]?.extensions?.info as string);
|
||||
setTimeout(() => setError(''), 3000);
|
||||
},
|
||||
});
|
||||
|
||||
const form = useFormik({
|
||||
initialValues: {
|
||||
name: '',
|
||||
description: '',
|
||||
imageName: '',
|
||||
imageSource: '',
|
||||
},
|
||||
validationSchema: Yup.object().shape({
|
||||
name: Yup.string().required('Name is required'),
|
||||
description: Yup.string().required('Description is required'),
|
||||
imageName: Yup.string().required('Image is required'),
|
||||
imageSource: Yup.string().required('Image is required'),
|
||||
}),
|
||||
onSubmit: ({ name, description, imageName, imageSource }) => {
|
||||
addCategory({
|
||||
variables: {
|
||||
category: {
|
||||
name,
|
||||
description,
|
||||
image: { name: imageName, src: imageSource },
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
if (role !== 'developer') return (
|
||||
<>
|
||||
{role === 'admin' && <Navigate to='/clients' />}
|
||||
{['client', 'productOwer'].includes(role as string) && <Navigate to='/project' />}
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Box>
|
||||
<Button
|
||||
text='Back'
|
||||
color={role || 'client'}
|
||||
size='small'
|
||||
onClick={() => navigate(-1)}
|
||||
iconLeft={<ArrowLeft />}
|
||||
/>
|
||||
<Text variant='headline' weight='bold'>
|
||||
Add Category
|
||||
</Text>
|
||||
</Box>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='0.5fr 2fr'
|
||||
columnGap='25px'
|
||||
marginTop='1rem'
|
||||
>
|
||||
<Box display='grid' rowGap='0.5rem' gridTemplateRows='50px'>
|
||||
<SectionSelector
|
||||
icon={<General />}
|
||||
color={role || 'client'}
|
||||
text='General'
|
||||
selected
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
background='white'
|
||||
boxShadow='1px 1px 10px 0px rgba(50, 59, 105, 0.25)'
|
||||
borderRadius='10px'
|
||||
width='100%'
|
||||
padding='30px'
|
||||
>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='auto 1fr'
|
||||
columnGap='1rem'
|
||||
alignItems='center'
|
||||
marginBottom='50px'
|
||||
>
|
||||
<Text variant='subheader' weight='bold'>
|
||||
General
|
||||
</Text>
|
||||
{error && <Alert color='error' text={error} />}
|
||||
</Box>
|
||||
<form onSubmit={form.handleSubmit}>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='auto'
|
||||
rowGap='0.5rem'
|
||||
position='relative'
|
||||
>
|
||||
<Input
|
||||
name='name'
|
||||
label='Name'
|
||||
color={role || 'client'}
|
||||
value={form.values.name}
|
||||
onChange={form.handleChange}
|
||||
onBlur={form.handleBlur}
|
||||
error={form.touched.name && !!form.errors.name}
|
||||
errorMessage={form.errors.name}
|
||||
/>
|
||||
<Input
|
||||
type='file'
|
||||
label='Image'
|
||||
color={role || 'client'}
|
||||
onChange={async (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
const formData = new FormData();
|
||||
|
||||
if (event.target.files && event.target.files[0]) {
|
||||
formData.append('file', event.target.files[0]);
|
||||
formData.append('upload_preset', 'xofll5kc');
|
||||
|
||||
const data = await (
|
||||
await fetch(`${import.meta.env.VITE_CLOUDINARY_URL}`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
).json();
|
||||
|
||||
const filename = data.original_filename;
|
||||
const filesource = data.secure_url;
|
||||
|
||||
form.setFieldValue('imageName', filename);
|
||||
form.setFieldValue('imageSource', filesource);
|
||||
}
|
||||
}}
|
||||
error={
|
||||
form.touched.imageName &&
|
||||
(!!form.errors.imageName || !!form.errors.imageSource)
|
||||
}
|
||||
errorMessage={form.errors.imageName}
|
||||
/>
|
||||
<TextArea
|
||||
name='description'
|
||||
label='Description'
|
||||
color={role || 'client'}
|
||||
value={form.values.description}
|
||||
onChange={form.handleChange}
|
||||
onBlur={form.handleBlur}
|
||||
error={form.touched.description && !!form.errors.description}
|
||||
errorMessage={form.errors.description}
|
||||
/>
|
||||
<Box marginTop='0.5rem' display='flex' justifyContent='flex-end'>
|
||||
<Button
|
||||
variant='primary-action'
|
||||
color={role || 'client'}
|
||||
text='Save'
|
||||
type='submit'
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</form>
|
||||
</Box>
|
||||
</Box>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddCategory;
|
||||
@@ -1,5 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
padding: 35px 45px 35px 120px;
|
||||
`;
|
||||
@@ -1,489 +0,0 @@
|
||||
import * as Yup from 'yup';
|
||||
import { useFormik } from 'formik';
|
||||
import { Navigate, useNavigate } from 'react-router';
|
||||
import { useMutation, useReactiveVar } from '@apollo/client';
|
||||
import React, { useState } from 'react';
|
||||
import { roleVar } from '../../graphql/state';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Text,
|
||||
SectionSelector,
|
||||
Input,
|
||||
Alert,
|
||||
TextArea,
|
||||
CheckBox,
|
||||
ImagePreview,
|
||||
} from '../../components';
|
||||
import { Wrapper } from './styles';
|
||||
import { ArrowLeft, General, Design } from '../../assets';
|
||||
import {
|
||||
AddFeatureMutation,
|
||||
AddFeatureMutationVariables,
|
||||
} from '../../graphql/types';
|
||||
import { ADD_FEATURE } from '../../graphql/feature.api';
|
||||
|
||||
const AddFeature = () => {
|
||||
const navigate = useNavigate();
|
||||
const role = useReactiveVar(roleVar);
|
||||
const [newFeature, setNewFeature] = useState<{
|
||||
name: string;
|
||||
description: string;
|
||||
featureType: string;
|
||||
image: {
|
||||
name: string;
|
||||
src: string;
|
||||
};
|
||||
wireframes?: Array<{
|
||||
name: string;
|
||||
src: string;
|
||||
}>;
|
||||
price: number;
|
||||
repo: string;
|
||||
}>({
|
||||
name: '',
|
||||
description: '',
|
||||
featureType: '',
|
||||
image: {
|
||||
name: '',
|
||||
src: '',
|
||||
},
|
||||
price: 0,
|
||||
repo: '',
|
||||
});
|
||||
|
||||
const [selectedSection, setSelectedSection] = useState<
|
||||
'general' | 'wireframes'
|
||||
>('general');
|
||||
const [error, setError] = useState<string>('');
|
||||
|
||||
const [addFeature, { loading }] = useMutation<
|
||||
AddFeatureMutation,
|
||||
AddFeatureMutationVariables
|
||||
>(ADD_FEATURE, {
|
||||
onCompleted({ addFeature: { id } }) {
|
||||
navigate(`/feature/${id}`);
|
||||
},
|
||||
onError({ graphQLErrors }) {
|
||||
setError(graphQLErrors[0]?.extensions?.info as string);
|
||||
setTimeout(() => setError(''), 3000);
|
||||
},
|
||||
});
|
||||
|
||||
const generalForm = useFormik({
|
||||
initialValues: {
|
||||
name: '',
|
||||
description: '',
|
||||
imageName: '',
|
||||
imageSource: '',
|
||||
featureType: '',
|
||||
price: '',
|
||||
repo: '',
|
||||
},
|
||||
validationSchema: Yup.object().shape({
|
||||
name: Yup.string().required('Name is required'),
|
||||
description: Yup.string().required('Description is required'),
|
||||
imageName: Yup.string().required('Image is required'),
|
||||
imageSource: Yup.string().required('Image is required'),
|
||||
featureType: Yup.string().required('Feature Type is required'),
|
||||
price: Yup.number().typeError('Price must be a number').required('Price is required'),
|
||||
repo: Yup.string().required('Repo is required'),
|
||||
}),
|
||||
onSubmit: ({
|
||||
name,
|
||||
description,
|
||||
imageName,
|
||||
imageSource,
|
||||
featureType,
|
||||
price,
|
||||
repo,
|
||||
}) => {
|
||||
setNewFeature({
|
||||
name,
|
||||
description,
|
||||
featureType,
|
||||
image: { name: imageName, src: imageSource },
|
||||
price: parseFloat(price),
|
||||
repo,
|
||||
});
|
||||
setSelectedSection('wireframes');
|
||||
},
|
||||
});
|
||||
|
||||
const wireframesForm = useFormik<{
|
||||
wireframes: Array<{ name: string; src: string }>;
|
||||
}>({
|
||||
initialValues: {
|
||||
wireframes: [],
|
||||
},
|
||||
onSubmit: ({ wireframes }) => {
|
||||
addFeature({ variables: { feature: { ...newFeature, wireframes } } });
|
||||
},
|
||||
});
|
||||
|
||||
if (role !== 'developer') return (
|
||||
<>
|
||||
{role === 'admin' && <Navigate to='/clients' />}
|
||||
{['client', 'productOwer'].includes(role as string) && <Navigate to='/project' />}
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Box>
|
||||
<Button
|
||||
text='Back'
|
||||
color={role || 'client'}
|
||||
size='small'
|
||||
onClick={() => navigate(-1)}
|
||||
iconLeft={<ArrowLeft />}
|
||||
/>
|
||||
<Text variant='headline' weight='bold'>
|
||||
Add Feature
|
||||
</Text>
|
||||
</Box>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='0.5fr 2fr'
|
||||
columnGap='25px'
|
||||
marginTop='1rem'
|
||||
>
|
||||
<Box display='grid' rowGap='0.5rem' gridTemplateRows='50px'>
|
||||
<SectionSelector
|
||||
icon={<General />}
|
||||
color={role || 'client'}
|
||||
text='General'
|
||||
selected={selectedSection === 'general'}
|
||||
/>
|
||||
<SectionSelector
|
||||
icon={<Design />}
|
||||
color={role || 'client'}
|
||||
text='Wireframes'
|
||||
selected={selectedSection === 'wireframes'}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
background='white'
|
||||
boxShadow='1px 1px 10px 0px rgba(50, 59, 105, 0.25)'
|
||||
borderRadius='10px'
|
||||
width='100%'
|
||||
padding='30px'
|
||||
>
|
||||
{selectedSection === 'general' && (
|
||||
<>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='auto 1fr'
|
||||
columnGap='1rem'
|
||||
alignItems='center'
|
||||
marginBottom='50px'
|
||||
>
|
||||
<Text variant='subheader' weight='bold'>
|
||||
{selectedSection === 'general' ? 'General' : 'Wireframes'}
|
||||
</Text>
|
||||
{error && <Alert color='error' text={error} />}
|
||||
</Box>
|
||||
<form onSubmit={generalForm.handleSubmit}>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='auto'
|
||||
rowGap='0.5rem'
|
||||
position='relative'
|
||||
>
|
||||
<Input
|
||||
name='name'
|
||||
label='Name'
|
||||
color={role || 'client'}
|
||||
value={generalForm.values.name}
|
||||
onChange={generalForm.handleChange}
|
||||
onBlur={generalForm.handleBlur}
|
||||
error={
|
||||
generalForm.touched.name && !!generalForm.errors.name
|
||||
}
|
||||
errorMessage={generalForm.errors.name}
|
||||
/>
|
||||
<Input
|
||||
type='file'
|
||||
label='Image'
|
||||
color={role || 'client'}
|
||||
onChange={async (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
const formData = new FormData();
|
||||
|
||||
if (event.target.files && event.target.files[0]) {
|
||||
formData.append('file', event.target.files[0]);
|
||||
formData.append('upload_preset', 'xofll5kc');
|
||||
|
||||
generalForm.setFieldValue('imageName', '');
|
||||
generalForm.setFieldValue('imageSource', '');
|
||||
|
||||
const data = await (
|
||||
await fetch(
|
||||
`${import.meta.env.VITE_CLOUDINARY_URL}`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
const filename = data.original_filename;
|
||||
const filesource = data.secure_url;
|
||||
|
||||
generalForm.setFieldValue('imageName', filename);
|
||||
generalForm.setFieldValue('imageSource', filesource);
|
||||
}
|
||||
}}
|
||||
error={
|
||||
generalForm.touched.imageName &&
|
||||
(!!generalForm.errors.imageName ||
|
||||
!!generalForm.errors.imageSource)
|
||||
}
|
||||
errorMessage={generalForm.errors.imageName}
|
||||
/>
|
||||
<Box>
|
||||
<Box
|
||||
display='flex'
|
||||
flexDirection='row'
|
||||
alignItems='center'
|
||||
justifyContent='space-between'
|
||||
>
|
||||
<Box justifySelf='flex-start'>
|
||||
<Text
|
||||
variant='body'
|
||||
weight='bold'
|
||||
className='feature-type'
|
||||
>
|
||||
Type
|
||||
</Text>
|
||||
</Box>
|
||||
{!!generalForm.touched.featureType &&
|
||||
generalForm.errors.featureType && (
|
||||
<Box justifySelf='flex-end'>
|
||||
<Text variant='body' color='error'>
|
||||
{generalForm.errors.featureType}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<Box
|
||||
display='flex'
|
||||
flexDirection='row'
|
||||
alignItems='center'
|
||||
marginTop='5px'
|
||||
>
|
||||
<Box marginRight='50px'>
|
||||
<CheckBox
|
||||
label='Frontend'
|
||||
name='featureType'
|
||||
color={role || 'client'}
|
||||
onClick={() => {
|
||||
if (
|
||||
generalForm.values.featureType === 'fullstack'
|
||||
) {
|
||||
generalForm.setFieldValue(
|
||||
'featureType',
|
||||
'backend'
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (generalForm.values.featureType === 'backend') {
|
||||
generalForm.setFieldValue(
|
||||
'featureType',
|
||||
'fullstack'
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (generalForm.values.featureType === 'frontend') {
|
||||
generalForm.setFieldValue('featureType', '');
|
||||
return;
|
||||
}
|
||||
generalForm.setFieldValue(
|
||||
'featureType',
|
||||
'frontend'
|
||||
);
|
||||
}}
|
||||
checked={
|
||||
generalForm.values.featureType === 'frontend' ||
|
||||
generalForm.values.featureType === 'fullstack'
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<CheckBox
|
||||
label='Backend'
|
||||
name='featureType'
|
||||
color={role || 'client'}
|
||||
onClick={() => {
|
||||
if (
|
||||
generalForm.values.featureType === 'fullstack'
|
||||
) {
|
||||
generalForm.setFieldValue(
|
||||
'featureType',
|
||||
'frontend'
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (generalForm.values.featureType === 'frontend') {
|
||||
generalForm.setFieldValue(
|
||||
'featureType',
|
||||
'fullstack'
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (generalForm.values.featureType === 'backend') {
|
||||
generalForm.setFieldValue('featureType', '');
|
||||
return;
|
||||
}
|
||||
generalForm.setFieldValue('featureType', 'backend');
|
||||
}}
|
||||
checked={
|
||||
generalForm.values.featureType === 'backend' ||
|
||||
generalForm.values.featureType === 'fullstack'
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<TextArea
|
||||
name='description'
|
||||
label='Description'
|
||||
color={role || 'client'}
|
||||
value={generalForm.values.description}
|
||||
onChange={generalForm.handleChange}
|
||||
onBlur={generalForm.handleBlur}
|
||||
error={
|
||||
generalForm.touched.description &&
|
||||
!!generalForm.errors.description
|
||||
}
|
||||
errorMessage={generalForm.errors.description}
|
||||
/>
|
||||
<Input
|
||||
name='price'
|
||||
label='Price'
|
||||
color={role || 'client'}
|
||||
value={generalForm.values.price}
|
||||
onChange={generalForm.handleChange}
|
||||
onBlur={generalForm.handleBlur}
|
||||
error={
|
||||
generalForm.touched.price && !!generalForm.errors.price
|
||||
}
|
||||
errorMessage={generalForm.errors.price}
|
||||
/>
|
||||
<Input
|
||||
name='repo'
|
||||
label='Repo'
|
||||
color={role || 'client'}
|
||||
value={generalForm.values.repo}
|
||||
onChange={generalForm.handleChange}
|
||||
onBlur={generalForm.handleBlur}
|
||||
error={
|
||||
generalForm.touched.repo && !!generalForm.errors.repo
|
||||
}
|
||||
errorMessage={generalForm.errors.repo}
|
||||
/>
|
||||
<Box
|
||||
marginTop='0.5rem'
|
||||
display='flex'
|
||||
justifyContent='flex-end'
|
||||
>
|
||||
<Button
|
||||
variant='primary-action'
|
||||
color={role || 'client'}
|
||||
text='Next'
|
||||
type='submit'
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
{selectedSection === 'wireframes' && (
|
||||
<>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='auto 1fr'
|
||||
columnGap='1rem'
|
||||
alignItems='center'
|
||||
marginBottom='50px'
|
||||
>
|
||||
<Text variant='subheader' weight='bold'>
|
||||
Wireframes
|
||||
</Text>
|
||||
{error && <Alert color='error' text={error} />}
|
||||
</Box>
|
||||
<form onSubmit={wireframesForm.handleSubmit}>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplate='auto / repeat(auto-fit, 175px)'
|
||||
gap='30px'
|
||||
alignItems='stretch'
|
||||
>
|
||||
<ImagePreview
|
||||
color={role}
|
||||
image={undefined}
|
||||
onChange={async (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
const formData = new FormData();
|
||||
|
||||
if (event.target.files && event.target.files[0]) {
|
||||
formData.append('file', event.target.files[0]);
|
||||
formData.append('upload_preset', 'xofll5kc');
|
||||
|
||||
const data = await (
|
||||
await fetch(
|
||||
`${import.meta.env.VITE_CLOUDINARY_URL}`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
const filename = data.original_filename;
|
||||
const filesource = data.secure_url;
|
||||
|
||||
wireframesForm.setFieldValue('wireframes', [
|
||||
...wireframesForm.values.wireframes,
|
||||
{ name: filename, src: filesource },
|
||||
]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{wireframesForm.values.wireframes.map((image) => (
|
||||
<ImagePreview
|
||||
key={image.name}
|
||||
color={role}
|
||||
image={image}
|
||||
onDelete={() => {
|
||||
wireframesForm.setFieldValue(
|
||||
'wireframes',
|
||||
wireframesForm.values.wireframes.filter(
|
||||
({ name }) => name !== image.name
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
<Box marginTop='1rem' display='flex' justifyContent='flex-end'>
|
||||
<Button
|
||||
variant='primary-action'
|
||||
color={role || 'client'}
|
||||
text='Save'
|
||||
type='submit'
|
||||
loading={loading}
|
||||
/>
|
||||
</Box>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddFeature;
|
||||
@@ -1,11 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
padding: 35px 45px 35px 120px;
|
||||
|
||||
.feature-type {
|
||||
background: ${({ theme }) => theme.colors.gray.dark};
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
`;
|
||||
@@ -1,28 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
type WrapperProps = {
|
||||
color?: 'client' | 'productOwner' | 'developer' | 'admin';
|
||||
};
|
||||
|
||||
export const Wrapper = styled.div<WrapperProps>`
|
||||
.carousel-arrow {
|
||||
background: none;
|
||||
border: none;
|
||||
align-self: center;
|
||||
cursor: pointer;
|
||||
|
||||
svg {
|
||||
stroke: ${({ theme, color }) => theme.colors[color || 'client'].main};
|
||||
}
|
||||
}
|
||||
|
||||
.wireframe {
|
||||
img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -1,833 +0,0 @@
|
||||
import * as Yup from 'yup';
|
||||
import { useFormik } from 'formik';
|
||||
import { Navigate, useNavigate } from 'react-router';
|
||||
import {
|
||||
useLazyQuery,
|
||||
useMutation,
|
||||
useQuery,
|
||||
useReactiveVar,
|
||||
} from '@apollo/client';
|
||||
import React, { useState } from 'react';
|
||||
import { roleVar } from '../../graphql/state';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Text,
|
||||
SectionSelector,
|
||||
Input,
|
||||
Alert,
|
||||
TextArea,
|
||||
Select,
|
||||
Spinner,
|
||||
FeatureCard,
|
||||
} from '../../components';
|
||||
import { Wrapper } from './styles';
|
||||
import { ArrowLeft, General, Specification, Features, Empty } from '../../assets';
|
||||
import {
|
||||
AddTemplateMutation,
|
||||
AddTemplateMutationVariables,
|
||||
FeatureOutput,
|
||||
GetAllCategoriesQuery,
|
||||
GetAllCategoriesQueryVariables,
|
||||
GetAllFeaturesQuery,
|
||||
GetAllFeaturesQueryVariables,
|
||||
TemplateInput,
|
||||
} from '../../graphql/types';
|
||||
import { ADD_TEMPLATE } from '../../graphql/template.api';
|
||||
import { GET_ALL_CATEGORIES } from '../../graphql/category.api';
|
||||
import { GET_ALL_FEATURES } from '../../graphql/feature.api';
|
||||
|
||||
const AddTemplate = () => {
|
||||
const navigate = useNavigate();
|
||||
const role = useReactiveVar(roleVar);
|
||||
const [newTemplate, setNewTemplate] = useState<TemplateInput>({
|
||||
name: '',
|
||||
description: '',
|
||||
image: {
|
||||
name: '',
|
||||
src: '',
|
||||
},
|
||||
category: '',
|
||||
specification: {
|
||||
introduction: {
|
||||
purpose: '',
|
||||
documentConventions: '',
|
||||
intendedAudience: '',
|
||||
projectScope: '',
|
||||
},
|
||||
overallDescription: {
|
||||
perspective: '',
|
||||
userCharacteristics: '',
|
||||
operatingEnvironment: '',
|
||||
designImplementationConstraints: '',
|
||||
userDocumentation: '',
|
||||
assemptionsDependencies: '',
|
||||
},
|
||||
nonFunctionalRequirements: {
|
||||
performanceRequirements: '',
|
||||
safetyRequirements: '',
|
||||
securityRequirements: '',
|
||||
softwareQualityAttributes: '',
|
||||
},
|
||||
otherRequirements: '',
|
||||
glossary: '',
|
||||
analysisModels: '',
|
||||
issuesList: '',
|
||||
},
|
||||
features: [],
|
||||
});
|
||||
|
||||
const [availableFeatures, setAvailableFeatures] =
|
||||
useState<Array<FeatureOutput>>();
|
||||
|
||||
const [selectedSection, setSelectedSection] = useState<
|
||||
'general' | 'specification' | 'features'
|
||||
>('general');
|
||||
const [error, setError] = useState<string>('');
|
||||
|
||||
const { data: categories, loading: categoriesLoading, error: categoriesError } = useQuery<
|
||||
GetAllCategoriesQuery,
|
||||
GetAllCategoriesQueryVariables
|
||||
>(GET_ALL_CATEGORIES);
|
||||
|
||||
const [getFeatures, { loading: featuresLoading, error: featuresError }] = useLazyQuery<
|
||||
GetAllFeaturesQuery,
|
||||
GetAllFeaturesQueryVariables
|
||||
>(GET_ALL_FEATURES, {
|
||||
onCompleted({ getAllFeatures }) {
|
||||
setAvailableFeatures(getAllFeatures);
|
||||
}
|
||||
});
|
||||
|
||||
const [addTemplate, { loading }] = useMutation<
|
||||
AddTemplateMutation,
|
||||
AddTemplateMutationVariables
|
||||
>(ADD_TEMPLATE, {
|
||||
onCompleted({ addTemplate: { id } }) {
|
||||
navigate(`/template/${id}`);
|
||||
},
|
||||
onError({ graphQLErrors }) {
|
||||
setError(graphQLErrors[0]?.extensions?.info as string);
|
||||
setTimeout(() => setError(''), 3000);
|
||||
},
|
||||
});
|
||||
|
||||
const generalForm = useFormik({
|
||||
initialValues: {
|
||||
name: '',
|
||||
description: '',
|
||||
imageName: '',
|
||||
imageSource: '',
|
||||
category: '',
|
||||
},
|
||||
validationSchema: Yup.object().shape({
|
||||
name: Yup.string().required('Name is required'),
|
||||
description: Yup.string().required('Description is required'),
|
||||
imageName: Yup.string().required('Image is required'),
|
||||
imageSource: Yup.string().required('Image is required'),
|
||||
category: Yup.string().required('Category is required'),
|
||||
}),
|
||||
onSubmit: ({ name, description, category, imageName, imageSource }) => {
|
||||
setNewTemplate({
|
||||
name,
|
||||
description,
|
||||
image: { name: imageName, src: imageSource },
|
||||
category,
|
||||
});
|
||||
setSelectedSection('specification');
|
||||
},
|
||||
});
|
||||
|
||||
const specificationForm = useFormik({
|
||||
initialValues: {
|
||||
purpose: '',
|
||||
documentConventions: '',
|
||||
intendedAudience: '',
|
||||
projectScope: '',
|
||||
perspective: '',
|
||||
userCharacteristics: '',
|
||||
operatingEnvironment: '',
|
||||
designImplementationConstraints: '',
|
||||
userDocumentation: '',
|
||||
assemptionsDependencies: '',
|
||||
performanceRequirements: '',
|
||||
safetyRequirements: '',
|
||||
securityRequirements: '',
|
||||
softwareQualityAttributes: '',
|
||||
otherRequirements: '',
|
||||
glossary: '',
|
||||
analysisModels: '',
|
||||
issuesList: '',
|
||||
},
|
||||
validationSchema: Yup.object().shape({
|
||||
purpose: Yup.string().required('Purpose is required'),
|
||||
documentConventions: Yup.string().required(
|
||||
'Document conventions is required'
|
||||
),
|
||||
intendedAudience: Yup.string().required('Intented audience is required'),
|
||||
projectScope: Yup.string().required('Project scope is required'),
|
||||
perspective: Yup.string().required('Perspective is required'),
|
||||
userCharacteristics: Yup.string().required(
|
||||
'User characteristics is required'
|
||||
),
|
||||
operatingEnvironment: Yup.string().required(
|
||||
'Operating environment is required'
|
||||
),
|
||||
designImplementationConstraints: Yup.string().required(
|
||||
'Design and implementation constraints is required'
|
||||
),
|
||||
userDocumentation: Yup.string().required(
|
||||
'User documentation is required'
|
||||
),
|
||||
assemptionsDependencies: Yup.string().required(
|
||||
'Assumptions and dependencies is required'
|
||||
),
|
||||
performanceRequirements: Yup.string().required(
|
||||
'Performance requirements is required'
|
||||
),
|
||||
safetyRequirements: Yup.string().required(
|
||||
'Safety requirements is required'
|
||||
),
|
||||
securityRequirements: Yup.string().required(
|
||||
'Security requirements is required'
|
||||
),
|
||||
softwareQualityAttributes: Yup.string().required(
|
||||
'Software quality attributes is required'
|
||||
),
|
||||
otherRequirements: Yup.string().required(
|
||||
'Other requirements is required'
|
||||
),
|
||||
glossary: Yup.string().required('Glossary is required'),
|
||||
analysisModels: Yup.string().required('Analysis models is required'),
|
||||
issuesList: Yup.string().required('Issues list is required'),
|
||||
}),
|
||||
onSubmit: ({
|
||||
purpose,
|
||||
documentConventions,
|
||||
intendedAudience,
|
||||
projectScope,
|
||||
perspective,
|
||||
userCharacteristics,
|
||||
operatingEnvironment,
|
||||
designImplementationConstraints,
|
||||
userDocumentation,
|
||||
assemptionsDependencies,
|
||||
performanceRequirements,
|
||||
safetyRequirements,
|
||||
securityRequirements,
|
||||
softwareQualityAttributes,
|
||||
otherRequirements,
|
||||
glossary,
|
||||
analysisModels,
|
||||
issuesList,
|
||||
}) => {
|
||||
setNewTemplate({
|
||||
...newTemplate,
|
||||
specification: {
|
||||
introduction: {
|
||||
purpose,
|
||||
documentConventions,
|
||||
intendedAudience,
|
||||
projectScope,
|
||||
},
|
||||
overallDescription: {
|
||||
perspective,
|
||||
userCharacteristics,
|
||||
operatingEnvironment,
|
||||
designImplementationConstraints,
|
||||
userDocumentation,
|
||||
assemptionsDependencies,
|
||||
},
|
||||
nonFunctionalRequirements: {
|
||||
performanceRequirements,
|
||||
safetyRequirements,
|
||||
securityRequirements,
|
||||
softwareQualityAttributes,
|
||||
},
|
||||
otherRequirements,
|
||||
glossary,
|
||||
analysisModels,
|
||||
issuesList,
|
||||
},
|
||||
});
|
||||
setSelectedSection('features');
|
||||
getFeatures();
|
||||
},
|
||||
});
|
||||
|
||||
const featuresForm = useFormik<{ features: Array<string> }>({
|
||||
initialValues: {
|
||||
features: [],
|
||||
},
|
||||
onSubmit: ({ features }) => {
|
||||
addTemplate({ variables: { template: { ...newTemplate, features } } });
|
||||
},
|
||||
});
|
||||
|
||||
if (role !== 'productOwner') return (
|
||||
<>
|
||||
{role === 'admin' && <Navigate to='/clients' />}
|
||||
{['client', 'developer'].includes(role as string) && <Navigate to='/project' />}
|
||||
</>
|
||||
);
|
||||
|
||||
if (categoriesLoading || featuresLoading) return (
|
||||
<Spinner fullScreen color={role || 'client'} />
|
||||
);
|
||||
|
||||
if (categoriesError || featuresError || !categories) return (
|
||||
<Wrapper color={role}>
|
||||
<Box
|
||||
width='100%'
|
||||
height='100vh'
|
||||
display='grid'
|
||||
alignItems='center'
|
||||
justifyContent='center'
|
||||
>
|
||||
<Box>
|
||||
<Empty />
|
||||
</Box>
|
||||
</Box>
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Box>
|
||||
<Button
|
||||
text='Back'
|
||||
color={role || 'client'}
|
||||
size='small'
|
||||
onClick={() => navigate(-1)}
|
||||
iconLeft={<ArrowLeft />}
|
||||
/>
|
||||
<Text variant='headline' weight='bold'>
|
||||
Add Template
|
||||
</Text>
|
||||
</Box>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='0.5fr 2fr'
|
||||
columnGap='25px'
|
||||
marginTop='1rem'
|
||||
>
|
||||
<Box display='grid' rowGap='0.5rem' gridTemplateRows='repeat(3, 50px)'>
|
||||
<SectionSelector
|
||||
icon={<General />}
|
||||
color={role || 'client'}
|
||||
text='General'
|
||||
selected={selectedSection === 'general'}
|
||||
/>
|
||||
<SectionSelector
|
||||
icon={<Specification />}
|
||||
color={role || 'client'}
|
||||
text='Specification'
|
||||
selected={selectedSection === 'specification'}
|
||||
/>
|
||||
<SectionSelector
|
||||
icon={<Features />}
|
||||
color={role || 'client'}
|
||||
text='Features'
|
||||
selected={selectedSection === 'features'}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
background='white'
|
||||
boxShadow='1px 1px 10px 0px rgba(50, 59, 105, 0.25)'
|
||||
borderRadius='10px'
|
||||
width='100%'
|
||||
padding='30px'
|
||||
>
|
||||
{selectedSection === 'general' && (
|
||||
<>
|
||||
{!categoriesLoading ? (
|
||||
<>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='auto 1fr'
|
||||
columnGap='1rem'
|
||||
alignItems='center'
|
||||
marginBottom='50px'
|
||||
>
|
||||
<Text variant='subheader' weight='bold'>
|
||||
{selectedSection === 'general' ? 'General' : 'Wireframes'}
|
||||
</Text>
|
||||
{error && <Alert color='error' text={error} />}
|
||||
</Box>
|
||||
<form onSubmit={generalForm.handleSubmit}>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='auto'
|
||||
rowGap='0.5rem'
|
||||
position='relative'
|
||||
>
|
||||
<Input
|
||||
name='name'
|
||||
label='Name'
|
||||
color={role || 'client'}
|
||||
value={generalForm.values.name}
|
||||
onChange={generalForm.handleChange}
|
||||
onBlur={generalForm.handleBlur}
|
||||
error={
|
||||
generalForm.touched.name && !!generalForm.errors.name
|
||||
}
|
||||
errorMessage={generalForm.errors.name}
|
||||
/>
|
||||
<Input
|
||||
type='file'
|
||||
label='Image'
|
||||
color={role || 'client'}
|
||||
onChange={async (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
const formData = new FormData();
|
||||
|
||||
if (event.target.files && event.target.files[0]) {
|
||||
formData.append('file', event.target.files[0]);
|
||||
formData.append('upload_preset', 'xofll5kc');
|
||||
|
||||
generalForm.setFieldValue('imageName', '');
|
||||
generalForm.setFieldValue('imageSource', '');
|
||||
|
||||
const data = await (
|
||||
await fetch(
|
||||
`${import.meta.env.VITE_CLOUDINARY_URL}`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
const filename = data.original_filename;
|
||||
const filesource = data.secure_url;
|
||||
|
||||
generalForm.setFieldValue('imageName', filename);
|
||||
generalForm.setFieldValue(
|
||||
'imageSource',
|
||||
filesource
|
||||
);
|
||||
}
|
||||
}}
|
||||
error={
|
||||
generalForm.touched.imageName &&
|
||||
(!!generalForm.errors.imageName ||
|
||||
!!generalForm.errors.imageSource)
|
||||
}
|
||||
errorMessage={generalForm.errors.imageName}
|
||||
/>
|
||||
<Select
|
||||
name='category'
|
||||
label='Category'
|
||||
onChange={generalForm.handleChange}
|
||||
onBlur={generalForm.handleBlur}
|
||||
value={generalForm.values.category}
|
||||
select={generalForm.values.category}
|
||||
options={
|
||||
categories?.getAllCategories
|
||||
? categories.getAllCategories.map(
|
||||
({ id, name }) => ({
|
||||
value: id,
|
||||
label: name,
|
||||
})
|
||||
)
|
||||
: [{ value: '', label: 'Default' }]
|
||||
}
|
||||
error={
|
||||
generalForm.touched.category &&
|
||||
!!generalForm.errors.category
|
||||
}
|
||||
errorMessage={generalForm.errors.category}
|
||||
/>
|
||||
<TextArea
|
||||
name='description'
|
||||
label='Description'
|
||||
color={role || 'client'}
|
||||
value={generalForm.values.description}
|
||||
onChange={generalForm.handleChange}
|
||||
onBlur={generalForm.handleBlur}
|
||||
error={
|
||||
generalForm.touched.description &&
|
||||
!!generalForm.errors.description
|
||||
}
|
||||
errorMessage={generalForm.errors.description}
|
||||
/>
|
||||
<Box
|
||||
marginTop='0.5rem'
|
||||
display='flex'
|
||||
justifyContent='flex-end'
|
||||
>
|
||||
<Button
|
||||
variant='primary-action'
|
||||
color={role || 'client'}
|
||||
text='Next'
|
||||
type='submit'
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</form>
|
||||
</>
|
||||
) : (
|
||||
<Box display='grid' alignItems='center' justifyContent='center'>
|
||||
<Spinner color={role || 'client'} />
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{selectedSection === 'specification' && (
|
||||
<>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='auto 1fr'
|
||||
columnGap='1rem'
|
||||
alignItems='center'
|
||||
marginBottom='50px'
|
||||
>
|
||||
<Text variant='subheader' weight='bold'>
|
||||
Specification
|
||||
</Text>
|
||||
{error && <Alert color='error' text={error} />}
|
||||
</Box>
|
||||
<form onSubmit={specificationForm.handleSubmit}>
|
||||
<Box display='grid' gridTemplateColumns='auto' rowGap='0.5rem'>
|
||||
<Text variant='headline' gutterBottom>
|
||||
Introduction
|
||||
</Text>
|
||||
<TextArea
|
||||
name='purpose'
|
||||
label='Purpose'
|
||||
color={role || 'client'}
|
||||
value={specificationForm.values.purpose}
|
||||
onChange={specificationForm.handleChange}
|
||||
onBlur={specificationForm.handleBlur}
|
||||
error={
|
||||
specificationForm.touched.purpose &&
|
||||
!!specificationForm.errors.purpose
|
||||
}
|
||||
errorMessage={specificationForm.errors.purpose}
|
||||
/>
|
||||
<TextArea
|
||||
name='documentConventions'
|
||||
label='Document Conventions'
|
||||
color={role || 'client'}
|
||||
value={specificationForm.values.documentConventions}
|
||||
onChange={specificationForm.handleChange}
|
||||
onBlur={specificationForm.handleBlur}
|
||||
error={
|
||||
specificationForm.touched.documentConventions &&
|
||||
!!specificationForm.errors.documentConventions
|
||||
}
|
||||
errorMessage={specificationForm.errors.documentConventions}
|
||||
/>
|
||||
<TextArea
|
||||
name='intendedAudience'
|
||||
label='Intended Audience'
|
||||
color={role || 'client'}
|
||||
value={specificationForm.values.intendedAudience}
|
||||
onChange={specificationForm.handleChange}
|
||||
onBlur={specificationForm.handleBlur}
|
||||
error={
|
||||
specificationForm.touched.intendedAudience &&
|
||||
!!specificationForm.errors.intendedAudience
|
||||
}
|
||||
errorMessage={specificationForm.errors.intendedAudience}
|
||||
/>
|
||||
<TextArea
|
||||
name='projectScope'
|
||||
label='Project Scope'
|
||||
color={role || 'client'}
|
||||
value={specificationForm.values.projectScope}
|
||||
onChange={specificationForm.handleChange}
|
||||
onBlur={specificationForm.handleBlur}
|
||||
error={
|
||||
specificationForm.touched.projectScope &&
|
||||
!!specificationForm.errors.projectScope
|
||||
}
|
||||
errorMessage={specificationForm.errors.projectScope}
|
||||
/>
|
||||
<Text variant='headline' gutterBottom>
|
||||
Overall Description
|
||||
</Text>
|
||||
<TextArea
|
||||
name='perspective'
|
||||
label='Perspective'
|
||||
color={role || 'client'}
|
||||
value={specificationForm.values.perspective}
|
||||
onChange={specificationForm.handleChange}
|
||||
onBlur={specificationForm.handleBlur}
|
||||
error={
|
||||
specificationForm.touched.perspective &&
|
||||
!!specificationForm.errors.perspective
|
||||
}
|
||||
errorMessage={specificationForm.errors.perspective}
|
||||
/>
|
||||
<TextArea
|
||||
name='userCharacteristics'
|
||||
label='User Characteristics'
|
||||
color={role || 'client'}
|
||||
value={specificationForm.values.userCharacteristics}
|
||||
onChange={specificationForm.handleChange}
|
||||
onBlur={specificationForm.handleBlur}
|
||||
error={
|
||||
specificationForm.touched.userCharacteristics &&
|
||||
!!specificationForm.errors.userCharacteristics
|
||||
}
|
||||
errorMessage={specificationForm.errors.userCharacteristics}
|
||||
/>
|
||||
<TextArea
|
||||
name='operatingEnvironment'
|
||||
label='Operating Environment'
|
||||
color={role || 'client'}
|
||||
value={specificationForm.values.operatingEnvironment}
|
||||
onChange={specificationForm.handleChange}
|
||||
onBlur={specificationForm.handleBlur}
|
||||
error={
|
||||
specificationForm.touched.operatingEnvironment &&
|
||||
!!specificationForm.errors.operatingEnvironment
|
||||
}
|
||||
errorMessage={specificationForm.errors.operatingEnvironment}
|
||||
/>
|
||||
<TextArea
|
||||
name='designImplementationConstraints'
|
||||
label='Design and Implementation Constraints'
|
||||
color={role || 'client'}
|
||||
value={
|
||||
specificationForm.values.designImplementationConstraints
|
||||
}
|
||||
onChange={specificationForm.handleChange}
|
||||
onBlur={specificationForm.handleBlur}
|
||||
error={
|
||||
specificationForm.touched
|
||||
.designImplementationConstraints &&
|
||||
!!specificationForm.errors.designImplementationConstraints
|
||||
}
|
||||
errorMessage={
|
||||
specificationForm.errors.designImplementationConstraints
|
||||
}
|
||||
/>
|
||||
<TextArea
|
||||
name='userDocumentation'
|
||||
label='User Documentation'
|
||||
color={role || 'client'}
|
||||
value={specificationForm.values.userDocumentation}
|
||||
onChange={specificationForm.handleChange}
|
||||
onBlur={specificationForm.handleBlur}
|
||||
error={
|
||||
specificationForm.touched.userDocumentation &&
|
||||
!!specificationForm.errors.userDocumentation
|
||||
}
|
||||
errorMessage={specificationForm.errors.userDocumentation}
|
||||
/>
|
||||
<TextArea
|
||||
name='assemptionsDependencies'
|
||||
label='Assumptions and Dependencies'
|
||||
color={role || 'client'}
|
||||
value={specificationForm.values.assemptionsDependencies}
|
||||
onChange={specificationForm.handleChange}
|
||||
onBlur={specificationForm.handleBlur}
|
||||
error={
|
||||
specificationForm.touched.assemptionsDependencies &&
|
||||
!!specificationForm.errors.assemptionsDependencies
|
||||
}
|
||||
errorMessage={
|
||||
specificationForm.errors.assemptionsDependencies
|
||||
}
|
||||
/>
|
||||
<Text variant='headline' gutterBottom>
|
||||
Non-Functional Requirements
|
||||
</Text>
|
||||
<TextArea
|
||||
name='performanceRequirements'
|
||||
label='Performance Requirements'
|
||||
color={role || 'client'}
|
||||
value={specificationForm.values.performanceRequirements}
|
||||
onChange={specificationForm.handleChange}
|
||||
onBlur={specificationForm.handleBlur}
|
||||
error={
|
||||
specificationForm.touched.performanceRequirements &&
|
||||
!!specificationForm.errors.performanceRequirements
|
||||
}
|
||||
errorMessage={
|
||||
specificationForm.errors.performanceRequirements
|
||||
}
|
||||
/>
|
||||
<TextArea
|
||||
name='safetyRequirements'
|
||||
label='Safety Requirements'
|
||||
color={role || 'client'}
|
||||
value={specificationForm.values.safetyRequirements}
|
||||
onChange={specificationForm.handleChange}
|
||||
onBlur={specificationForm.handleBlur}
|
||||
error={
|
||||
specificationForm.touched.safetyRequirements &&
|
||||
!!specificationForm.errors.safetyRequirements
|
||||
}
|
||||
errorMessage={specificationForm.errors.safetyRequirements}
|
||||
/>
|
||||
<TextArea
|
||||
name='securityRequirements'
|
||||
label='Security Requirements'
|
||||
color={role || 'client'}
|
||||
value={specificationForm.values.securityRequirements}
|
||||
onChange={specificationForm.handleChange}
|
||||
onBlur={specificationForm.handleBlur}
|
||||
error={
|
||||
specificationForm.touched.securityRequirements &&
|
||||
!!specificationForm.errors.securityRequirements
|
||||
}
|
||||
errorMessage={specificationForm.errors.securityRequirements}
|
||||
/>
|
||||
<TextArea
|
||||
name='softwareQualityAttributes'
|
||||
label='Software Quality Attributes'
|
||||
color={role || 'client'}
|
||||
value={specificationForm.values.softwareQualityAttributes}
|
||||
onChange={specificationForm.handleChange}
|
||||
onBlur={specificationForm.handleBlur}
|
||||
error={
|
||||
specificationForm.touched.softwareQualityAttributes &&
|
||||
!!specificationForm.errors.softwareQualityAttributes
|
||||
}
|
||||
errorMessage={
|
||||
specificationForm.errors.softwareQualityAttributes
|
||||
}
|
||||
/>
|
||||
<TextArea
|
||||
name='otherRequirements'
|
||||
label='Other Requirements'
|
||||
color={role || 'client'}
|
||||
value={specificationForm.values.otherRequirements}
|
||||
onChange={specificationForm.handleChange}
|
||||
onBlur={specificationForm.handleBlur}
|
||||
error={
|
||||
specificationForm.touched.otherRequirements &&
|
||||
!!specificationForm.errors.otherRequirements
|
||||
}
|
||||
errorMessage={specificationForm.errors.otherRequirements}
|
||||
/>
|
||||
<TextArea
|
||||
name='glossary'
|
||||
label='Glossary'
|
||||
color={role || 'client'}
|
||||
value={specificationForm.values.glossary}
|
||||
onChange={specificationForm.handleChange}
|
||||
onBlur={specificationForm.handleBlur}
|
||||
error={
|
||||
specificationForm.touched.glossary &&
|
||||
!!specificationForm.errors.glossary
|
||||
}
|
||||
errorMessage={specificationForm.errors.glossary}
|
||||
/>
|
||||
<TextArea
|
||||
name='analysisModels'
|
||||
label='Analysis Models'
|
||||
color={role || 'client'}
|
||||
value={specificationForm.values.analysisModels}
|
||||
onChange={specificationForm.handleChange}
|
||||
onBlur={specificationForm.handleBlur}
|
||||
error={
|
||||
specificationForm.touched.analysisModels &&
|
||||
!!specificationForm.errors.analysisModels
|
||||
}
|
||||
errorMessage={specificationForm.errors.analysisModels}
|
||||
/>
|
||||
<TextArea
|
||||
name='issuesList'
|
||||
label='Issues List'
|
||||
color={role || 'client'}
|
||||
value={specificationForm.values.issuesList}
|
||||
onChange={specificationForm.handleChange}
|
||||
onBlur={specificationForm.handleBlur}
|
||||
error={
|
||||
specificationForm.touched.issuesList &&
|
||||
!!specificationForm.errors.issuesList
|
||||
}
|
||||
errorMessage={specificationForm.errors.issuesList}
|
||||
/>
|
||||
</Box>
|
||||
<Box marginTop='1rem' display='flex' justifyContent='flex-end'>
|
||||
<Button
|
||||
variant='primary-action'
|
||||
color={role || 'client'}
|
||||
text='Next'
|
||||
type='submit'
|
||||
/>
|
||||
</Box>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
{selectedSection === 'features' && (
|
||||
<>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='auto 1fr'
|
||||
columnGap='1rem'
|
||||
alignItems='center'
|
||||
marginBottom='50px'
|
||||
>
|
||||
<Text variant='subheader' weight='bold'>
|
||||
Features
|
||||
</Text>
|
||||
{error && <Alert color='error' text={error} />}
|
||||
</Box>
|
||||
<form onSubmit={featuresForm.handleSubmit}>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='repeat(2, 1fr)'
|
||||
columnGap='40px'
|
||||
rowGap='45px'
|
||||
alignItems='stretch'
|
||||
>
|
||||
{availableFeatures &&
|
||||
availableFeatures.map((feature) => (
|
||||
<FeatureCard
|
||||
feature={feature}
|
||||
selectable
|
||||
selected={featuresForm.values.features.includes(
|
||||
feature.id
|
||||
)}
|
||||
toggleSelect={() => {
|
||||
if (
|
||||
!featuresForm.values.features.includes(
|
||||
feature.id
|
||||
)
|
||||
) {
|
||||
featuresForm.setFieldValue('features', [
|
||||
...featuresForm.values.features,
|
||||
feature.id,
|
||||
]);
|
||||
} else {
|
||||
featuresForm.setFieldValue(
|
||||
'features',
|
||||
featuresForm.values.features.filter(
|
||||
(id) => id !== feature.id
|
||||
)
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
<Box
|
||||
marginTop='1rem'
|
||||
display='flex'
|
||||
justifyContent='flex-end'
|
||||
>
|
||||
<Button
|
||||
variant='primary-action'
|
||||
color={role || 'client'}
|
||||
text='Save'
|
||||
type='submit'
|
||||
loading={loading}
|
||||
/>
|
||||
</Box>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddTemplate;
|
||||
@@ -1,14 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
type WrapperProps = {
|
||||
color?: 'client' | 'productOwner' | 'developer' | 'admin';
|
||||
};
|
||||
|
||||
export const Wrapper = styled.div<WrapperProps>`
|
||||
padding: 35px 45px 35px 120px;
|
||||
|
||||
.empty {
|
||||
fill: ${({ theme, color }) =>
|
||||
color ? theme.colors[color].main : theme.colors.client.main};
|
||||
}
|
||||
`;
|
||||
@@ -1,279 +0,0 @@
|
||||
import * as Yup from 'yup';
|
||||
import { useFormik } from 'formik';
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useMutation, useQuery, useReactiveVar } from '@apollo/client';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Input,
|
||||
Select,
|
||||
Text,
|
||||
Alert,
|
||||
Spinner,
|
||||
} from '../../../components';
|
||||
import { theme } from '../../../themes';
|
||||
import { Wrapper } from './styles';
|
||||
import {
|
||||
GetCountryCodesQuery,
|
||||
GetCountryCodesQueryVariables,
|
||||
UpdateUserInfoMutation,
|
||||
UpdateUserInfoMutationVariables,
|
||||
} from '../../../graphql/types';
|
||||
import { GET_COUNTRY_CODES, UPDATE_USER_INFO } from '../../../graphql/auth.api';
|
||||
import { userVar } from '../../../graphql/state';
|
||||
|
||||
const AdditionalInfo = () => {
|
||||
const navigate = useNavigate();
|
||||
const [error, setError] = useState<string>('');
|
||||
const currentUser = useReactiveVar(userVar);
|
||||
const { data: countryCodes, loading: countryCodesLoading } = useQuery<
|
||||
GetCountryCodesQuery,
|
||||
GetCountryCodesQueryVariables
|
||||
>(GET_COUNTRY_CODES);
|
||||
|
||||
const [updateUserInfo, { loading }] = useMutation<
|
||||
UpdateUserInfoMutation,
|
||||
UpdateUserInfoMutationVariables
|
||||
>(UPDATE_USER_INFO, {
|
||||
onCompleted({ updateUserInfo: user }) {
|
||||
userVar(user);
|
||||
navigate('/');
|
||||
},
|
||||
onError({ graphQLErrors }) {
|
||||
setError(graphQLErrors[0]?.extensions?.info as string);
|
||||
setTimeout(() => setError(''), 3000);
|
||||
},
|
||||
});
|
||||
|
||||
const form = useFormik({
|
||||
initialValues: {
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
prefix: '',
|
||||
number: '',
|
||||
place: '',
|
||||
city: '',
|
||||
zip: '',
|
||||
country: '',
|
||||
},
|
||||
validationSchema: Yup.object().shape({
|
||||
firstName: Yup.string().required('First Name is required'),
|
||||
lastName: Yup.string().required('Last Name is required'),
|
||||
prefix: Yup.string().required('Prefix is required'),
|
||||
number: Yup.number()
|
||||
// prettier-ignore
|
||||
.typeError('Phone must be a number')
|
||||
.required('Phone is required'),
|
||||
place: Yup.string().required('Address is required'),
|
||||
city: Yup.string().required('City is required'),
|
||||
country: Yup.string().required('Country is required'),
|
||||
zip: Yup.number()
|
||||
// prettier-ignore
|
||||
.typeError('Zip must be a number')
|
||||
.required('Zip is required'),
|
||||
}),
|
||||
onSubmit: ({
|
||||
firstName,
|
||||
lastName,
|
||||
prefix,
|
||||
number,
|
||||
place,
|
||||
city,
|
||||
country,
|
||||
zip,
|
||||
}) =>
|
||||
updateUserInfo({
|
||||
variables: {
|
||||
user: {
|
||||
id: currentUser?.id!,
|
||||
email: currentUser?.email!,
|
||||
firstName,
|
||||
lastName,
|
||||
phone: { prefix, number },
|
||||
address: { place, city, country, zip },
|
||||
role: currentUser?.role!,
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Box
|
||||
display='grid'
|
||||
alignItems='center'
|
||||
justifyContent='center'
|
||||
padding='100px 300px'
|
||||
>
|
||||
<Box
|
||||
background={theme.colors.white.main}
|
||||
padding='80px 175px'
|
||||
borderRadius='5px'
|
||||
>
|
||||
<Box marginBottom='35px' textAlign='center'>
|
||||
<Text variant='headline' weight='bold'>
|
||||
Tell us more about yourself
|
||||
</Text>
|
||||
</Box>
|
||||
{!countryCodesLoading ? (
|
||||
<form onSubmit={form.handleSubmit}>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='auto'
|
||||
rowGap='0.5rem'
|
||||
position='relative'
|
||||
>
|
||||
<Input
|
||||
name='firstName'
|
||||
label='First Name'
|
||||
value={form.values.firstName}
|
||||
onChange={form.handleChange}
|
||||
onBlur={form.handleBlur}
|
||||
error={form.touched.firstName && !!form.errors.firstName}
|
||||
errorMessage={form.errors.firstName}
|
||||
/>
|
||||
<Input
|
||||
name='lastName'
|
||||
label='Last Name'
|
||||
value={form.values.lastName}
|
||||
onChange={form.handleChange}
|
||||
onBlur={form.handleBlur}
|
||||
error={form.touched.lastName && !!form.errors.lastName}
|
||||
errorMessage={form.errors.lastName}
|
||||
/>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='1fr 1.5fr'
|
||||
columnGap='10px'
|
||||
>
|
||||
<Select
|
||||
name='prefix'
|
||||
label='Country Code'
|
||||
options={
|
||||
countryCodes?.getCountryCode
|
||||
? [
|
||||
{
|
||||
value: '',
|
||||
label: 'Choose',
|
||||
},
|
||||
...countryCodes.getCountryCode.map(
|
||||
({ prefix, country }) => ({
|
||||
value: prefix,
|
||||
label: `+${prefix} (${country})`,
|
||||
})
|
||||
),
|
||||
]
|
||||
: [
|
||||
{
|
||||
value: '',
|
||||
label: 'Choose',
|
||||
},
|
||||
{ value: '216', label: '+216' },
|
||||
]
|
||||
}
|
||||
onChange={form.handleChange}
|
||||
onBlur={form.handleBlur}
|
||||
value={form.values.prefix}
|
||||
error={form.touched.prefix && !!form.errors.prefix}
|
||||
errorMessage={form.errors.prefix}
|
||||
/>
|
||||
<Input
|
||||
name='number'
|
||||
type='tel'
|
||||
label='Phone'
|
||||
onChange={form.handleChange}
|
||||
onBlur={form.handleBlur}
|
||||
value={form.values.number}
|
||||
error={form.touched.number && !!form.errors.number}
|
||||
errorMessage={form.errors.number}
|
||||
/>
|
||||
</Box>
|
||||
<Input
|
||||
name='place'
|
||||
label='Address'
|
||||
onChange={form.handleChange}
|
||||
onBlur={form.handleBlur}
|
||||
value={form.values.place}
|
||||
error={form.touched.place && !!form.errors.place}
|
||||
errorMessage={form.errors.place}
|
||||
/>
|
||||
<Input
|
||||
name='city'
|
||||
label='City'
|
||||
onChange={form.handleChange}
|
||||
onBlur={form.handleBlur}
|
||||
value={form.values.city}
|
||||
error={form.touched.city && !!form.errors.city}
|
||||
errorMessage={form.errors.city}
|
||||
/>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='2fr 1fr'
|
||||
columnGap='10px'
|
||||
>
|
||||
<Select
|
||||
name='country'
|
||||
label='Country'
|
||||
options={
|
||||
countryCodes?.getCountryCode
|
||||
? [
|
||||
{
|
||||
value: '',
|
||||
label: 'Choose',
|
||||
},
|
||||
...countryCodes.getCountryCode.map(
|
||||
({ country }) => ({
|
||||
value: country,
|
||||
label: country,
|
||||
})
|
||||
),
|
||||
]
|
||||
: [
|
||||
{
|
||||
value: '',
|
||||
label: 'Choose',
|
||||
},
|
||||
{ value: 'Tunisia', label: 'Tunisia' },
|
||||
]
|
||||
}
|
||||
onChange={form.handleChange}
|
||||
onBlur={form.handleBlur}
|
||||
value={form.values.country}
|
||||
error={form.touched.country && !!form.errors.country}
|
||||
errorMessage={form.errors.country}
|
||||
/>
|
||||
<Input
|
||||
name='zip'
|
||||
label='Zip Code'
|
||||
onChange={form.handleChange}
|
||||
onBlur={form.handleBlur}
|
||||
value={form.values.zip}
|
||||
error={form.touched.zip && !!form.errors.zip}
|
||||
errorMessage={form.errors.zip}
|
||||
/>
|
||||
</Box>
|
||||
<Box marginTop='0.5rem'>
|
||||
<Button
|
||||
fullWidth
|
||||
variant='primary-action'
|
||||
color='client'
|
||||
text='Done'
|
||||
type='submit'
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
/>
|
||||
</Box>
|
||||
{error && <Alert color='error' text={error} />}
|
||||
</Box>
|
||||
</form>
|
||||
) : (
|
||||
<Spinner fullScreen />
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdditionalInfo;
|
||||
@@ -1,5 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
background: ${({ theme }) => theme.colors.client.main};
|
||||
`;
|
||||
@@ -1,161 +0,0 @@
|
||||
import * as Yup from 'yup';
|
||||
import { useMutation } from '@apollo/client';
|
||||
import { useFormik } from 'formik';
|
||||
import { useState } from 'react';
|
||||
import { Login as LoginIllustration, Logo } from '../../../assets';
|
||||
import { Box, Button, Input, Link, Text, Alert } from '../../../components';
|
||||
import { RESET_PASSWORD } from '../../../graphql/auth.api';
|
||||
import {
|
||||
ResetPasswordMutation,
|
||||
ResetPasswordMutationVariables,
|
||||
} from '../../../graphql/types';
|
||||
import { theme } from '../../../themes';
|
||||
import { Wrapper } from './styles';
|
||||
|
||||
const ForgotPassword = () => {
|
||||
const [error, setError] = useState<string>('');
|
||||
const [success, setSuccess] = useState<boolean>(false);
|
||||
|
||||
const [resetPassword, { loading }] = useMutation<
|
||||
ResetPasswordMutation,
|
||||
ResetPasswordMutationVariables
|
||||
>(RESET_PASSWORD, {
|
||||
onCompleted() {
|
||||
setSuccess(true);
|
||||
setTimeout(() => setSuccess(false), 3000);
|
||||
},
|
||||
onError({ graphQLErrors }) {
|
||||
setError(graphQLErrors[0]?.extensions?.info as string);
|
||||
setTimeout(() => setError(''), 3000);
|
||||
},
|
||||
});
|
||||
|
||||
const form = useFormik({
|
||||
initialValues: {
|
||||
email: '',
|
||||
password: '',
|
||||
},
|
||||
validationSchema: Yup.object().shape({
|
||||
email: Yup.string()
|
||||
.required('Email is required')
|
||||
.email('Email is invalid'),
|
||||
}),
|
||||
onSubmit: ({ email }) => {
|
||||
resetPassword({ variables: { email } });
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='1fr 1.25fr'
|
||||
height='100vh'
|
||||
overflow='hidden'
|
||||
>
|
||||
<Box padding='3.438rem 4.375rem'>
|
||||
<Box
|
||||
display='flex'
|
||||
flexDirection='row'
|
||||
alignItems='center'
|
||||
marginBottom='3.125rem'
|
||||
>
|
||||
<Box marginRight='0.625rem'>
|
||||
<Logo />
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='auto 1fr'
|
||||
columnGap='1rem'
|
||||
alignItems='center'
|
||||
>
|
||||
<Text variant='headline' weight='bold'>
|
||||
Forgot Password
|
||||
</Text>
|
||||
{error && <Alert color='error' text={error} />}
|
||||
{success && (
|
||||
<Alert
|
||||
color='success'
|
||||
text='Check your email to recover your account'
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<form onSubmit={form.handleSubmit}>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='auto'
|
||||
alignItems='center'
|
||||
rowGap='0.5rem'
|
||||
padding='1.563rem 0rem'
|
||||
>
|
||||
<Input
|
||||
label='Email'
|
||||
name='email'
|
||||
value={form.values.email}
|
||||
onChange={form.handleChange}
|
||||
onBlur={form.handleBlur}
|
||||
error={!!form.errors.email}
|
||||
errorMessage={form.errors.email}
|
||||
/>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='auto'
|
||||
rowGap='1rem'
|
||||
marginTop='0.5rem'
|
||||
>
|
||||
<Button
|
||||
variant='primary-action'
|
||||
fullWidth
|
||||
color='client'
|
||||
type='submit'
|
||||
text='Send Reset Link'
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
/>
|
||||
</Box>
|
||||
<Box display='flex' flexDirection='row'>
|
||||
<Box flexGrow='1'>
|
||||
<Text variant='body' display='inline'>
|
||||
Don’t have an account ?{' '}
|
||||
</Text>
|
||||
<Link href='/signup'>Signup</Link>
|
||||
</Box>
|
||||
<Link href='/' color='gray'>
|
||||
Build a Project
|
||||
</Link>
|
||||
</Box>
|
||||
</Box>
|
||||
</form>
|
||||
</Box>
|
||||
<Box
|
||||
background={theme.colors.client.main}
|
||||
display='flex'
|
||||
flexDirection='column'
|
||||
alignItems='center'
|
||||
justifyContent='center'
|
||||
>
|
||||
<Box>
|
||||
<LoginIllustration />
|
||||
</Box>
|
||||
<Box marginTop='1.563rem'>
|
||||
<Text color='white' variant='headline' align='center'>
|
||||
Make your idea alive
|
||||
</Text>
|
||||
</Box>
|
||||
<Box marginTop='0.938rem'>
|
||||
<Text
|
||||
color='rgba(255, 255, 255, 0.6)'
|
||||
variant='subheader'
|
||||
align='center'
|
||||
>
|
||||
Create your dream software with no coding skills
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ForgotPassword;
|
||||
@@ -1,3 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Wrapper = styled.div``;
|
||||
@@ -1,180 +0,0 @@
|
||||
import * as Yup from 'yup';
|
||||
import { useFormik } from 'formik';
|
||||
import { useMutation } from '@apollo/client';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { useState } from 'react';
|
||||
import { tokenVar, roleVar, userVar } from '../../../graphql/state';
|
||||
import { Login as LoginIllustration, Logo } from '../../../assets';
|
||||
import { Alert, Box, Button, Input, Link, Text } from '../../../components';
|
||||
import { theme } from '../../../themes';
|
||||
import { Wrapper } from './styles';
|
||||
import { LOGIN } from '../../../graphql/auth.api';
|
||||
import { LoginMutation, LoginMutationVariables } from '../../../graphql/types';
|
||||
|
||||
const Login = () => {
|
||||
const navigate = useNavigate();
|
||||
const [error, setError] = useState<string>('');
|
||||
|
||||
const [login, { loading }] = useMutation<
|
||||
LoginMutation,
|
||||
LoginMutationVariables
|
||||
>(LOGIN, {
|
||||
onCompleted({ login: { user, token } }) {
|
||||
switch (user.role) {
|
||||
case 'Client':
|
||||
roleVar('client');
|
||||
break;
|
||||
case 'ProductOwner':
|
||||
roleVar('productOwner');
|
||||
break;
|
||||
case 'Developer':
|
||||
roleVar('developer');
|
||||
break;
|
||||
case 'Admin':
|
||||
roleVar('admin');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
tokenVar(token);
|
||||
userVar(user);
|
||||
localStorage.setItem('token', token);
|
||||
navigate('/');
|
||||
},
|
||||
onError({ graphQLErrors }) {
|
||||
setError(graphQLErrors[0]?.extensions?.info as string);
|
||||
setTimeout(() => setError(''), 3000);
|
||||
},
|
||||
});
|
||||
|
||||
const form = useFormik({
|
||||
initialValues: {
|
||||
email: '',
|
||||
password: '',
|
||||
},
|
||||
validationSchema: Yup.object().shape({
|
||||
email: Yup.string()
|
||||
.required('Email is required')
|
||||
.email('Email is invalid'),
|
||||
password: Yup.string()
|
||||
.required('Password is required')
|
||||
.min(6, 'Password is 6 characters minimum'),
|
||||
}),
|
||||
onSubmit: ({ email, password }) => {
|
||||
login({ variables: { email, password } });
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='1fr 1.25fr'
|
||||
height='100vh'
|
||||
overflow='hidden'
|
||||
>
|
||||
<Box padding='3.438rem 4.375rem'>
|
||||
<Box
|
||||
display='flex'
|
||||
flexDirection='row'
|
||||
alignItems='center'
|
||||
marginBottom='3.125rem'
|
||||
>
|
||||
<Box marginRight='0.625rem'>
|
||||
<Logo />
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='auto 1fr'
|
||||
columnGap='1rem'
|
||||
alignItems='center'
|
||||
>
|
||||
<Text variant='headline' weight='bold'>
|
||||
Login
|
||||
</Text>
|
||||
{error && <Alert color='error' text={error} />}
|
||||
</Box>
|
||||
<form onSubmit={form.handleSubmit}>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='auto'
|
||||
alignItems='center'
|
||||
rowGap='0.5rem'
|
||||
padding='1.563rem 0rem'
|
||||
>
|
||||
<Input
|
||||
label='Email'
|
||||
name='email'
|
||||
value={form.values.email}
|
||||
onChange={form.handleChange}
|
||||
onBlur={form.handleBlur}
|
||||
error={form.touched.email && !!form.errors.email}
|
||||
errorMessage={form.errors.email}
|
||||
/>
|
||||
<Input
|
||||
label='Password'
|
||||
name='password'
|
||||
type='password'
|
||||
value={form.values.password}
|
||||
onChange={form.handleChange}
|
||||
onBlur={form.handleBlur}
|
||||
error={form.touched.password && !!form.errors.password}
|
||||
errorMessage={form.errors.password}
|
||||
/>
|
||||
<Box textAlign='right'>
|
||||
<Link href='/forgot-password'>Forgot Password</Link>
|
||||
</Box>
|
||||
<Box display='grid' gridTemplateColumns='auto' rowGap='1rem'>
|
||||
<Button
|
||||
variant='primary-action'
|
||||
fullWidth
|
||||
color='client'
|
||||
text='Login'
|
||||
type='submit'
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
/>
|
||||
</Box>
|
||||
<Box display='flex' flexDirection='row'>
|
||||
<Box flexGrow='1'>
|
||||
<Text variant='body' display='inline'>
|
||||
Don’t have an account ?{' '}
|
||||
</Text>
|
||||
<Link href='/signup'>Signup</Link>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</form>
|
||||
</Box>
|
||||
<Box
|
||||
background={theme.colors.client.main}
|
||||
display='flex'
|
||||
flexDirection='column'
|
||||
alignItems='center'
|
||||
justifyContent='center'
|
||||
>
|
||||
<Box>
|
||||
<LoginIllustration />
|
||||
</Box>
|
||||
<Box marginTop='1.563rem'>
|
||||
<Text color='white' variant='headline' align='center'>
|
||||
Make your idea alive
|
||||
</Text>
|
||||
</Box>
|
||||
<Box marginTop='0.938rem'>
|
||||
<Text
|
||||
color='rgba(255, 255, 255, 0.6)'
|
||||
variant='subheader'
|
||||
align='center'
|
||||
>
|
||||
Create your dream software with no coding skills
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
@@ -1,7 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
button svg path {
|
||||
stroke: transparent !important;
|
||||
}
|
||||
`;
|
||||
@@ -1,162 +0,0 @@
|
||||
import * as Yup from 'yup';
|
||||
import { useMutation } from '@apollo/client';
|
||||
import { useFormik } from 'formik';
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Login as LoginIllustration, Logo } from '../../../assets';
|
||||
import { Box, Button, Input, Text, Alert } from '../../../components';
|
||||
import { CONFIRM_USER_RESET_PASSWORD } from '../../../graphql/auth.api';
|
||||
import {
|
||||
ConfirmUserResetPasswordMutation,
|
||||
ConfirmUserResetPasswordMutationVariables,
|
||||
} from '../../../graphql/types';
|
||||
import { theme } from '../../../themes';
|
||||
import { Wrapper } from './styles';
|
||||
|
||||
const RecoverAccount = () => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const navigate = useNavigate();
|
||||
const [error, setError] = useState<string>('');
|
||||
|
||||
const [confirmResetPassword, { loading }] = useMutation<
|
||||
ConfirmUserResetPasswordMutation,
|
||||
ConfirmUserResetPasswordMutationVariables
|
||||
>(CONFIRM_USER_RESET_PASSWORD, {
|
||||
onCompleted() {
|
||||
navigate('/login');
|
||||
},
|
||||
onError({ graphQLErrors }) {
|
||||
setError(graphQLErrors[0]?.extensions?.info as string);
|
||||
setTimeout(() => setError(''), 3000);
|
||||
},
|
||||
});
|
||||
|
||||
const form = useFormik({
|
||||
initialValues: {
|
||||
newPassword: '',
|
||||
confirmNewPassword: '',
|
||||
},
|
||||
validationSchema: Yup.object().shape({
|
||||
newPassword: Yup.string()
|
||||
.required('New password is required')
|
||||
.min(6, 'new Password is 6 characters minimum'),
|
||||
confirmNewPassword: Yup.string().oneOf(
|
||||
[Yup.ref('newPassword')],
|
||||
"Passwords don't match"
|
||||
),
|
||||
}),
|
||||
onSubmit: ({ newPassword }) => {
|
||||
confirmResetPassword({
|
||||
variables: { id: params.get('code')!, password: newPassword },
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='1fr 1.25fr'
|
||||
height='100vh'
|
||||
overflow='hidden'
|
||||
>
|
||||
<Box padding='3.438rem 4.375rem'>
|
||||
<Box
|
||||
display='flex'
|
||||
flexDirection='row'
|
||||
alignItems='center'
|
||||
marginBottom='3.125rem'
|
||||
>
|
||||
<Box marginRight='0.625rem'>
|
||||
<Logo />
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='auto 1fr'
|
||||
columnGap='1rem'
|
||||
alignItems='center'
|
||||
>
|
||||
<Text variant='headline' weight='bold'>
|
||||
Recover Account
|
||||
</Text>
|
||||
{error && <Alert color='error' text={error} />}
|
||||
</Box>
|
||||
<form onSubmit={form.handleSubmit}>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='auto'
|
||||
alignItems='center'
|
||||
rowGap='0.5rem'
|
||||
padding='1.563rem 0rem'
|
||||
>
|
||||
<Input
|
||||
label='New Password'
|
||||
name='newPassword'
|
||||
type='password'
|
||||
value={form.values.newPassword}
|
||||
onChange={form.handleChange}
|
||||
onBlur={form.handleBlur}
|
||||
error={!!form.errors.newPassword}
|
||||
errorMessage={form.errors.newPassword}
|
||||
/>
|
||||
<Input
|
||||
label='Confirm New Password'
|
||||
name='confirmNewPassword'
|
||||
type='password'
|
||||
value={form.values.confirmNewPassword}
|
||||
onChange={form.handleChange}
|
||||
onBlur={form.handleBlur}
|
||||
error={!!form.errors.confirmNewPassword}
|
||||
errorMessage={form.errors.confirmNewPassword}
|
||||
/>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='auto'
|
||||
rowGap='1rem'
|
||||
marginTop='0.5rem'
|
||||
>
|
||||
<Button
|
||||
variant='primary-action'
|
||||
fullWidth
|
||||
color='client'
|
||||
text='Recover Account'
|
||||
type='submit'
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</form>
|
||||
</Box>
|
||||
<Box
|
||||
background={theme.colors.client.main}
|
||||
display='flex'
|
||||
flexDirection='column'
|
||||
alignItems='center'
|
||||
justifyContent='center'
|
||||
>
|
||||
<Box>
|
||||
<LoginIllustration />
|
||||
</Box>
|
||||
<Box marginTop='1.563rem'>
|
||||
<Text color='white' variant='headline' align='center'>
|
||||
Make your idea alive
|
||||
</Text>
|
||||
</Box>
|
||||
<Box marginTop='0.938rem'>
|
||||
<Text
|
||||
color='rgba(255, 255, 255, 0.6)'
|
||||
variant='subheader'
|
||||
align='center'
|
||||
>
|
||||
Create your dream software with no coding skills
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default RecoverAccount;
|
||||
@@ -1,3 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Wrapper = styled.div``;
|
||||
@@ -1,170 +0,0 @@
|
||||
import * as Yup from 'yup';
|
||||
import { useFormik } from 'formik';
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useMutation } from '@apollo/client';
|
||||
import { Signup as SignupIllustration, Logo } from '../../../assets';
|
||||
import { Box, Button, Input, Link, Text, Alert } from '../../../components';
|
||||
import { theme } from '../../../themes';
|
||||
import { Wrapper } from './styles';
|
||||
import {
|
||||
SignupMutation,
|
||||
SignupMutationVariables,
|
||||
} from '../../../graphql/types';
|
||||
import { SIGNUP } from '../../../graphql/auth.api';
|
||||
import { roleVar, tokenVar, userVar } from '../../../graphql/state';
|
||||
|
||||
const Signup = () => {
|
||||
const navigate = useNavigate();
|
||||
const [error, setError] = useState<string>('');
|
||||
|
||||
const [signup, { loading }] = useMutation<
|
||||
SignupMutation,
|
||||
SignupMutationVariables
|
||||
>(SIGNUP, {
|
||||
onCompleted({ signup: { token, user } }) {
|
||||
roleVar('client');
|
||||
tokenVar(token);
|
||||
userVar(user);
|
||||
localStorage.setItem('token', token);
|
||||
navigate('/additional-info');
|
||||
},
|
||||
onError({ graphQLErrors }) {
|
||||
setError(graphQLErrors[0]?.extensions?.info as string);
|
||||
setTimeout(() => setError(''), 3000);
|
||||
},
|
||||
});
|
||||
|
||||
const form = useFormik({
|
||||
initialValues: {
|
||||
email: '',
|
||||
password: '',
|
||||
},
|
||||
validationSchema: Yup.object().shape({
|
||||
email: Yup.string()
|
||||
.required('Email is required')
|
||||
.email('Email is invalid'),
|
||||
password: Yup.string()
|
||||
.required('Password is required')
|
||||
.min(6, 'Password is 6 characters minimum'),
|
||||
}),
|
||||
onSubmit: ({ email, password }) => {
|
||||
signup({ variables: { email, password } });
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='1fr 1.25fr'
|
||||
height='100vh'
|
||||
overflow='hidden'
|
||||
>
|
||||
<Box padding='3.438rem 4.375rem'>
|
||||
<Box
|
||||
display='flex'
|
||||
flexDirection='row'
|
||||
alignItems='center'
|
||||
marginBottom='3.125rem'
|
||||
>
|
||||
<Box marginRight='0.625rem'>
|
||||
<Logo />
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='auto 1fr'
|
||||
columnGap='1rem'
|
||||
alignItems='center'
|
||||
>
|
||||
<Text variant='headline' weight='bold'>
|
||||
Signup
|
||||
</Text>
|
||||
{error && <Alert color='error' text={error} />}
|
||||
</Box>
|
||||
<form onSubmit={form.handleSubmit}>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='auto'
|
||||
alignItems='center'
|
||||
rowGap='0.5rem'
|
||||
padding='1.563rem 0rem'
|
||||
>
|
||||
<Input
|
||||
label='Email'
|
||||
name='email'
|
||||
value={form.values.email}
|
||||
onChange={form.handleChange}
|
||||
onBlur={form.handleBlur}
|
||||
error={form.touched.email && !!form.errors.email}
|
||||
errorMessage={form.errors.email}
|
||||
/>
|
||||
<Input
|
||||
label='Password'
|
||||
name='password'
|
||||
type='password'
|
||||
value={form.values.password}
|
||||
onChange={form.handleChange}
|
||||
onBlur={form.handleBlur}
|
||||
error={form.touched.password && !!form.errors.password}
|
||||
errorMessage={form.errors.password}
|
||||
/>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='auto'
|
||||
marginTop='0.5rem'
|
||||
rowGap='1rem'
|
||||
>
|
||||
<Button
|
||||
variant='primary-action'
|
||||
fullWidth
|
||||
type='submit'
|
||||
color='client'
|
||||
text='Signup'
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
/>
|
||||
</Box>
|
||||
<Box display='flex' flexDirection='row'>
|
||||
<Box flexGrow='1'>
|
||||
<Text variant='body' display='inline'>
|
||||
Already have an account ?{' '}
|
||||
</Text>
|
||||
<Link href='/login'>Login</Link>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</form>
|
||||
</Box>
|
||||
<Box
|
||||
background={theme.colors.client.main}
|
||||
display='flex'
|
||||
flexDirection='column'
|
||||
alignItems='center'
|
||||
justifyContent='center'
|
||||
>
|
||||
<Box>
|
||||
<SignupIllustration />
|
||||
</Box>
|
||||
<Box>
|
||||
<Text color='white' variant='headline' align='center'>
|
||||
Make your idea alive
|
||||
</Text>
|
||||
</Box>
|
||||
<Box marginTop='0.938rem'>
|
||||
<Text
|
||||
color='rgba(255, 255, 255, 0.6)'
|
||||
variant='subheader'
|
||||
align='center'
|
||||
>
|
||||
Create your dream software with no coding skills
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Signup;
|
||||
@@ -1,7 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
button svg path {
|
||||
stroke: transparent !important;
|
||||
}
|
||||
`;
|
||||
@@ -1,112 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useLazyQuery, useReactiveVar } from '@apollo/client';
|
||||
import { Navigate, useNavigate, useParams } from 'react-router';
|
||||
import { roleVar } from '../../graphql/state';
|
||||
import { Empty, Settings } from '../../assets';
|
||||
import { Box, Button, Spinner, Text } from '../../components';
|
||||
import { Wrapper } from './styles';
|
||||
import {
|
||||
CategoryOutput,
|
||||
GetAllCategoriesQuery,
|
||||
GetAllCategoriesQueryVariables,
|
||||
GetCategoryByIdQuery,
|
||||
GetCategoryByIdQueryVariables,
|
||||
} from '../../graphql/types';
|
||||
import {
|
||||
GET_ALL_CATEGORIES,
|
||||
GET_CATEGORY_BY_ID,
|
||||
} from '../../graphql/category.api';
|
||||
|
||||
const Category = () => {
|
||||
const role = useReactiveVar(roleVar);
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const [category, setCategory] = useState<CategoryOutput>();
|
||||
|
||||
const [getCategories, { loading: categoriesLoading, error: categoriesError }] = useLazyQuery<
|
||||
GetAllCategoriesQuery,
|
||||
GetAllCategoriesQueryVariables
|
||||
>(GET_ALL_CATEGORIES, {
|
||||
onCompleted({ getAllCategories }) {
|
||||
setCategory(getAllCategories[0]);
|
||||
}
|
||||
});
|
||||
|
||||
const [getCategory, { loading: categoryLoading, error: categoryError }] = useLazyQuery<
|
||||
GetCategoryByIdQuery,
|
||||
GetCategoryByIdQueryVariables
|
||||
>(GET_CATEGORY_BY_ID, {
|
||||
onCompleted({ getCategoryById }) {
|
||||
setCategory(getCategoryById);
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
getCategory({ variables: { id } });
|
||||
} else {
|
||||
getCategories();
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
if (role !== 'developer') return (
|
||||
<>
|
||||
{role === 'admin' && <Navigate to='/clients' />}
|
||||
{['client', 'productOwer'].includes(role as string) && <Navigate to='/project' />}
|
||||
</>
|
||||
)
|
||||
|
||||
if (categoriesLoading || categoryLoading) return (
|
||||
<Spinner fullScreen color={role || 'client'} />
|
||||
);
|
||||
|
||||
if (categoriesError || categoryError || !category) return (
|
||||
<Wrapper color={role}>
|
||||
<Box
|
||||
width='100%'
|
||||
height='100vh'
|
||||
display='grid'
|
||||
alignItems='center'
|
||||
justifyContent='center'
|
||||
>
|
||||
<Box>
|
||||
<Empty />
|
||||
</Box>
|
||||
</Box>
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Box padding='35px 45px 0px 120px'>
|
||||
<Box
|
||||
display='flex'
|
||||
flexDirection='row'
|
||||
alignItems='center'
|
||||
marginBottom='20px'
|
||||
>
|
||||
<Box flexGrow='1'>
|
||||
<Text variant='headline' weight='bold'>
|
||||
{category.name}
|
||||
</Text>
|
||||
</Box>
|
||||
<Button
|
||||
color={role || 'client'}
|
||||
variant='primary-action'
|
||||
text='Settings'
|
||||
iconLeft={<Settings />}
|
||||
onClick={() =>
|
||||
navigate(`/category-settings/${id || category.id}`)
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text variant='headline'>Description</Text>
|
||||
<Text>{category.description}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Category;
|
||||
@@ -1,12 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
type WrapperProps = {
|
||||
color?: 'client' | 'productOwner' | 'developer' | 'admin';
|
||||
};
|
||||
|
||||
export const Wrapper = styled.div<WrapperProps>`
|
||||
.empty {
|
||||
fill: ${({ theme, color }) =>
|
||||
color ? theme.colors[color].main : theme.colors.client.main};
|
||||
}
|
||||
`;
|
||||
@@ -1,294 +0,0 @@
|
||||
import * as Yup from 'yup';
|
||||
import { useFormik } from 'formik';
|
||||
import { Navigate, useNavigate, useParams } from 'react-router';
|
||||
import { useLazyQuery, useMutation, useReactiveVar } from '@apollo/client';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { roleVar } from '../../graphql/state';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Text,
|
||||
SectionSelector,
|
||||
Input,
|
||||
Alert,
|
||||
TextArea,
|
||||
Spinner,
|
||||
Modal,
|
||||
} from '../../components';
|
||||
import { Wrapper } from './styles';
|
||||
import { ArrowLeft, Empty, General } from '../../assets';
|
||||
import {
|
||||
CategoryOutput,
|
||||
DeleteCategoryMutation,
|
||||
DeleteCategoryMutationVariables,
|
||||
GetCategoryByIdQuery,
|
||||
GetCategoryByIdQueryVariables,
|
||||
UpdateCategoryMutation,
|
||||
UpdateCategoryMutationVariables,
|
||||
} from '../../graphql/types';
|
||||
import {
|
||||
DELETE_CATEGORY,
|
||||
GET_CATEGORY_BY_ID,
|
||||
UPDATE_CATEGORY,
|
||||
} from '../../graphql/category.api';
|
||||
|
||||
const CategorySettings = () => {
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const role = useReactiveVar(roleVar);
|
||||
|
||||
const [error, setError] = useState<string>('');
|
||||
const [success, setSuccess] = useState<boolean>(false);
|
||||
const [category, setCategory] = useState<CategoryOutput>();
|
||||
|
||||
const [deleteCategoryModal, setDeleteCategoryModal] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const [getCategory, { loading: categoryLoading, error: categoryError }] = useLazyQuery<
|
||||
GetCategoryByIdQuery,
|
||||
GetCategoryByIdQueryVariables
|
||||
>(GET_CATEGORY_BY_ID, {
|
||||
onCompleted({ getCategoryById }) {
|
||||
setCategory(getCategoryById);
|
||||
}
|
||||
});
|
||||
|
||||
const [updateCategory, { loading }] = useMutation<
|
||||
UpdateCategoryMutation,
|
||||
UpdateCategoryMutationVariables
|
||||
>(UPDATE_CATEGORY, {
|
||||
onCompleted({ updateCategory: data }) {
|
||||
setCategory(data);
|
||||
setSuccess(true);
|
||||
setTimeout(() => setSuccess(false), 3000);
|
||||
},
|
||||
onError({ graphQLErrors }) {
|
||||
setError(graphQLErrors[0]?.extensions?.info as string);
|
||||
setTimeout(() => setError(''), 3000);
|
||||
},
|
||||
});
|
||||
|
||||
const [deleteCategory] = useMutation<
|
||||
DeleteCategoryMutation,
|
||||
DeleteCategoryMutationVariables
|
||||
>(DELETE_CATEGORY, {
|
||||
onCompleted() {
|
||||
navigate('/category');
|
||||
},
|
||||
onError({ graphQLErrors }) {
|
||||
setError(graphQLErrors[0]?.extensions?.info as string);
|
||||
setTimeout(() => setError(''), 3000);
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
getCategory({ variables: { id: id as string } });
|
||||
}, [id]);
|
||||
|
||||
const form = useFormik({
|
||||
initialValues: {
|
||||
name: category?.name || '',
|
||||
description: category?.description || '',
|
||||
imageName: category?.image.name || '',
|
||||
imageSource: category?.image.src || '',
|
||||
},
|
||||
validationSchema: Yup.object().shape({
|
||||
name: Yup.string().required('Name is required'),
|
||||
description: Yup.string().required('Description is required'),
|
||||
imageName: Yup.string().required('Image is required'),
|
||||
imageSource: Yup.string().required('Image is required'),
|
||||
}),
|
||||
onSubmit: ({ name, description, imageName, imageSource }) => {
|
||||
updateCategory({
|
||||
variables: {
|
||||
id: id as string,
|
||||
category: {
|
||||
name,
|
||||
description,
|
||||
image: { name: imageName, src: imageSource },
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
enableReinitialize: true,
|
||||
});
|
||||
|
||||
if (role !== 'developer') return (
|
||||
<>
|
||||
{role === 'admin' && <Navigate to='/clients' />}
|
||||
{['client', 'productOwer'].includes(role as string) && <Navigate to='/project' />}
|
||||
</>
|
||||
)
|
||||
|
||||
if (categoryLoading) return (
|
||||
<Spinner fullScreen color={role || 'client'} />
|
||||
);
|
||||
|
||||
if (categoryError || !category) return (
|
||||
<Wrapper color={role}>
|
||||
<Box
|
||||
width='100%'
|
||||
height='100vh'
|
||||
display='grid'
|
||||
alignItems='center'
|
||||
justifyContent='center'
|
||||
>
|
||||
<Box>
|
||||
<Empty />
|
||||
</Box>
|
||||
</Box>
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
{deleteCategoryModal && (
|
||||
<Modal
|
||||
color={role || 'client'}
|
||||
title='Delete Category'
|
||||
description='
|
||||
If you delete this category you cannot recover it.'
|
||||
onClose={() => setDeleteCategoryModal(false)}
|
||||
onConfirm={() => deleteCategory({ variables: { id: id as string } })}
|
||||
></Modal>
|
||||
)}
|
||||
<Box>
|
||||
<Button
|
||||
text='Back'
|
||||
color={role || 'client'}
|
||||
size='small'
|
||||
onClick={() => navigate(-1)}
|
||||
iconLeft={<ArrowLeft />}
|
||||
/>
|
||||
<Text variant='headline' weight='bold'>
|
||||
Update Category
|
||||
</Text>
|
||||
</Box>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='0.5fr 2fr'
|
||||
columnGap='25px'
|
||||
marginTop='1rem'
|
||||
>
|
||||
<Box display='grid' rowGap='0.5rem' gridTemplateRows='50px'>
|
||||
<SectionSelector
|
||||
icon={<General />}
|
||||
color={role || 'client'}
|
||||
text='General'
|
||||
selected
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
background='white'
|
||||
boxShadow='1px 1px 10px 0px rgba(50, 59, 105, 0.25)'
|
||||
borderRadius='10px'
|
||||
width='100%'
|
||||
padding='30px'
|
||||
>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='auto 1fr'
|
||||
columnGap='1rem'
|
||||
alignItems='center'
|
||||
marginBottom='50px'
|
||||
>
|
||||
<Text variant='subheader' weight='bold'>
|
||||
General
|
||||
</Text>
|
||||
{error && <Alert color='error' text={error} />}
|
||||
{success && (
|
||||
<Alert color='success' text='Category updated successfully' />
|
||||
)}
|
||||
</Box>
|
||||
<form onSubmit={form.handleSubmit}>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='auto'
|
||||
rowGap='0.5rem'
|
||||
position='relative'
|
||||
>
|
||||
<Input
|
||||
name='name'
|
||||
label='Name'
|
||||
color={role || 'client'}
|
||||
value={form.values.name}
|
||||
onChange={form.handleChange}
|
||||
onBlur={form.handleBlur}
|
||||
error={form.touched.name && !!form.errors.name}
|
||||
errorMessage={form.errors.name}
|
||||
/>
|
||||
<Input
|
||||
type='file'
|
||||
label='Image'
|
||||
color={role || 'client'}
|
||||
onChange={async (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
const formData = new FormData();
|
||||
|
||||
if (event.target.files && event.target.files[0]) {
|
||||
formData.append('file', event.target.files[0]);
|
||||
formData.append('upload_preset', 'xofll5kc');
|
||||
|
||||
form.setFieldValue('imageName', '');
|
||||
form.setFieldValue('imageSource', '');
|
||||
|
||||
const data = await (
|
||||
await fetch(`${import.meta.env.VITE_CLOUDINARY_URL}`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
).json();
|
||||
|
||||
const filename = data.original_filename;
|
||||
const filesource = data.secure_url;
|
||||
|
||||
form.setFieldValue('imageName', filename);
|
||||
form.setFieldValue('imageSource', filesource);
|
||||
}
|
||||
}}
|
||||
error={
|
||||
form.touched.imageName &&
|
||||
(!!form.errors.imageName || !!form.errors.imageSource)
|
||||
}
|
||||
errorMessage={form.errors.imageName}
|
||||
/>
|
||||
<TextArea
|
||||
name='description'
|
||||
label='Description'
|
||||
color={role || 'client'}
|
||||
value={form.values.description}
|
||||
onChange={form.handleChange}
|
||||
onBlur={form.handleBlur}
|
||||
error={form.touched.description && !!form.errors.description}
|
||||
errorMessage={form.errors.description}
|
||||
/>
|
||||
<Box
|
||||
marginTop='0.5rem'
|
||||
display='flex'
|
||||
justifyContent='space-between'
|
||||
>
|
||||
<Button
|
||||
variant='text'
|
||||
color='error'
|
||||
text='Delete Category'
|
||||
onClick={() => setDeleteCategoryModal(true)}
|
||||
/>
|
||||
<Button
|
||||
variant='primary-action'
|
||||
color={role || 'client'}
|
||||
text='Save'
|
||||
type='submit'
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</form>
|
||||
</Box>
|
||||
</Box>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default CategorySettings;
|
||||
@@ -1,5 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
padding: 35px 45px 35px 120px;
|
||||
`;
|
||||
@@ -1,480 +0,0 @@
|
||||
import * as Yup from 'yup';
|
||||
import { useFormik } from 'formik';
|
||||
import { Navigate, useNavigate, useParams } from 'react-router';
|
||||
import { useMutation, useQuery, useReactiveVar } from '@apollo/client';
|
||||
import { useState } from 'react';
|
||||
import { roleVar } from '../../graphql/state';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Text,
|
||||
SectionSelector,
|
||||
Input,
|
||||
Select,
|
||||
Alert,
|
||||
Spinner,
|
||||
} from '../../components';
|
||||
import { Wrapper } from './styles';
|
||||
import { ArrowLeft, Profile, Security } from '../../assets';
|
||||
import {
|
||||
GetCountryCodesQuery,
|
||||
GetCountryCodesQueryVariables,
|
||||
CreateUserMutation,
|
||||
CreateUserMutationVariables,
|
||||
} from '../../graphql/types';
|
||||
import { GET_COUNTRY_CODES } from '../../graphql/auth.api';
|
||||
import { CREATE_USER, GET_ALL_USERS } from '../../graphql/admin.api';
|
||||
|
||||
const CreateUser = () => {
|
||||
const navigate = useNavigate();
|
||||
const { role: newUserRole } = useParams<{
|
||||
role: 'Client' | 'ProductOwner' | 'Developer';
|
||||
}>();
|
||||
const role = useReactiveVar(roleVar);
|
||||
const [newUser, setNewUser] = useState<{
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
password: string;
|
||||
phone: {
|
||||
prefix: string;
|
||||
number: string;
|
||||
};
|
||||
address: {
|
||||
place: string;
|
||||
city: string;
|
||||
country: string;
|
||||
zip: string;
|
||||
};
|
||||
role: 'Client' | 'ProductOwner' | 'Developer';
|
||||
}>({
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
email: '',
|
||||
password: '',
|
||||
phone: {
|
||||
prefix: '',
|
||||
number: '',
|
||||
},
|
||||
address: {
|
||||
place: '',
|
||||
city: '',
|
||||
country: '',
|
||||
zip: '',
|
||||
},
|
||||
role: newUserRole!,
|
||||
});
|
||||
|
||||
const [selectedSection, setSelectedSection] = useState<
|
||||
'general' | 'security'
|
||||
>('general');
|
||||
const [error, setError] = useState<string>('');
|
||||
|
||||
const [createUser, { loading: createUserLoading }] = useMutation<
|
||||
CreateUserMutation,
|
||||
CreateUserMutationVariables
|
||||
>(CREATE_USER, {
|
||||
onCompleted() {
|
||||
const location =
|
||||
newUserRole === 'Client'
|
||||
? '/clients'
|
||||
: newUserRole === 'ProductOwner'
|
||||
? '/product-owners'
|
||||
: '/developers';
|
||||
navigate(location);
|
||||
},
|
||||
onError({ graphQLErrors }) {
|
||||
setError(graphQLErrors[0]?.extensions?.info as string);
|
||||
setTimeout(() => setError(''), 3000);
|
||||
},
|
||||
refetchQueries: [{ query: GET_ALL_USERS }],
|
||||
});
|
||||
|
||||
const generalForm = useFormik({
|
||||
initialValues: {
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
email: '',
|
||||
prefix: '',
|
||||
number: '',
|
||||
place: '',
|
||||
city: '',
|
||||
zip: '',
|
||||
country: '',
|
||||
},
|
||||
validationSchema: Yup.object().shape({
|
||||
firstName: Yup.string().required('First Name is required'),
|
||||
lastName: Yup.string().required('Last Name is required'),
|
||||
email: Yup.string()
|
||||
.required('Email is required')
|
||||
.email('Email is invalid'),
|
||||
prefix: Yup.string().required('Prefix is required'),
|
||||
// prettier-ignore
|
||||
number: Yup.number().typeError('Phone must be a number').required('Phone is required'),
|
||||
place: Yup.string().required('Address is required'),
|
||||
city: Yup.string().required('City is required'),
|
||||
country: Yup.string().required('Country is required'),
|
||||
// prettier-ignore
|
||||
zip: Yup.number().typeError('Zip must be a number').required('Zip is required'),
|
||||
}),
|
||||
onSubmit: ({
|
||||
firstName,
|
||||
lastName,
|
||||
email,
|
||||
prefix,
|
||||
number,
|
||||
place,
|
||||
city,
|
||||
country,
|
||||
zip,
|
||||
}) => {
|
||||
setNewUser({
|
||||
...newUser,
|
||||
firstName,
|
||||
lastName,
|
||||
email,
|
||||
phone: { prefix, number },
|
||||
address: { place, city, country, zip },
|
||||
});
|
||||
setSelectedSection('security');
|
||||
},
|
||||
});
|
||||
|
||||
const { data: countryCodes, loading: countryCodesLoading } = useQuery<
|
||||
GetCountryCodesQuery,
|
||||
GetCountryCodesQueryVariables
|
||||
>(GET_COUNTRY_CODES, {
|
||||
onCompleted({ getCountryCode }) {
|
||||
generalForm.setFieldValue('prefix', getCountryCode[0].prefix);
|
||||
generalForm.setFieldValue('country', getCountryCode[0].country);
|
||||
},
|
||||
});
|
||||
|
||||
const securityForm = useFormik({
|
||||
initialValues: {
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
},
|
||||
validationSchema: Yup.object().shape({
|
||||
password: Yup.string()
|
||||
.required('Password is required')
|
||||
.min(6, 'Password is 6 characters minimum'),
|
||||
confirmPassword: Yup.string()
|
||||
.required('Confirm password is required')
|
||||
.oneOf(
|
||||
[Yup.ref('password')],
|
||||
"Confirm new password doesn't match with new password"
|
||||
),
|
||||
}),
|
||||
onSubmit: ({ password }) => {
|
||||
setNewUser({ ...newUser, password });
|
||||
createUser({ variables: { user: { ...newUser, password } } });
|
||||
},
|
||||
});
|
||||
|
||||
if (role !== 'admin') return (
|
||||
<Navigate to='/' />
|
||||
)
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Box>
|
||||
<Button
|
||||
text='Back'
|
||||
color={role || 'client'}
|
||||
size='small'
|
||||
onClick={() => navigate(-1)}
|
||||
iconLeft={<ArrowLeft />}
|
||||
/>
|
||||
<Text variant='headline' weight='bold'>
|
||||
Create{' '}
|
||||
{newUserRole === 'ProductOwner' ? 'Product Owner' : newUserRole}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='0.5fr 2fr'
|
||||
columnGap='25px'
|
||||
marginTop='1rem'
|
||||
>
|
||||
<Box display='grid' rowGap='0.5rem' gridTemplateRows='50px'>
|
||||
<SectionSelector
|
||||
icon={<Profile />}
|
||||
color={role || 'client'}
|
||||
text='General'
|
||||
selected={selectedSection === 'general'}
|
||||
/>
|
||||
<SectionSelector
|
||||
icon={<Security />}
|
||||
color={role || 'client'}
|
||||
text='Security'
|
||||
selected={selectedSection === 'security'}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
background='white'
|
||||
boxShadow='1px 1px 10px 0px rgba(50, 59, 105, 0.25)'
|
||||
borderRadius='10px'
|
||||
width='100%'
|
||||
padding='30px'
|
||||
>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='auto 1fr'
|
||||
columnGap='1rem'
|
||||
alignItems='center'
|
||||
marginBottom='50px'
|
||||
>
|
||||
<Text variant='subheader' weight='bold'>
|
||||
{selectedSection === 'general' ? 'General' : 'Security'}
|
||||
</Text>
|
||||
{error && <Alert color='error' text={error} />}
|
||||
</Box>
|
||||
{selectedSection === 'general' && (
|
||||
<>
|
||||
{!countryCodesLoading ? (
|
||||
<form onSubmit={generalForm.handleSubmit}>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='auto'
|
||||
rowGap='0.5rem'
|
||||
position='relative'
|
||||
>
|
||||
<Input
|
||||
name='firstName'
|
||||
label='First Name'
|
||||
color={role || 'client'}
|
||||
value={generalForm.values.firstName}
|
||||
onChange={generalForm.handleChange}
|
||||
onBlur={generalForm.handleBlur}
|
||||
error={
|
||||
generalForm.touched.firstName &&
|
||||
!!generalForm.errors.firstName
|
||||
}
|
||||
errorMessage={generalForm.errors.firstName}
|
||||
/>
|
||||
<Input
|
||||
name='lastName'
|
||||
label='Last Name'
|
||||
color={role || 'client'}
|
||||
value={generalForm.values.lastName}
|
||||
onChange={generalForm.handleChange}
|
||||
onBlur={generalForm.handleBlur}
|
||||
error={
|
||||
generalForm.touched.lastName &&
|
||||
!!generalForm.errors.lastName
|
||||
}
|
||||
errorMessage={generalForm.errors.lastName}
|
||||
/>
|
||||
<Input
|
||||
name='email'
|
||||
label='Email'
|
||||
color={role || 'client'}
|
||||
value={generalForm.values.email}
|
||||
onChange={generalForm.handleChange}
|
||||
onBlur={generalForm.handleBlur}
|
||||
error={
|
||||
generalForm.touched.email && !!generalForm.errors.email
|
||||
}
|
||||
errorMessage={generalForm.errors.email}
|
||||
/>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='1fr 1.5fr'
|
||||
columnGap='10px'
|
||||
>
|
||||
<Select
|
||||
name='prefix'
|
||||
label='Country Code'
|
||||
color={role || 'client'}
|
||||
options={
|
||||
countryCodes?.getCountryCode
|
||||
? countryCodes.getCountryCode.map(
|
||||
({ prefix, country }) => ({
|
||||
value: prefix,
|
||||
label: `+${prefix} (${country})`,
|
||||
})
|
||||
)
|
||||
: [{ value: '216', label: '+216' }]
|
||||
}
|
||||
onChange={generalForm.handleChange}
|
||||
onBlur={generalForm.handleBlur}
|
||||
value={generalForm.values.prefix}
|
||||
select={generalForm.values.prefix}
|
||||
error={
|
||||
generalForm.touched.prefix &&
|
||||
!!generalForm.errors.prefix
|
||||
}
|
||||
errorMessage={generalForm.errors.prefix}
|
||||
/>
|
||||
<Input
|
||||
name='number'
|
||||
type='tel'
|
||||
label='Phone'
|
||||
color={role || 'client'}
|
||||
onChange={generalForm.handleChange}
|
||||
onBlur={generalForm.handleBlur}
|
||||
value={generalForm.values.number}
|
||||
error={
|
||||
generalForm.touched.number &&
|
||||
!!generalForm.errors.number
|
||||
}
|
||||
errorMessage={generalForm.errors.number}
|
||||
/>
|
||||
</Box>
|
||||
<Input
|
||||
name='place'
|
||||
label='Address'
|
||||
color={role || 'client'}
|
||||
onChange={generalForm.handleChange}
|
||||
onBlur={generalForm.handleBlur}
|
||||
value={generalForm.values.place}
|
||||
error={
|
||||
generalForm.touched.place && !!generalForm.errors.place
|
||||
}
|
||||
errorMessage={generalForm.errors.place}
|
||||
/>
|
||||
<Input
|
||||
name='city'
|
||||
label='City'
|
||||
color={role || 'client'}
|
||||
onChange={generalForm.handleChange}
|
||||
onBlur={generalForm.handleBlur}
|
||||
value={generalForm.values.city}
|
||||
error={
|
||||
generalForm.touched.city && !!generalForm.errors.city
|
||||
}
|
||||
errorMessage={generalForm.errors.city}
|
||||
/>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='2fr 1fr'
|
||||
columnGap='10px'
|
||||
>
|
||||
<Select
|
||||
name='country'
|
||||
label='Country'
|
||||
color={role || 'client'}
|
||||
options={
|
||||
countryCodes?.getCountryCode
|
||||
? countryCodes.getCountryCode.map(
|
||||
({ country }) => ({
|
||||
value: country,
|
||||
label: country,
|
||||
})
|
||||
)
|
||||
: [{ value: 'Tunisia', label: 'Tunisia' }]
|
||||
}
|
||||
onChange={generalForm.handleChange}
|
||||
onBlur={generalForm.handleBlur}
|
||||
value={generalForm.values.country}
|
||||
select={generalForm.values.country}
|
||||
error={
|
||||
generalForm.touched.country &&
|
||||
!!generalForm.errors.country
|
||||
}
|
||||
errorMessage={generalForm.errors.country}
|
||||
/>
|
||||
<Input
|
||||
name='zip'
|
||||
label='Zip Code'
|
||||
color={role || 'client'}
|
||||
onChange={generalForm.handleChange}
|
||||
onBlur={generalForm.handleBlur}
|
||||
value={generalForm.values.zip}
|
||||
error={
|
||||
generalForm.touched.zip && !!generalForm.errors.zip
|
||||
}
|
||||
errorMessage={generalForm.errors.zip}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
marginTop='0.5rem'
|
||||
display='grid'
|
||||
gridTemplateColumns='repeat(2, auto)'
|
||||
justifyContent='flex-end'
|
||||
>
|
||||
<Button
|
||||
variant='primary-action'
|
||||
color={role || 'client'}
|
||||
text='Next'
|
||||
type='submit'
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</form>
|
||||
) : (
|
||||
<Box display='grid' alignItems='center' justifyContent='center'>
|
||||
<Spinner color={role || 'client'} />
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{selectedSection === 'security' && (
|
||||
<form onSubmit={securityForm.handleSubmit}>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='auto'
|
||||
rowGap='0.5rem'
|
||||
position='relative'
|
||||
>
|
||||
<Input
|
||||
name='password'
|
||||
label='Password'
|
||||
color={role || 'client'}
|
||||
type='password'
|
||||
value={securityForm.values.password}
|
||||
onChange={securityForm.handleChange}
|
||||
onBlur={securityForm.handleBlur}
|
||||
error={
|
||||
securityForm.touched.password &&
|
||||
!!securityForm.errors.password
|
||||
}
|
||||
errorMessage={securityForm.errors.password}
|
||||
/>
|
||||
<Input
|
||||
name='confirmPassword'
|
||||
label='Confirm Password'
|
||||
color={role || 'client'}
|
||||
type='password'
|
||||
value={securityForm.values.confirmPassword}
|
||||
onChange={securityForm.handleChange}
|
||||
onBlur={securityForm.handleBlur}
|
||||
error={
|
||||
securityForm.touched.confirmPassword &&
|
||||
!!securityForm.errors.confirmPassword
|
||||
}
|
||||
errorMessage={securityForm.errors.confirmPassword}
|
||||
/>
|
||||
<Box
|
||||
marginTop='0.5rem'
|
||||
display='flex'
|
||||
justifyContent='flex-end'
|
||||
>
|
||||
<Box marginRight='15px' display='flex' alignItems='center'>
|
||||
<Button
|
||||
color={role || 'client'}
|
||||
text='Previous'
|
||||
type='submit'
|
||||
onClick={() => setSelectedSection('general')}
|
||||
/>
|
||||
</Box>
|
||||
<Button
|
||||
variant='primary-action'
|
||||
color={role || 'client'}
|
||||
text='Create'
|
||||
type='submit'
|
||||
loading={createUserLoading}
|
||||
disabled={createUserLoading}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</form>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateUser;
|
||||
@@ -1,9 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
padding: 35px 45px 35px 120px;
|
||||
|
||||
div {
|
||||
cursor: default;
|
||||
}
|
||||
`;
|
||||
@@ -1,179 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useLazyQuery, useReactiveVar } from '@apollo/client';
|
||||
import { Navigate } from 'react-router';
|
||||
import { roleVar } from '../../graphql/state';
|
||||
import { Empty, Settings } from '../../assets';
|
||||
import {
|
||||
Box,
|
||||
Text,
|
||||
Button,
|
||||
Spinner,
|
||||
ImagePreview,
|
||||
Link,
|
||||
} from '../../components';
|
||||
import { Wrapper } from './styles';
|
||||
import {
|
||||
FeatureOutput,
|
||||
GetAllFeaturesQuery,
|
||||
GetAllFeaturesQueryVariables,
|
||||
GetFeatureByIdQuery,
|
||||
GetFeatureByIdQueryVariables,
|
||||
} from '../../graphql/types';
|
||||
import { GET_ALL_FEATURES, GET_FEATURE_BY_ID } from '../../graphql/feature.api';
|
||||
|
||||
const Feature = () => {
|
||||
const role = useReactiveVar(roleVar);
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const [feature, setFeature] = useState<FeatureOutput>();
|
||||
|
||||
const [getFeatures, { loading: featuresLoading, error: featuresError }] = useLazyQuery<
|
||||
GetAllFeaturesQuery,
|
||||
GetAllFeaturesQueryVariables
|
||||
>(GET_ALL_FEATURES, {
|
||||
onCompleted({ getAllFeatures }) {
|
||||
setFeature(getAllFeatures[0]);
|
||||
}
|
||||
});
|
||||
|
||||
const [getFeature, { loading: featureLoading, error: featureError }] = useLazyQuery<
|
||||
GetFeatureByIdQuery,
|
||||
GetFeatureByIdQueryVariables
|
||||
>(GET_FEATURE_BY_ID, {
|
||||
onCompleted({ getFeatureById }) {
|
||||
setFeature(getFeatureById);
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
getFeature({ variables: { id } });
|
||||
} else {
|
||||
getFeatures();
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
if (role !== 'developer') return (
|
||||
<>
|
||||
{role === 'admin' && <Navigate to='/clients' />}
|
||||
{['client', 'productOwer'].includes(role as string) && <Navigate to='/project' />}
|
||||
</>
|
||||
);
|
||||
|
||||
if (featureLoading || featureLoading) return (
|
||||
<Spinner fullScreen color={role || 'client'} />
|
||||
);
|
||||
|
||||
if (featuresError || featureError || !feature) return (
|
||||
<Wrapper color={role}>
|
||||
<Box
|
||||
width='100%'
|
||||
height='100vh'
|
||||
display='grid'
|
||||
alignItems='center'
|
||||
justifyContent='center'
|
||||
>
|
||||
<Box>
|
||||
<Empty />
|
||||
</Box>
|
||||
</Box>
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Box padding='35px 45px 0px 120px'>
|
||||
<Box
|
||||
display='flex'
|
||||
flexDirection='row'
|
||||
alignItems='center'
|
||||
marginBottom='20px'
|
||||
>
|
||||
<Box flexGrow='1'>
|
||||
<Text variant='headline' weight='bold'>
|
||||
{feature.name}
|
||||
</Text>
|
||||
</Box>
|
||||
<Button
|
||||
color={role || 'client'}
|
||||
variant='primary-action'
|
||||
text='Settings'
|
||||
iconLeft={<Settings />}
|
||||
onClick={() =>
|
||||
navigate(`/feature-settings/${id || feature.id}`)
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
<Box marginBottom='30px'>
|
||||
<Text variant='headline' gutterBottom>
|
||||
Description
|
||||
</Text>
|
||||
<Text>{feature.description}</Text>
|
||||
</Box>
|
||||
<Box
|
||||
display='flex'
|
||||
flexDirection='row'
|
||||
alignItems='center'
|
||||
marginBottom='30px'
|
||||
>
|
||||
<Box marginRight='35px'>
|
||||
<Text variant='headline' gutterBottom>
|
||||
Price
|
||||
</Text>
|
||||
</Box>
|
||||
<Text variant='title' weight='bold'>
|
||||
${feature.price}
|
||||
</Text>
|
||||
</Box>
|
||||
{feature.wireframes && (
|
||||
<Box
|
||||
display='flex'
|
||||
flexDirection='column'
|
||||
marginBottom='30px'
|
||||
>
|
||||
<Text variant='headline' gutterBottom>
|
||||
Wireframes
|
||||
</Text>
|
||||
<Box
|
||||
background='white'
|
||||
boxShadow='1px 1px 10px 0px rgba(50, 59, 105, 0.25)'
|
||||
borderRadius='10px'
|
||||
width='100%'
|
||||
padding='30px'
|
||||
display='grid'
|
||||
gridTemplate='auto / repeat(auto-fit, 175px)'
|
||||
gap='30px'
|
||||
alignItems='stretch'
|
||||
>
|
||||
{feature.wireframes.map((image) => (
|
||||
<ImagePreview
|
||||
key={image.name}
|
||||
color={role}
|
||||
image={image}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
<Box
|
||||
display='flex'
|
||||
flexDirection='row'
|
||||
alignItems='center'
|
||||
marginBottom='30px'
|
||||
>
|
||||
<Box marginRight='35px'>
|
||||
<Text variant='headline' gutterBottom>
|
||||
Repo
|
||||
</Text>
|
||||
</Box>
|
||||
<Link url target='_blank' href={feature.repo}>
|
||||
{feature.repo}
|
||||
</Link>
|
||||
</Box>
|
||||
</Box>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Feature;
|
||||
@@ -1,12 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
type WrapperProps = {
|
||||
color?: 'client' | 'productOwner' | 'developer' | 'admin';
|
||||
};
|
||||
|
||||
export const Wrapper = styled.div<WrapperProps>`
|
||||
.empty {
|
||||
fill: ${({ theme, color }) =>
|
||||
color ? theme.colors[color].main : theme.colors.client.main};
|
||||
}
|
||||
`;
|
||||
@@ -1,586 +0,0 @@
|
||||
import * as Yup from 'yup';
|
||||
import { useFormik } from 'formik';
|
||||
import { Navigate, useNavigate, useParams } from 'react-router';
|
||||
import { useLazyQuery, useMutation, useReactiveVar } from '@apollo/client';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { roleVar } from '../../graphql/state';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Text,
|
||||
SectionSelector,
|
||||
Input,
|
||||
Alert,
|
||||
TextArea,
|
||||
Spinner,
|
||||
Modal,
|
||||
CheckBox,
|
||||
ImagePreview,
|
||||
} from '../../components';
|
||||
import { Wrapper } from './styles';
|
||||
import { ArrowLeft, Design, Empty, General } from '../../assets';
|
||||
import {
|
||||
DeleteFeatureMutation,
|
||||
DeleteFeatureMutationVariables,
|
||||
FeatureOutput,
|
||||
GetFeatureByIdQuery,
|
||||
GetFeatureByIdQueryVariables,
|
||||
UpdateFeatureMutation,
|
||||
UpdateFeatureMutationVariables,
|
||||
} from '../../graphql/types';
|
||||
import {
|
||||
DELETE_FEATURE,
|
||||
GET_FEATURE_BY_ID,
|
||||
UPDATE_FEATURE,
|
||||
} from '../../graphql/feature.api';
|
||||
|
||||
const FeatureSettings = () => {
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const role = useReactiveVar(roleVar);
|
||||
|
||||
const [selectedSection, setSelectedSection] = useState<
|
||||
'general' | 'wireframes'
|
||||
>('general');
|
||||
const [error, setError] = useState<string>('');
|
||||
const [success, setSuccess] = useState<boolean>(false);
|
||||
const [feature, setFeature] = useState<FeatureOutput>({
|
||||
id: id as string,
|
||||
name: '',
|
||||
description: '',
|
||||
featureType: '',
|
||||
image: {
|
||||
name: '',
|
||||
src: '',
|
||||
},
|
||||
price: 0,
|
||||
repo: '',
|
||||
});
|
||||
|
||||
const [deleteFeatureModal, setDeleteFeatureModal] = useState<boolean>(false);
|
||||
|
||||
const [getFeature, { loading: featureLoading, error: featureError }] = useLazyQuery<
|
||||
GetFeatureByIdQuery,
|
||||
GetFeatureByIdQueryVariables
|
||||
>(GET_FEATURE_BY_ID, {
|
||||
onCompleted({ getFeatureById }) {
|
||||
setFeature(getFeatureById);
|
||||
}
|
||||
});
|
||||
|
||||
const [updateFeature, { loading }] = useMutation<
|
||||
UpdateFeatureMutation,
|
||||
UpdateFeatureMutationVariables
|
||||
>(UPDATE_FEATURE, {
|
||||
onCompleted({ updateFeature: data }) {
|
||||
setFeature(data);
|
||||
setSuccess(true);
|
||||
setTimeout(() => setSuccess(false), 3000);
|
||||
},
|
||||
onError({ graphQLErrors }) {
|
||||
setError(graphQLErrors[0]?.extensions?.info as string);
|
||||
setTimeout(() => setError(''), 3000);
|
||||
},
|
||||
});
|
||||
|
||||
const [deleteFeature] = useMutation<
|
||||
DeleteFeatureMutation,
|
||||
DeleteFeatureMutationVariables
|
||||
>(DELETE_FEATURE, {
|
||||
onCompleted() {
|
||||
navigate('/feature');
|
||||
},
|
||||
onError({ graphQLErrors }) {
|
||||
setError(graphQLErrors[0]?.extensions?.info as string);
|
||||
setTimeout(() => setError(''), 3000);
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
getFeature({ variables: { id: id as string } });
|
||||
}, [id]);
|
||||
|
||||
const generalForm = useFormik({
|
||||
initialValues: {
|
||||
name: feature?.name || '',
|
||||
description: feature?.description || '',
|
||||
imageName: feature?.image.name || '',
|
||||
imageSource: feature?.image.src || '',
|
||||
featureType: feature?.featureType || '',
|
||||
price: feature?.price.toString() || '',
|
||||
repo: feature?.repo || '',
|
||||
},
|
||||
validationSchema: Yup.object().shape({
|
||||
name: Yup.string().required('Name is required'),
|
||||
description: Yup.string().required('Description is required'),
|
||||
imageName: Yup.string().required('Image is required'),
|
||||
imageSource: Yup.string().required('Image is required'),
|
||||
featureType: Yup.string().required('Feature Type is required'),
|
||||
// prettier-ignore
|
||||
price: Yup.number().typeError('Price must be a number').required('Price is required'),
|
||||
repo: Yup.string().required('Repo is required'),
|
||||
}),
|
||||
onSubmit: ({
|
||||
name,
|
||||
description,
|
||||
imageName,
|
||||
imageSource,
|
||||
featureType,
|
||||
price,
|
||||
repo,
|
||||
}) => {
|
||||
updateFeature({
|
||||
variables: {
|
||||
id: id as string,
|
||||
feature: {
|
||||
name,
|
||||
description,
|
||||
featureType,
|
||||
image: { name: imageName, src: imageSource },
|
||||
price: parseFloat(price),
|
||||
repo,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
enableReinitialize: true,
|
||||
});
|
||||
|
||||
const wireframesForm = useFormik<{
|
||||
wireframes: Array<{ name: string; src: string }>;
|
||||
}>({
|
||||
initialValues: {
|
||||
wireframes:
|
||||
feature?.wireframes?.map((wireframe) => ({
|
||||
name: wireframe.name,
|
||||
src: wireframe.src,
|
||||
})) || [],
|
||||
},
|
||||
onSubmit: ({ wireframes }) => {
|
||||
updateFeature({
|
||||
variables: {
|
||||
id: id as string,
|
||||
feature: {
|
||||
name: feature.name,
|
||||
description: feature.description,
|
||||
price: feature.price,
|
||||
repo: feature.repo,
|
||||
featureType: feature.featureType,
|
||||
image: { name: feature.image.name, src: feature.image.src },
|
||||
wireframes,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
enableReinitialize: true,
|
||||
});
|
||||
|
||||
if (role !== 'developer') return (
|
||||
<>
|
||||
{role === 'admin' && <Navigate to='/clients' />}
|
||||
{['client', 'productOwer'].includes(role as string) && <Navigate to='/project' />}
|
||||
</>
|
||||
)
|
||||
|
||||
if (featureLoading || featureLoading) return (
|
||||
<Spinner fullScreen color={role || 'client'} />
|
||||
);
|
||||
|
||||
if (featureError || !feature) return (
|
||||
<Wrapper color={role}>
|
||||
<Box
|
||||
width='100%'
|
||||
height='100vh'
|
||||
display='grid'
|
||||
alignItems='center'
|
||||
justifyContent='center'
|
||||
>
|
||||
<Box>
|
||||
<Empty />
|
||||
</Box>
|
||||
</Box>
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Box>
|
||||
<Button
|
||||
text='Back'
|
||||
color={role || 'client'}
|
||||
size='small'
|
||||
onClick={() => navigate(-1)}
|
||||
iconLeft={<ArrowLeft />}
|
||||
/>
|
||||
<Text variant='headline' weight='bold'>
|
||||
Update Feature
|
||||
</Text>
|
||||
</Box>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='0.5fr 2fr'
|
||||
columnGap='25px'
|
||||
marginTop='1rem'
|
||||
>
|
||||
<Box display='grid' rowGap='0.5rem' gridTemplateRows='50px'>
|
||||
<SectionSelector
|
||||
icon={<General />}
|
||||
color={role || 'client'}
|
||||
text='General'
|
||||
selected={selectedSection === 'general'}
|
||||
onClick={() => setSelectedSection('general')}
|
||||
/>
|
||||
<SectionSelector
|
||||
icon={<Design />}
|
||||
color={role || 'client'}
|
||||
text='Wireframes'
|
||||
selected={selectedSection === 'wireframes'}
|
||||
onClick={() => setSelectedSection('wireframes')}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
background='white'
|
||||
boxShadow='1px 1px 10px 0px rgba(50, 59, 105, 0.25)'
|
||||
borderRadius='10px'
|
||||
width='100%'
|
||||
padding='30px'
|
||||
>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='auto 1fr'
|
||||
columnGap='1rem'
|
||||
alignItems='center'
|
||||
marginBottom='50px'
|
||||
>
|
||||
<Text variant='subheader' weight='bold'>
|
||||
{selectedSection === 'general' ? 'General' : 'Wireframes'}
|
||||
</Text>
|
||||
{error && <Alert color='error' text={error} />}
|
||||
{success && (
|
||||
<Alert color='success' text='Feature updated successfully' />
|
||||
)}
|
||||
</Box>
|
||||
{selectedSection === 'general' && (
|
||||
<>
|
||||
{deleteFeatureModal && (
|
||||
<Modal
|
||||
color={role || 'client'}
|
||||
title='Delete Feature'
|
||||
description='
|
||||
If you delete this feature you cannot recover it.'
|
||||
onClose={() => setDeleteFeatureModal(false)}
|
||||
onConfirm={() =>
|
||||
deleteFeature({ variables: { id: id as string } })
|
||||
}
|
||||
></Modal>
|
||||
)}
|
||||
<form onSubmit={generalForm.handleSubmit}>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='auto'
|
||||
rowGap='0.5rem'
|
||||
position='relative'
|
||||
>
|
||||
<Input
|
||||
name='name'
|
||||
label='Name'
|
||||
color={role || 'client'}
|
||||
value={generalForm.values.name}
|
||||
onChange={generalForm.handleChange}
|
||||
onBlur={generalForm.handleBlur}
|
||||
error={
|
||||
generalForm.touched.name && !!generalForm.errors.name
|
||||
}
|
||||
errorMessage={generalForm.errors.name}
|
||||
/>
|
||||
<Input
|
||||
type='file'
|
||||
label='Image'
|
||||
color={role || 'client'}
|
||||
onChange={async (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
const formData = new FormData();
|
||||
|
||||
if (event.target.files && event.target.files[0]) {
|
||||
formData.append('file', event.target.files[0]);
|
||||
formData.append('upload_preset', 'xofll5kc');
|
||||
|
||||
generalForm.setFieldValue('imageName', '');
|
||||
generalForm.setFieldValue('imageSource', '');
|
||||
|
||||
const data = await (
|
||||
await fetch(
|
||||
`${import.meta.env.VITE_CLOUDINARY_URL}`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
const filename = data.original_filename;
|
||||
const filesource = data.secure_url;
|
||||
|
||||
generalForm.setFieldValue('imageName', filename);
|
||||
generalForm.setFieldValue(
|
||||
'imageSource',
|
||||
filesource
|
||||
);
|
||||
}
|
||||
}}
|
||||
error={
|
||||
generalForm.touched.imageName &&
|
||||
(!!generalForm.errors.imageName ||
|
||||
!!generalForm.errors.imageSource)
|
||||
}
|
||||
errorMessage={generalForm.errors.imageName}
|
||||
/>
|
||||
<Box>
|
||||
<Box
|
||||
display='flex'
|
||||
flexDirection='row'
|
||||
alignItems='center'
|
||||
justifyContent='space-between'
|
||||
>
|
||||
<Box justifySelf='flex-start'>
|
||||
<Text
|
||||
variant='body'
|
||||
weight='bold'
|
||||
className='feature-type'
|
||||
>
|
||||
Type
|
||||
</Text>
|
||||
</Box>
|
||||
{!!generalForm.errors.featureType && (
|
||||
<Box justifySelf='flex-end'>
|
||||
<Text variant='body' color='error'>
|
||||
{generalForm.errors.featureType}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<Box
|
||||
display='flex'
|
||||
flexDirection='row'
|
||||
alignItems='center'
|
||||
marginTop='5px'
|
||||
>
|
||||
<Box marginRight='50px'>
|
||||
<CheckBox
|
||||
label='Frontend'
|
||||
name='featureType'
|
||||
color={role || 'client'}
|
||||
onClick={() => {
|
||||
if (
|
||||
generalForm.values.featureType === 'fullstack'
|
||||
) {
|
||||
generalForm.setFieldValue(
|
||||
'featureType',
|
||||
'backend'
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
generalForm.values.featureType === 'backend'
|
||||
) {
|
||||
generalForm.setFieldValue(
|
||||
'featureType',
|
||||
'fullstack'
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
generalForm.values.featureType === 'frontend'
|
||||
) {
|
||||
generalForm.setFieldValue('featureType', '');
|
||||
return;
|
||||
}
|
||||
generalForm.setFieldValue(
|
||||
'featureType',
|
||||
'frontend'
|
||||
);
|
||||
}}
|
||||
checked={
|
||||
generalForm.values.featureType === 'frontend' ||
|
||||
generalForm.values.featureType === 'fullstack'
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<CheckBox
|
||||
label='Backend'
|
||||
name='featureType'
|
||||
color={role || 'client'}
|
||||
onClick={() => {
|
||||
if (
|
||||
generalForm.values.featureType === 'fullstack'
|
||||
) {
|
||||
generalForm.setFieldValue(
|
||||
'featureType',
|
||||
'frontend'
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
generalForm.values.featureType === 'frontend'
|
||||
) {
|
||||
generalForm.setFieldValue(
|
||||
'featureType',
|
||||
'fullstack'
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
generalForm.values.featureType === 'backend'
|
||||
) {
|
||||
generalForm.setFieldValue('featureType', '');
|
||||
return;
|
||||
}
|
||||
generalForm.setFieldValue(
|
||||
'featureType',
|
||||
'backend'
|
||||
);
|
||||
}}
|
||||
checked={
|
||||
generalForm.values.featureType === 'backend' ||
|
||||
generalForm.values.featureType === 'fullstack'
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<TextArea
|
||||
name='description'
|
||||
label='Description'
|
||||
color={role || 'client'}
|
||||
value={generalForm.values.description}
|
||||
onChange={generalForm.handleChange}
|
||||
onBlur={generalForm.handleBlur}
|
||||
error={
|
||||
generalForm.touched.description &&
|
||||
!!generalForm.errors.description
|
||||
}
|
||||
errorMessage={generalForm.errors.description}
|
||||
/>
|
||||
<Input
|
||||
name='price'
|
||||
label='Price'
|
||||
color={role || 'client'}
|
||||
value={generalForm.values.price}
|
||||
onChange={generalForm.handleChange}
|
||||
onBlur={generalForm.handleBlur}
|
||||
error={
|
||||
generalForm.touched.price &&
|
||||
!!generalForm.errors.price
|
||||
}
|
||||
errorMessage={generalForm.errors.price}
|
||||
/>
|
||||
<Input
|
||||
name='repo'
|
||||
label='Repo'
|
||||
color={role || 'client'}
|
||||
value={generalForm.values.repo}
|
||||
onChange={generalForm.handleChange}
|
||||
onBlur={generalForm.handleBlur}
|
||||
error={
|
||||
generalForm.touched.repo && !!generalForm.errors.repo
|
||||
}
|
||||
errorMessage={generalForm.errors.repo}
|
||||
/>
|
||||
<Box
|
||||
marginTop='0.5rem'
|
||||
display='flex'
|
||||
justifyContent='space-between'
|
||||
>
|
||||
<Button
|
||||
variant='text'
|
||||
color='error'
|
||||
text='Delete Feature'
|
||||
onClick={() => setDeleteFeatureModal(true)}
|
||||
/>
|
||||
<Button
|
||||
variant='primary-action'
|
||||
color={role || 'client'}
|
||||
text='Save'
|
||||
type='submit'
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
{selectedSection === 'wireframes' && (
|
||||
<form onSubmit={wireframesForm.handleSubmit}>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplate='auto / repeat(auto-fit, 175px)'
|
||||
gap='30px'
|
||||
alignItems='stretch'
|
||||
>
|
||||
<ImagePreview
|
||||
color={role}
|
||||
image={undefined}
|
||||
onChange={async (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
const formData = new FormData();
|
||||
|
||||
if (event.target.files && event.target.files[0]) {
|
||||
formData.append('file', event.target.files[0]);
|
||||
formData.append('upload_preset', 'xofll5kc');
|
||||
|
||||
const data = await (
|
||||
await fetch(`${import.meta.env.VITE_CLOUDINARY_URL}`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
).json();
|
||||
|
||||
const filename = data.original_filename;
|
||||
const filesource = data.secure_url;
|
||||
|
||||
wireframesForm.setFieldValue('wireframes', [
|
||||
...wireframesForm.values.wireframes,
|
||||
{ name: filename, src: filesource },
|
||||
]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{wireframesForm.values.wireframes.map((image) => (
|
||||
<ImagePreview
|
||||
key={image.name}
|
||||
color={role}
|
||||
image={image}
|
||||
deletable
|
||||
onDelete={() => {
|
||||
wireframesForm.setFieldValue(
|
||||
'wireframes',
|
||||
wireframesForm.values.wireframes.filter(
|
||||
({ name }) => name !== image.name
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
<Box marginTop='1rem' display='flex' justifyContent='flex-end'>
|
||||
<Button
|
||||
variant='primary-action'
|
||||
color={role || 'client'}
|
||||
text='Save'
|
||||
type='submit'
|
||||
loading={loading}
|
||||
/>
|
||||
</Box>
|
||||
</form>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeatureSettings;
|
||||
@@ -1,12 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
padding: 35px 45px 35px 120px;
|
||||
|
||||
.feature-type {
|
||||
background: ${({ theme }) => theme.colors.gray.dark};
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
`;
|
||||
@@ -1,503 +0,0 @@
|
||||
import * as Yup from 'yup';
|
||||
import { useFormik } from 'formik';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useLazyQuery, useReactiveVar } from '@apollo/client';
|
||||
import { Navigate } from 'react-router';
|
||||
import { roleVar } from '../../graphql/state';
|
||||
import { Wrapper } from './styles';
|
||||
import { Alert, Box, Button, Input, Spinner, Text } from '../../components';
|
||||
import { ArrowLeft } from '../../assets';
|
||||
import {
|
||||
GetProjectByIdQuery,
|
||||
GetProjectByIdQueryVariables,
|
||||
ProjectOutput,
|
||||
} from '../../graphql/types';
|
||||
import { GET_PROJECT_BY_ID } from '../../graphql/project.api';
|
||||
|
||||
type Transaction = {
|
||||
amount: number;
|
||||
created: boolean;
|
||||
selectedOption: number;
|
||||
_id: string;
|
||||
};
|
||||
|
||||
type TransactionData = {
|
||||
transactions: Array<Transaction>;
|
||||
remaining_amount: number;
|
||||
amount: number;
|
||||
project_id: string;
|
||||
status: boolean;
|
||||
total_amount: number;
|
||||
_id: string;
|
||||
};
|
||||
|
||||
const Payments = () => {
|
||||
const role = useReactiveVar(roleVar);
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const [project, setProject] = useState<ProjectOutput>();
|
||||
const [success, setSuccess] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string>('');
|
||||
const [transactionsData, setTransactionsData] = useState<TransactionData>();
|
||||
const [selectedChunk, setSelectedChunk] = useState<number | undefined>();
|
||||
const [paymentLoading, setPaymentLoading] = useState<boolean>(false);
|
||||
|
||||
const [getProject, { loading: projectLoading }] = useLazyQuery<
|
||||
GetProjectByIdQuery,
|
||||
GetProjectByIdQueryVariables
|
||||
>(GET_PROJECT_BY_ID, {
|
||||
onCompleted({ getProjectById }) {
|
||||
setProject(getProjectById);
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (id) {
|
||||
getProject({ variables: { id } });
|
||||
|
||||
try {
|
||||
const transactionsResult = await (
|
||||
await fetch(`${import.meta.env.VITE_PAYMENT_API}/transactions`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ project_id: id }),
|
||||
})
|
||||
).json();
|
||||
if (transactionsResult) setTransactionsData(transactionsResult);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
// eslint-disable-next-line
|
||||
}, [id]);
|
||||
|
||||
const paymentForm = useFormik({
|
||||
initialValues: {
|
||||
number: '',
|
||||
expMonth: '',
|
||||
expYear: '',
|
||||
cvc: '',
|
||||
},
|
||||
validationSchema: Yup.object().shape({
|
||||
number: Yup.number()
|
||||
.typeError('Card Number must be a number')
|
||||
.required('Card Number is required'),
|
||||
|
||||
expMonth: Yup.number()
|
||||
.typeError('Expiary Month must be a number')
|
||||
.required('Expiary Month is required'),
|
||||
|
||||
expYear: Yup.number()
|
||||
.typeError('Expiary Year must be a number')
|
||||
.required('Expiary Year is required'),
|
||||
|
||||
cvc: Yup.number()
|
||||
.typeError('CVC must be a number')
|
||||
.required('CVC is required'),
|
||||
}),
|
||||
onSubmit: async ({ number, expMonth, expYear, cvc }, { resetForm }) => {
|
||||
try {
|
||||
setPaymentLoading(true);
|
||||
let amount = 0;
|
||||
|
||||
switch (selectedChunk) {
|
||||
case 0: {
|
||||
amount =
|
||||
(project?.paymentOption.optOne! * project?.totalPrice!) / 100;
|
||||
break;
|
||||
}
|
||||
case 1: {
|
||||
amount =
|
||||
(project?.paymentOption.optTwo! * project?.totalPrice!) / 100;
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
amount =
|
||||
(project?.paymentOption.optThree! * project?.totalPrice!) / 100;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const transactionsResult = await (
|
||||
await fetch(`${import.meta.env.VITE_PAYMENT_API}/charge`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
project_id: project?.id,
|
||||
total_amount: project?.totalPrice,
|
||||
selectedOption: selectedChunk,
|
||||
amount,
|
||||
card: {
|
||||
number,
|
||||
exp_month: expMonth,
|
||||
exp_year: expYear,
|
||||
cvc,
|
||||
},
|
||||
}),
|
||||
})
|
||||
).json();
|
||||
|
||||
if (transactionsResult) {
|
||||
setPaymentLoading(false);
|
||||
setTransactionsData(transactionsResult);
|
||||
setSuccess(true);
|
||||
setSelectedChunk(undefined);
|
||||
resetForm();
|
||||
setTimeout(() => setSuccess(false), 3000);
|
||||
}
|
||||
} catch (err) {
|
||||
setPaymentLoading(false);
|
||||
setError(err as string);
|
||||
setSelectedChunk(undefined);
|
||||
resetForm();
|
||||
setTimeout(() => setError(''), 3000);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return role === 'client' ? (
|
||||
<>
|
||||
{!projectLoading ? (
|
||||
<Wrapper>
|
||||
<Box padding='35px 45px 35px 120px'>
|
||||
<Box
|
||||
display='flex'
|
||||
flexDirection='row'
|
||||
alignItems='center'
|
||||
marginBottom='20px'
|
||||
>
|
||||
<Box flexGrow='1' marginRight='20px'>
|
||||
<Button
|
||||
text='Back'
|
||||
color={role || 'client'}
|
||||
size='small'
|
||||
onClick={() => navigate(-1)}
|
||||
iconLeft={<ArrowLeft />}
|
||||
/>
|
||||
<Text variant='headline' weight='bold'>
|
||||
Payments
|
||||
</Text>
|
||||
</Box>
|
||||
{error && <Alert color='error' text={error} />}
|
||||
{success && <Alert color='success' text='Payment successfull' />}
|
||||
</Box>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='auto 1fr'
|
||||
alignItems='stretch'
|
||||
columnGap='40px'
|
||||
>
|
||||
<Box>
|
||||
<Box
|
||||
padding='15px 20px'
|
||||
borderRadius='10px'
|
||||
boxShadow='1px 1px 10px 0px rgba(50, 59, 105, 0.25)'
|
||||
display='grid'
|
||||
gridTemplateColumns='repeat(3, 1fr)'
|
||||
alignItems='center'
|
||||
justifyContent='flex-start'
|
||||
className='table-head'
|
||||
marginBottom='20px'
|
||||
columnGap='3rem'
|
||||
>
|
||||
<Text variant='title'>Part</Text>
|
||||
<Text variant='title'>Percentage</Text>
|
||||
<Text variant='title'>Action</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
{project?.paymentOption.optOne && (
|
||||
<Box
|
||||
padding='15px 20px'
|
||||
borderRadius='10px'
|
||||
boxShadow='1px 1px 10px 0px rgba(50, 59, 105, 0.25)'
|
||||
display='grid'
|
||||
gridTemplateColumns='repeat(3, 1fr)'
|
||||
alignItems='center'
|
||||
justifyContent='flex-start'
|
||||
marginBottom='20px'
|
||||
columnGap='3rem'
|
||||
onClick={() => {
|
||||
setSelectedChunk(0);
|
||||
}}
|
||||
>
|
||||
<Text variant='headline' weight='bold'>
|
||||
#1
|
||||
</Text>
|
||||
<Text variant='headline' weight='bold'>
|
||||
{project?.paymentOption.optOne}%
|
||||
</Text>
|
||||
<Box>
|
||||
<Button
|
||||
text={
|
||||
transactionsData &&
|
||||
transactionsData.transactions &&
|
||||
!!Array.from(transactionsData.transactions).find(
|
||||
(transaction: any) =>
|
||||
transaction.selectedOption === 0
|
||||
)
|
||||
? 'Paid'
|
||||
: 'Select'
|
||||
}
|
||||
color={role || 'client'}
|
||||
disabled={
|
||||
selectedChunk === 0 ||
|
||||
(transactionsData &&
|
||||
transactionsData.transactions &&
|
||||
!!Array.from(transactionsData.transactions).find(
|
||||
(transaction: any) =>
|
||||
transaction.selectedOption === 0
|
||||
))
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
{project?.paymentOption.optTwo && (
|
||||
<Box
|
||||
padding='15px 20px'
|
||||
borderRadius='10px'
|
||||
boxShadow='1px 1px 10px 0px rgba(50, 59, 105, 0.25)'
|
||||
display='grid'
|
||||
gridTemplateColumns='repeat(3, 1fr)'
|
||||
alignItems='center'
|
||||
justifyContent='flex-start'
|
||||
marginBottom='20px'
|
||||
columnGap='3rem'
|
||||
onClick={() => {
|
||||
setSelectedChunk(1);
|
||||
}}
|
||||
>
|
||||
<Text variant='headline' weight='bold'>
|
||||
#2
|
||||
</Text>
|
||||
<Text variant='headline' weight='bold'>
|
||||
{project?.paymentOption.optTwo}%
|
||||
</Text>
|
||||
<Box>
|
||||
<Button
|
||||
text={
|
||||
transactionsData &&
|
||||
transactionsData.transactions &&
|
||||
!!Array.from(transactionsData.transactions).find(
|
||||
(transaction: any) =>
|
||||
transaction.selectedOption === 1
|
||||
)
|
||||
? 'Paid'
|
||||
: 'Select'
|
||||
}
|
||||
color={role || 'client'}
|
||||
disabled={
|
||||
selectedChunk === 1 ||
|
||||
(transactionsData &&
|
||||
transactionsData.transactions &&
|
||||
!!Array.from(transactionsData.transactions).find(
|
||||
(transaction: any) =>
|
||||
transaction.selectedOption === 1
|
||||
))
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
{project?.paymentOption.optThree && (
|
||||
<Box
|
||||
padding='15px 20px'
|
||||
borderRadius='10px'
|
||||
boxShadow='1px 1px 10px 0px rgba(50, 59, 105, 0.25)'
|
||||
display='grid'
|
||||
gridTemplateColumns='repeat(3, 1fr)'
|
||||
alignItems='center'
|
||||
justifyContent='flex-start'
|
||||
marginBottom='20px'
|
||||
columnGap='3rem'
|
||||
onClick={() => {
|
||||
setSelectedChunk(2);
|
||||
}}
|
||||
>
|
||||
<Text variant='headline' weight='bold'>
|
||||
#3
|
||||
</Text>
|
||||
<Text variant='headline' weight='bold'>
|
||||
{project?.paymentOption.optThree}%
|
||||
</Text>
|
||||
<Box>
|
||||
<Button
|
||||
text={
|
||||
transactionsData &&
|
||||
transactionsData.transactions &&
|
||||
!!Array.from(transactionsData.transactions).find(
|
||||
(transaction: any) =>
|
||||
transaction.selectedOption === 2
|
||||
)
|
||||
? 'Paid'
|
||||
: 'Select'
|
||||
}
|
||||
color={role || 'client'}
|
||||
disabled={
|
||||
selectedChunk === 2 ||
|
||||
(transactionsData &&
|
||||
transactionsData.transactions &&
|
||||
!!Array.from(transactionsData.transactions).find(
|
||||
(transaction: any) =>
|
||||
transaction.selectedOption === 2
|
||||
))
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box>
|
||||
<Box
|
||||
padding='15px 20px'
|
||||
borderRadius='10px'
|
||||
boxShadow='1px 1px 10px 0px rgba(50, 59, 105, 0.25)'
|
||||
background='#F9FAFA'
|
||||
marginBottom='20px'
|
||||
>
|
||||
<Box
|
||||
display='flex'
|
||||
flexDirection='row'
|
||||
alignItems='center'
|
||||
justifyContent='space-between'
|
||||
marginBottom='30px'
|
||||
>
|
||||
<Text variant='subheader'>Paid Costs</Text>
|
||||
<Text variant='subheader' weight='bold'>
|
||||
{transactionsData?.remaining_amount && project?.totalPrice
|
||||
? project.totalPrice ===
|
||||
transactionsData.remaining_amount
|
||||
? project.totalPrice
|
||||
: project.totalPrice -
|
||||
transactionsData.remaining_amount
|
||||
: 0}
|
||||
$
|
||||
</Text>
|
||||
</Box>
|
||||
<Box
|
||||
display='flex'
|
||||
flexDirection='row'
|
||||
alignItems='center'
|
||||
justifyContent='space-between'
|
||||
>
|
||||
<Text variant='subheader'>Remaining Costs</Text>
|
||||
<Text variant='subheader' weight='bold'>
|
||||
{transactionsData?.transactions
|
||||
? transactionsData?.remaining_amount
|
||||
: project?.totalPrice}
|
||||
$
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
{selectedChunk !== undefined && (
|
||||
<Box
|
||||
background='#FFFFFF'
|
||||
padding='15px 20px'
|
||||
borderRadius='10px'
|
||||
boxShadow='1px 1px 10px 0px rgba(50, 59, 105, 0.25)'
|
||||
>
|
||||
<Box marginBottom='10px'>
|
||||
<Text variant='headline' weight='bold'>
|
||||
Pay with Credit Card
|
||||
</Text>
|
||||
</Box>
|
||||
<form onSubmit={paymentForm.handleSubmit}>
|
||||
<Box marginBottom='25px'>
|
||||
<Input
|
||||
name='number'
|
||||
label='Card Number'
|
||||
value={paymentForm.values.number}
|
||||
onChange={paymentForm.handleChange}
|
||||
onBlur={paymentForm.handleBlur}
|
||||
error={
|
||||
paymentForm.touched.number &&
|
||||
!!paymentForm.errors.number
|
||||
}
|
||||
errorMessage={paymentForm.errors.number}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='repeat(2, 1fr)'
|
||||
columnGap='20px'
|
||||
alignItems='center'
|
||||
marginBottom='25px'
|
||||
>
|
||||
<Input
|
||||
name='expMonth'
|
||||
label='Expiary Month'
|
||||
value={paymentForm.values.expMonth}
|
||||
onChange={paymentForm.handleChange}
|
||||
onBlur={paymentForm.handleBlur}
|
||||
error={
|
||||
paymentForm.touched.expMonth &&
|
||||
!!paymentForm.errors.expMonth
|
||||
}
|
||||
errorMessage={paymentForm.errors.expMonth}
|
||||
/>
|
||||
<Input
|
||||
name='expYear'
|
||||
label='Expiary Year'
|
||||
value={paymentForm.values.expYear}
|
||||
onChange={paymentForm.handleChange}
|
||||
onBlur={paymentForm.handleBlur}
|
||||
error={
|
||||
paymentForm.touched.expYear &&
|
||||
!!paymentForm.errors.expYear
|
||||
}
|
||||
errorMessage={paymentForm.errors.expYear}
|
||||
/>
|
||||
</Box>
|
||||
<Box marginBottom='25px'>
|
||||
<Input
|
||||
name='cvc'
|
||||
label='CVC'
|
||||
value={paymentForm.values.cvc}
|
||||
onChange={paymentForm.handleChange}
|
||||
onBlur={paymentForm.handleBlur}
|
||||
error={
|
||||
paymentForm.touched.cvc && !!paymentForm.errors.cvc
|
||||
}
|
||||
errorMessage={paymentForm.errors.cvc}
|
||||
/>
|
||||
</Box>
|
||||
<Button
|
||||
fullWidth
|
||||
color={role || 'client'}
|
||||
text='Pay'
|
||||
variant='primary-action'
|
||||
type='submit'
|
||||
loading={paymentLoading}
|
||||
/>
|
||||
</form>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Wrapper>
|
||||
) : (
|
||||
<Spinner fullScreen color={role} />
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{role === 'admin' && <Navigate to='/clients' />}
|
||||
{role === 'developer' ||
|
||||
(role === 'productOwner' && <Navigate to='/project' />)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Payments;
|
||||
@@ -1,8 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
.empty {
|
||||
fill: ${({ theme, color }) =>
|
||||
color ? theme.colors[color].main : theme.colors.client.main};
|
||||
}
|
||||
`;
|
||||
@@ -1,800 +0,0 @@
|
||||
import * as Yup from 'yup';
|
||||
import { useFormik } from 'formik';
|
||||
import { useReactToPrint } from 'react-to-print';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import { useLazyQuery, useMutation, useReactiveVar } from '@apollo/client';
|
||||
import { Navigate } from 'react-router';
|
||||
import { roleVar, userVar } from '../../graphql/state';
|
||||
import {
|
||||
Design,
|
||||
Empty,
|
||||
FullBuild,
|
||||
MVP,
|
||||
Settings,
|
||||
Specification,
|
||||
} from '../../assets';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
FeatureCard,
|
||||
Text,
|
||||
Spinner,
|
||||
Link,
|
||||
Specification as SpecificationPrint,
|
||||
Chip,
|
||||
Alert,
|
||||
Modal,
|
||||
Input,
|
||||
} from '../../components';
|
||||
import { Wrapper } from './styles';
|
||||
import {
|
||||
AddProjectDesignMutation,
|
||||
AddProjectDesignMutationVariables,
|
||||
AddProjectFullBuildMutation,
|
||||
AddProjectFullBuildMutationVariables,
|
||||
AddProjectMvpMutation,
|
||||
AddProjectMvpMutationVariables,
|
||||
ChangeProjectStateMutation,
|
||||
ChangeProjectStateMutationVariables,
|
||||
GetAllProjectsByClientIdQuery,
|
||||
GetAllProjectsByClientIdQueryVariables,
|
||||
GetAllProjectsQuery,
|
||||
GetAllUsersQueryVariables,
|
||||
GetProjectByIdQuery,
|
||||
GetProjectByIdQueryVariables,
|
||||
ProjectOutput,
|
||||
} from '../../graphql/types';
|
||||
import {
|
||||
ADD_PROJECT_DESIGN,
|
||||
ADD_PROJECT_FULL_BUILD,
|
||||
ADD_PROJECT_MVP,
|
||||
CHANGE_PROJECT_STATE,
|
||||
GET_ALL_PROJECTS,
|
||||
GET_ALL_PROJECTS_BY_CLIENT_ID,
|
||||
GET_PROJECT_BY_ID,
|
||||
} from '../../graphql/project.api';
|
||||
|
||||
const Project = () => {
|
||||
const role = useReactiveVar(roleVar);
|
||||
const currentUser = useReactiveVar(userVar);
|
||||
const navigate = useNavigate();
|
||||
const printRef = useRef<HTMLDivElement>(null);
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const [project, setProject] = useState<ProjectOutput>();
|
||||
const [error, setError] = useState<string>('');
|
||||
const [designModal, setDesignModal] = useState<boolean>(false);
|
||||
const [mvpModal, setMvpModal] = useState<boolean>(false);
|
||||
const [fullBuildModal, setFullBuildModal] = useState<boolean>(false);
|
||||
|
||||
const [
|
||||
getProjectsByClientId,
|
||||
{ loading: clientProjectsLoading, error: clientProjectsError },
|
||||
] = useLazyQuery<
|
||||
GetAllProjectsByClientIdQuery,
|
||||
GetAllProjectsByClientIdQueryVariables
|
||||
>(GET_ALL_PROJECTS_BY_CLIENT_ID, {
|
||||
variables: {
|
||||
id: currentUser?.id!,
|
||||
},
|
||||
onCompleted({ getAllProjectsByClientId }) {
|
||||
if (getAllProjectsByClientId.length > 0) {
|
||||
setProject(getAllProjectsByClientId[0]);
|
||||
navigate(`/project/${getAllProjectsByClientId[0]?.id}`, { replace: true })
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const [getProjects, { loading: projectsLoading, error: projectsError }] =
|
||||
useLazyQuery<GetAllProjectsQuery, GetAllUsersQueryVariables>(
|
||||
GET_ALL_PROJECTS,
|
||||
{
|
||||
onCompleted({ getAllProjects }) {
|
||||
if (getAllProjects.length > 0) setProject(getAllProjects[0]);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const [getProject, { loading: projectLoading, error: projectError }] =
|
||||
useLazyQuery<GetProjectByIdQuery, GetProjectByIdQueryVariables>(
|
||||
GET_PROJECT_BY_ID,
|
||||
{
|
||||
onCompleted({ getProjectById }) {
|
||||
setProject(getProjectById);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const [changeProjectState] = useMutation<
|
||||
ChangeProjectStateMutation,
|
||||
ChangeProjectStateMutationVariables
|
||||
>(CHANGE_PROJECT_STATE, {
|
||||
onCompleted({ changeProjectState: changedStateProject }) {
|
||||
setProject(changedStateProject);
|
||||
},
|
||||
onError({ graphQLErrors }) {
|
||||
setError(graphQLErrors[0].extensions?.info as string);
|
||||
setTimeout(() => setError(''), 3000);
|
||||
},
|
||||
});
|
||||
|
||||
const [addProjectDesign] = useMutation<
|
||||
AddProjectDesignMutation,
|
||||
AddProjectDesignMutationVariables
|
||||
>(ADD_PROJECT_DESIGN, {
|
||||
onCompleted({ addProjectDesign: projectWithDesign }) {
|
||||
setDesignModal(false);
|
||||
setProject(projectWithDesign);
|
||||
},
|
||||
onError({ graphQLErrors }) {
|
||||
setDesignModal(false);
|
||||
setError(graphQLErrors[0].extensions?.info as string);
|
||||
setTimeout(() => setError(''), 3000);
|
||||
},
|
||||
});
|
||||
|
||||
const [addProjectMvp] = useMutation<
|
||||
AddProjectMvpMutation,
|
||||
AddProjectMvpMutationVariables
|
||||
>(ADD_PROJECT_MVP, {
|
||||
onCompleted({ addProjectMvp: projectWithMvp }) {
|
||||
setMvpModal(false);
|
||||
setProject(projectWithMvp);
|
||||
},
|
||||
onError({ graphQLErrors }) {
|
||||
setMvpModal(false);
|
||||
setError(graphQLErrors[0].extensions?.info as string);
|
||||
setTimeout(() => setError(''), 3000);
|
||||
},
|
||||
});
|
||||
|
||||
const [addProjectFullBuild] = useMutation<
|
||||
AddProjectFullBuildMutation,
|
||||
AddProjectFullBuildMutationVariables
|
||||
>(ADD_PROJECT_FULL_BUILD, {
|
||||
onCompleted({ addProjectFullBuild: projectWithFullBuild }) {
|
||||
setFullBuildModal(false);
|
||||
setProject(projectWithFullBuild);
|
||||
},
|
||||
onError({ graphQLErrors }) {
|
||||
setFullBuildModal(false);
|
||||
setError(graphQLErrors[0].extensions?.info as string);
|
||||
setTimeout(() => setError(''), 3000);
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
getProject({ variables: { id } });
|
||||
} else if (role === 'client' && currentUser?.id) {
|
||||
getProjectsByClientId({
|
||||
variables: {
|
||||
id: currentUser?.id!,
|
||||
},
|
||||
});
|
||||
} else getProjects();
|
||||
|
||||
return () => {
|
||||
setProject(undefined);
|
||||
};
|
||||
}, [id, role]);
|
||||
|
||||
const handlePrint = useReactToPrint({
|
||||
content: () => printRef.current,
|
||||
});
|
||||
|
||||
const addDesignForm = useFormik({
|
||||
initialValues: {
|
||||
fileName: '',
|
||||
fileSource: '',
|
||||
},
|
||||
onSubmit: ({ fileName, fileSource }) => {
|
||||
addProjectDesign({
|
||||
variables: {
|
||||
design: {
|
||||
id: project?.id!,
|
||||
name: fileName,
|
||||
src: fileSource,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const addMvpForm = useFormik({
|
||||
initialValues: {
|
||||
fileName: '',
|
||||
fileSource: '',
|
||||
},
|
||||
onSubmit: ({ fileName, fileSource }) => {
|
||||
addProjectMvp({
|
||||
variables: {
|
||||
mvp: {
|
||||
id: project?.id!,
|
||||
name: fileName,
|
||||
src: fileSource,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const addFullBuildForm = useFormik({
|
||||
initialValues: {
|
||||
url: '',
|
||||
},
|
||||
validationSchema: Yup.object().shape({
|
||||
url: Yup.string().required('URL is required'),
|
||||
}),
|
||||
onSubmit: ({ url }) => {
|
||||
addProjectFullBuild({
|
||||
variables: {
|
||||
fullBuild: {
|
||||
id: project?.id!,
|
||||
url,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
if (role === 'admin') return <Navigate to='/clients' />;
|
||||
|
||||
if (clientProjectsLoading || projectsLoading || projectLoading)
|
||||
return <Spinner fullScreen color={role || 'client'} />;
|
||||
|
||||
if (clientProjectsError || projectsError || projectError || !project)
|
||||
return (
|
||||
<Wrapper color={role}>
|
||||
<Box
|
||||
width='100%'
|
||||
height='100vh'
|
||||
display='grid'
|
||||
alignItems='center'
|
||||
justifyContent='center'
|
||||
>
|
||||
<Box>
|
||||
<Empty />
|
||||
</Box>
|
||||
</Box>
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{designModal && (
|
||||
<Modal
|
||||
color={role || 'client'}
|
||||
title='Upload Design'
|
||||
description='Upload design file'
|
||||
onClose={() => setDesignModal(false)}
|
||||
onConfirm={addDesignForm.handleSubmit}
|
||||
>
|
||||
<Input
|
||||
type='file'
|
||||
label='File'
|
||||
file
|
||||
color={role || 'client'}
|
||||
onChange={async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const formData = new FormData();
|
||||
|
||||
if (event.target.files && event.target.files[0]) {
|
||||
formData.append('file', event.target.files[0]);
|
||||
formData.append('upload_preset', 'xofll5kc');
|
||||
|
||||
addDesignForm.setFieldValue('fileName', '');
|
||||
addDesignForm.setFieldValue('fileSource', '');
|
||||
|
||||
const data = await (
|
||||
await fetch(`${import.meta.env.VITE_CLOUDINARY_URL}`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
).json();
|
||||
|
||||
const filename = data.original_filename;
|
||||
const filesource = data.secure_url;
|
||||
|
||||
addDesignForm.setFieldValue('fileName', filename);
|
||||
addDesignForm.setFieldValue('fileSource', filesource);
|
||||
}
|
||||
}}
|
||||
error={
|
||||
addDesignForm.touched.fileName &&
|
||||
(!!addDesignForm.errors.fileName ||
|
||||
!!addDesignForm.errors.fileSource)
|
||||
}
|
||||
errorMessage={addDesignForm.errors.fileName}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
{mvpModal && (
|
||||
<Modal
|
||||
color={role || 'client'}
|
||||
title='Upload MVP'
|
||||
description='Upload mvp file'
|
||||
onClose={() => setMvpModal(false)}
|
||||
onConfirm={addMvpForm.handleSubmit}
|
||||
>
|
||||
<Input
|
||||
type='file'
|
||||
label='File'
|
||||
file
|
||||
color={role || 'client'}
|
||||
onChange={async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const formData = new FormData();
|
||||
|
||||
if (event.target.files && event.target.files[0]) {
|
||||
formData.append('file', event.target.files[0]);
|
||||
formData.append('upload_preset', 'xofll5kc');
|
||||
|
||||
addMvpForm.setFieldValue('fileName', '');
|
||||
addMvpForm.setFieldValue('fileSource', '');
|
||||
|
||||
const data = await (
|
||||
await fetch(`${import.meta.env.VITE_CLOUDINARY_URL}`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
).json();
|
||||
|
||||
const filename = data.original_filename;
|
||||
const filesource = data.secure_url;
|
||||
|
||||
addMvpForm.setFieldValue('fileName', filename);
|
||||
addMvpForm.setFieldValue('fileSource', filesource);
|
||||
}
|
||||
}}
|
||||
error={
|
||||
addMvpForm.touched.fileName &&
|
||||
(!!addMvpForm.errors.fileName || !!addMvpForm.errors.fileSource)
|
||||
}
|
||||
errorMessage={addMvpForm.errors.fileName}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
{fullBuildModal && (
|
||||
<Modal
|
||||
color={role || 'client'}
|
||||
title='Add full build'
|
||||
description='Add full build url'
|
||||
onClose={() => setMvpModal(false)}
|
||||
onConfirm={addFullBuildForm.handleSubmit}
|
||||
>
|
||||
<Input
|
||||
name='url'
|
||||
label='URL'
|
||||
color={role || 'client'}
|
||||
value={addFullBuildForm.values.url}
|
||||
onChange={addFullBuildForm.handleChange}
|
||||
onBlur={addFullBuildForm.handleBlur}
|
||||
error={
|
||||
addFullBuildForm.touched.url && !!addFullBuildForm.errors.url
|
||||
}
|
||||
errorMessage={addFullBuildForm.errors.url}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
<Wrapper>
|
||||
<Box padding='35px 45px 0px 120px'>
|
||||
<Box
|
||||
display='flex'
|
||||
flexDirection='row'
|
||||
alignItems='center'
|
||||
marginBottom='20px'
|
||||
>
|
||||
<Box
|
||||
flexGrow='1'
|
||||
display='flex'
|
||||
flexDirection='row'
|
||||
alignItems='center'
|
||||
>
|
||||
<Text variant='headline' weight='bold'>
|
||||
{project.name}
|
||||
</Text>
|
||||
</Box>
|
||||
{error && (
|
||||
<Box margin='0px 20px'>
|
||||
<Alert color='error' text={error} />
|
||||
</Box>
|
||||
)}
|
||||
{project.state === 'Approved' ? (
|
||||
<>
|
||||
<Box marginRight={role === 'client' ? '20px' : undefined}>
|
||||
<Button
|
||||
color={role || 'client'}
|
||||
variant='primary-action'
|
||||
text='Prototype'
|
||||
iconLeft={<Design />}
|
||||
disabled={!project.template.id}
|
||||
onClick={() =>
|
||||
navigate(`/prototype/${project.template.id}`)
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
{role === 'client' && (
|
||||
<Box>
|
||||
<Button
|
||||
color={role || 'client'}
|
||||
variant='primary-action'
|
||||
text='Settings'
|
||||
iconLeft={<Settings />}
|
||||
onClick={() => navigate(`/project-settings/${id}`)}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{project.state === 'OnReview' && role === 'productOwner' ? (
|
||||
<>
|
||||
<Box marginRight='20px'>
|
||||
<Button
|
||||
color={role || 'client'}
|
||||
variant='primary-action'
|
||||
text='Approve'
|
||||
onClick={() =>
|
||||
changeProjectState({
|
||||
variables: {
|
||||
id: project.id,
|
||||
state: 'Approved',
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<Button
|
||||
color={role || 'client'}
|
||||
variant='outlined'
|
||||
text='Decline'
|
||||
onClick={() =>
|
||||
changeProjectState({
|
||||
variables: {
|
||||
id: project.id,
|
||||
state: 'Declined',
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{project.state === 'OnReview' && (
|
||||
<Chip
|
||||
text={project.state}
|
||||
color='warning'
|
||||
variant='filled'
|
||||
/>
|
||||
)}
|
||||
{project.state === 'Declined' && (
|
||||
<Chip
|
||||
text={project.state}
|
||||
color='error'
|
||||
variant='filled'
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
{project.template.features && (
|
||||
<Box display='flex' flexDirection='column' marginBottom='30px'>
|
||||
<Box marginBottom='10px'>
|
||||
<Text variant='headline' gutterBottom>
|
||||
Features
|
||||
</Text>
|
||||
</Box>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='repeat(3, 1fr)'
|
||||
columnGap='40px'
|
||||
rowGap='45px'
|
||||
alignItems='stretch'
|
||||
>
|
||||
{project.template.features.map((feature) => (
|
||||
<FeatureCard feature={feature} key={feature.id} />
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
{project.delivrable && (
|
||||
<Box
|
||||
display='flex'
|
||||
flexDirection='column'
|
||||
marginBottom='30px'
|
||||
className='deliverables'
|
||||
>
|
||||
<Box marginBottom='10px'>
|
||||
<Text variant='headline' gutterBottom>
|
||||
Deliverables
|
||||
</Text>
|
||||
</Box>
|
||||
<Box
|
||||
display='flex'
|
||||
flexDirection='column'
|
||||
justifyContent='space-between'
|
||||
padding='35px 20px'
|
||||
boxShadow='1px 1px 10px rgba(50, 59, 105, 0.25)'
|
||||
borderRadius='10px'
|
||||
>
|
||||
<Box
|
||||
display='flex'
|
||||
flexDirection='row'
|
||||
alignItems='center'
|
||||
justifyContent='space-between'
|
||||
marginBottom='10px'
|
||||
>
|
||||
<Box display='flex' flexDirection='row' alignItems='center'>
|
||||
<Box marginRight='10px'>
|
||||
<Specification />
|
||||
</Box>
|
||||
<Text variant='title'>Specification</Text>
|
||||
</Box>
|
||||
|
||||
{project.state === 'Approved' ? (
|
||||
<Link href='#' color={role} onClick={handlePrint}>
|
||||
Print
|
||||
</Link>
|
||||
) : (
|
||||
<>
|
||||
{project.state === 'OnReview' && (
|
||||
<Text variant='body' color='warning'>
|
||||
{project.state}
|
||||
</Text>
|
||||
)}
|
||||
{project.state === 'Declined' && (
|
||||
<Text variant='body' color='error'>
|
||||
{project.state}
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
<Box
|
||||
display='flex'
|
||||
flexDirection='row'
|
||||
alignItems='center'
|
||||
justifyContent='space-between'
|
||||
marginBottom='10px'
|
||||
>
|
||||
<Box display='flex' flexDirection='row' alignItems='center'>
|
||||
<Box marginRight='10px'>
|
||||
<Design />
|
||||
</Box>
|
||||
<Text variant='title'>Design</Text>
|
||||
</Box>
|
||||
{project.delivrable.design.src ? (
|
||||
<Link
|
||||
url
|
||||
href={
|
||||
/http/.test(project.delivrable.design.src)
|
||||
? project.delivrable.design.src
|
||||
: `http://${project.delivrable.design.src}`
|
||||
}
|
||||
target='_blank'
|
||||
color={role}
|
||||
>
|
||||
Download
|
||||
</Link>
|
||||
) : role !== 'productOwner' ? (
|
||||
<>
|
||||
{project.state === 'OnReview' && (
|
||||
<Text variant='body' color='warning'>
|
||||
{project.state}
|
||||
</Text>
|
||||
)}
|
||||
{project.state === 'Declined' && (
|
||||
<Text variant='body' color='error'>
|
||||
{project.state}
|
||||
</Text>
|
||||
)}
|
||||
{project.state === 'Approved' && (
|
||||
<Text variant='body' color={role || 'client'}>
|
||||
In Progress
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{project.state === 'Approved' ? (
|
||||
<Box
|
||||
cursor='pointer'
|
||||
onClick={() => setDesignModal(true)}
|
||||
>
|
||||
<Text variant='body' color={role || 'client'}>
|
||||
Upload
|
||||
</Text>
|
||||
</Box>
|
||||
) : (
|
||||
<>
|
||||
{project.state === 'OnReview' && (
|
||||
<Text variant='body' color='warning'>
|
||||
{project.state}
|
||||
</Text>
|
||||
)}
|
||||
{project.state === 'Declined' && (
|
||||
<Text variant='body' color='error'>
|
||||
{project.state}
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
<Box
|
||||
display='flex'
|
||||
flexDirection='row'
|
||||
alignItems='center'
|
||||
justifyContent='space-between'
|
||||
marginBottom='10px'
|
||||
>
|
||||
<Box display='flex' flexDirection='row' alignItems='center'>
|
||||
<Box marginRight='10px'>
|
||||
<MVP />
|
||||
</Box>
|
||||
<Text variant='title'>MVP</Text>
|
||||
</Box>
|
||||
{project.delivrable.mvp.src ? (
|
||||
<Link
|
||||
url
|
||||
href={
|
||||
/http/.test(project.delivrable.mvp.src)
|
||||
? project.delivrable.mvp.src
|
||||
: `http://${project.delivrable.mvp.src}`
|
||||
}
|
||||
target='_blank'
|
||||
color={role}
|
||||
>
|
||||
Download
|
||||
</Link>
|
||||
) : role !== 'productOwner' ? (
|
||||
<>
|
||||
{project.state === 'OnReview' && (
|
||||
<Text variant='body' color='warning'>
|
||||
{project.state}
|
||||
</Text>
|
||||
)}
|
||||
{project.state === 'Declined' && (
|
||||
<Text variant='body' color='error'>
|
||||
{project.state}
|
||||
</Text>
|
||||
)}
|
||||
{project.state === 'Approved' && (
|
||||
<Text variant='body' color={role || 'client'}>
|
||||
In Progress
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{project.state === 'Approved' ? (
|
||||
<Box
|
||||
cursor='pointer'
|
||||
onClick={() => setDesignModal(true)}
|
||||
>
|
||||
<Text variant='body' color={role || 'client'}>
|
||||
Upload
|
||||
</Text>
|
||||
</Box>
|
||||
) : (
|
||||
<>
|
||||
{project.state === 'OnReview' && (
|
||||
<Text variant='body' color='warning'>
|
||||
{project.state}
|
||||
</Text>
|
||||
)}
|
||||
{project.state === 'Declined' && (
|
||||
<Text variant='body' color='error'>
|
||||
{project.state}
|
||||
</Text>
|
||||
)}
|
||||
{project.state === 'Approved' && (
|
||||
<Text variant='body' color={role || 'client'}>
|
||||
In Progress
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
<Box
|
||||
display='flex'
|
||||
flexDirection='row'
|
||||
alignItems='center'
|
||||
justifyContent='space-between'
|
||||
marginBottom='10px'
|
||||
>
|
||||
<Box display='flex' flexDirection='row' alignItems='center'>
|
||||
<Box marginRight='10px'>
|
||||
<FullBuild />
|
||||
</Box>
|
||||
<Text variant='title'>Full Build</Text>
|
||||
</Box>
|
||||
{project.delivrable.fullBuild !== '' ? (
|
||||
<Link
|
||||
url
|
||||
href={
|
||||
/http/.test(project?.delivrable?.fullBuild)
|
||||
? project?.delivrable?.fullBuild
|
||||
: `http://${project?.delivrable?.fullBuild}`
|
||||
}
|
||||
target='_blank'
|
||||
color={role}
|
||||
>
|
||||
Get
|
||||
</Link>
|
||||
) : role !== 'productOwner' ? (
|
||||
<>
|
||||
{project.state === 'OnReview' && (
|
||||
<Text variant='body' color='warning'>
|
||||
{project.state}
|
||||
</Text>
|
||||
)}
|
||||
{project.state === 'Declined' && (
|
||||
<Text variant='body' color='error'>
|
||||
{project.state}
|
||||
</Text>
|
||||
)}
|
||||
{project.state === 'Approved' && (
|
||||
<Text variant='body' color={role || 'client'}>
|
||||
In Progress
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{project.state === 'Approved' ? (
|
||||
<Box
|
||||
cursor='pointer'
|
||||
onClick={() => setDesignModal(true)}
|
||||
>
|
||||
<Text variant='body' color={role || 'client'}>
|
||||
Add
|
||||
</Text>
|
||||
</Box>
|
||||
) : (
|
||||
<>
|
||||
{project.state === 'OnReview' && (
|
||||
<Text variant='body' color='warning'>
|
||||
{project.state}
|
||||
</Text>
|
||||
)}
|
||||
{project.state === 'Declined' && (
|
||||
<Text variant='body' color='error'>
|
||||
{project.state}
|
||||
</Text>
|
||||
)}
|
||||
{project.state === 'Approved' && (
|
||||
<Text variant='body' color={role || 'client'}>
|
||||
In Progress
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
{project.template.specification && project.template.features && (
|
||||
<Box display='none'>
|
||||
<SpecificationPrint
|
||||
ref={printRef}
|
||||
specification={project.template.specification}
|
||||
features={project.template.features}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Wrapper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Project;
|
||||
@@ -1,18 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
type WrapperProps = {
|
||||
color?: 'client' | 'productOwner' | 'developer' | 'admin';
|
||||
};
|
||||
|
||||
export const Wrapper = styled.div<WrapperProps>`
|
||||
.empty {
|
||||
fill: ${({ theme, color }) =>
|
||||
color ? theme.colors[color].main : theme.colors.client.main};
|
||||
}
|
||||
|
||||
.deliverables {
|
||||
svg path {
|
||||
stroke: black;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -1,355 +0,0 @@
|
||||
import { useEffect, useState, useRef, useCallback, useMemo } from 'react';
|
||||
import ReactFlow, {
|
||||
addEdge,
|
||||
MiniMap,
|
||||
Controls,
|
||||
ControlButton,
|
||||
Connection,
|
||||
Edge,
|
||||
MarkerType,
|
||||
useEdgesState,
|
||||
useNodesState
|
||||
} from 'reactflow';
|
||||
import 'reactflow/dist/style.css';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useLazyQuery, useMutation, useReactiveVar } from '@apollo/client';
|
||||
import { Navigate } from 'react-router';
|
||||
import { roleVar } from '../../graphql/state';
|
||||
import { Empty, ArrowLeft, Edit, Close, CheckCircle } from '../../assets';
|
||||
import {
|
||||
Box,
|
||||
Text,
|
||||
Button,
|
||||
Spinner,
|
||||
FrontendFeatureCard,
|
||||
BackendFeatureCard,
|
||||
Alert,
|
||||
} from '../../components';
|
||||
import { Wrapper } from './styles';
|
||||
import {
|
||||
AddPrototypeMutation,
|
||||
AddPrototypeMutationVariables,
|
||||
FeatureOutput,
|
||||
GetPrototypeByIdQuery,
|
||||
GetPrototypeByIdQueryVariables,
|
||||
GetTemplateByIdQuery,
|
||||
GetTemplateByIdQueryVariables,
|
||||
ProtoTypeOutput,
|
||||
TemplateOutput,
|
||||
UpdatePrototypeMutation,
|
||||
UpdatePrototypeMutationVariables,
|
||||
} from '../../graphql/types';
|
||||
import { GET_TEMPLATE_BY_ID } from '../../graphql/template.api';
|
||||
import {
|
||||
ADD_PROTOTYPE,
|
||||
GET_PROTOTYPE_BY_ID,
|
||||
UPDATE_PROTOTYPE,
|
||||
} from '../../graphql/prototype.api';
|
||||
|
||||
const Prototype = () => {
|
||||
const role = useReactiveVar(roleVar);
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const nodeTypes = useMemo(() => ({ featureCard: FrontendFeatureCard }), []);
|
||||
const [template, setTemplate] = useState<TemplateOutput>();
|
||||
const [prototype, setPrototype] = useState<Array<ProtoTypeOutput>>();
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState<FeatureOutput>([]);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||
const [editing, setEditing] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string>('');
|
||||
const [success, setSuccess] = useState<boolean>(false);
|
||||
const diagramParentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [getTemplate, { loading: templateLoading, error: templateError }] = useLazyQuery<
|
||||
GetTemplateByIdQuery,
|
||||
GetTemplateByIdQueryVariables
|
||||
>(GET_TEMPLATE_BY_ID, {
|
||||
onCompleted({ getTemplateById }) {
|
||||
setTemplate(getTemplateById);
|
||||
},
|
||||
});
|
||||
|
||||
const [getPrototype, { loading: prototypeLoading }] = useLazyQuery<
|
||||
GetPrototypeByIdQuery,
|
||||
GetPrototypeByIdQueryVariables
|
||||
>(GET_PROTOTYPE_BY_ID, {
|
||||
onCompleted({ getPrototypeById }) {
|
||||
setPrototype(getPrototypeById.prototype);
|
||||
},
|
||||
});
|
||||
|
||||
const [addPrototype] = useMutation<
|
||||
AddPrototypeMutation,
|
||||
AddPrototypeMutationVariables
|
||||
>(ADD_PROTOTYPE, {
|
||||
onCompleted({ addPrototype: addPrototypeResult }) {
|
||||
setPrototype(addPrototypeResult.prototype);
|
||||
setSuccess(true);
|
||||
setTimeout(() => setSuccess(false), 3000);
|
||||
},
|
||||
onError({ graphQLErrors }) {
|
||||
setError(graphQLErrors[0]?.extensions?.info as string);
|
||||
setTimeout(() => setError(''), 3000);
|
||||
},
|
||||
});
|
||||
|
||||
const [updatePrototype] = useMutation<
|
||||
UpdatePrototypeMutation,
|
||||
UpdatePrototypeMutationVariables
|
||||
>(UPDATE_PROTOTYPE, {
|
||||
onCompleted({ updatePrototype: updatePrototypeResult }) {
|
||||
setPrototype(updatePrototypeResult.prototype);
|
||||
setSuccess(true);
|
||||
setTimeout(() => setSuccess(false), 3000);
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
getTemplate({ variables: { id } });
|
||||
getPrototype({ variables: { id } });
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (template && template.features) {
|
||||
const initialNodes = template?.features?.map((feature, index) => ({
|
||||
id: feature.id,
|
||||
type: 'featureCard',
|
||||
data: feature,
|
||||
position: { x: index * 100, y: index * 200 },
|
||||
style: {
|
||||
width: 'auto',
|
||||
},
|
||||
connectable: role === 'developer' && editing
|
||||
}));
|
||||
|
||||
if (initialNodes) setNodes(initialNodes);
|
||||
}
|
||||
|
||||
if (prototype) {
|
||||
const initialEdges: Array<Edge> = [];
|
||||
|
||||
prototype.forEach((link) => {
|
||||
link.connections.forEach((connection) => {
|
||||
initialEdges.push({
|
||||
id: `edge-${link.feature.id}`,
|
||||
source: link.feature.id,
|
||||
target: connection.to,
|
||||
markerEnd: MarkerType.Arrow,
|
||||
className: 'normal-edge'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
if (initialEdges) setEdges(initialEdges);
|
||||
}
|
||||
}, [template, prototype, editing]);
|
||||
|
||||
const onConnect = useCallback(
|
||||
(params: Edge | Connection) => setEdges((els) => addEdge(params, els)),
|
||||
[setEdges]
|
||||
);
|
||||
|
||||
const handleEditPrototype = () => {
|
||||
if (editing) {
|
||||
const prototypeInput = edges
|
||||
.map((edge) => ({
|
||||
featureId: edge.source,
|
||||
connections: [
|
||||
{
|
||||
to: edge.target,
|
||||
releations: { back: false, forword: true },
|
||||
},
|
||||
],
|
||||
}));
|
||||
if (prototypeInput && prototypeInput.length > 0) {
|
||||
if (prototype) {
|
||||
updatePrototype({
|
||||
variables: {
|
||||
prototype: {
|
||||
templateId: id as string,
|
||||
prototype: prototypeInput,
|
||||
},
|
||||
},
|
||||
});
|
||||
} else {
|
||||
addPrototype({
|
||||
variables: {
|
||||
prototype: {
|
||||
templateId: id as string,
|
||||
prototype: prototypeInput,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setEditing(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (role === 'admin') return (
|
||||
<Navigate to='/clients' />
|
||||
)
|
||||
|
||||
if (templateLoading || prototypeLoading) return (
|
||||
<Spinner fullScreen color={role || 'client'} />
|
||||
);
|
||||
|
||||
if (templateError || !template) return (
|
||||
<Wrapper color={role}>
|
||||
<Box
|
||||
width='100%'
|
||||
height='100vh'
|
||||
display='grid'
|
||||
alignItems='center'
|
||||
justifyContent='center'
|
||||
>
|
||||
<Box>
|
||||
<Empty />
|
||||
</Box>
|
||||
</Box>
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
return (
|
||||
<Wrapper color={role}>
|
||||
<Box padding='35px 45px 0px 120px'>
|
||||
<Box
|
||||
display='flex'
|
||||
flexDirection='row'
|
||||
alignItems='center'
|
||||
marginBottom='20px'
|
||||
>
|
||||
<Box marginRight='50px'>
|
||||
<Button
|
||||
text='Back'
|
||||
color={role || 'client'}
|
||||
size='small'
|
||||
onClick={() => navigate(-1)}
|
||||
iconLeft={<ArrowLeft />}
|
||||
/>
|
||||
<Text variant='headline' weight='bold'>
|
||||
Prototype
|
||||
</Text>
|
||||
</Box>
|
||||
{success && (
|
||||
<Alert
|
||||
color='success'
|
||||
text='Prototype updated successfully'
|
||||
/>
|
||||
)}
|
||||
{error && <Alert color='error' text={error} />}
|
||||
</Box>
|
||||
{template.features && (
|
||||
<>
|
||||
<Box
|
||||
display='flex'
|
||||
flexDirection='column'
|
||||
marginBottom='30px'
|
||||
>
|
||||
<Box marginBottom='10px'>
|
||||
<Text variant='headline' gutterBottom>
|
||||
Frontend Features
|
||||
</Text>
|
||||
</Box>
|
||||
<Box
|
||||
display='grid'
|
||||
background='#F9FAFA'
|
||||
boxShadow='1px 1px 10px rgba(50, 59, 105, 0.25)'
|
||||
borderRadius='10px'
|
||||
width='100%'
|
||||
height='400px'
|
||||
ref={diagramParentRef}
|
||||
>
|
||||
<Box
|
||||
width={
|
||||
diagramParentRef.current
|
||||
? `${
|
||||
getComputedStyle(diagramParentRef.current)
|
||||
?.width
|
||||
}}px`
|
||||
: '100%'
|
||||
}
|
||||
height='auto'
|
||||
>
|
||||
<ReactFlow
|
||||
fitView
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
nodeTypes={nodeTypes}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
>
|
||||
{role === 'developer' && (
|
||||
<>
|
||||
<MiniMap />
|
||||
<Controls
|
||||
showInteractive={false}
|
||||
showFitView
|
||||
>
|
||||
{!editing && (
|
||||
<ControlButton onClick={() => setEditing(true)}>
|
||||
<Edit />
|
||||
</ControlButton>
|
||||
)}
|
||||
{editing && (
|
||||
<>
|
||||
<ControlButton onClick={() => setEditing(false)}>
|
||||
<Close />
|
||||
</ControlButton>
|
||||
<ControlButton onClick={handleEditPrototype}>
|
||||
<CheckCircle />
|
||||
</ControlButton>
|
||||
</>
|
||||
)}
|
||||
</Controls>
|
||||
</>
|
||||
)}
|
||||
</ReactFlow>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
display='flex'
|
||||
flexDirection='column'
|
||||
marginBottom='30px'
|
||||
>
|
||||
<Box marginBottom='10px'>
|
||||
<Text variant='headline' gutterBottom>
|
||||
Backend Features
|
||||
</Text>
|
||||
</Box>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='repeat(3, 1fr)'
|
||||
gap='20px'
|
||||
alignItems='stretch'
|
||||
justifyContent='center'
|
||||
>
|
||||
{template.features.map((feature) => {
|
||||
if (
|
||||
feature.featureType === 'backend' ||
|
||||
feature.featureType === 'fullstack'
|
||||
) {
|
||||
return (
|
||||
<BackendFeatureCard
|
||||
feature={feature}
|
||||
key={feature.id}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Prototype;
|
||||
@@ -1,12 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
type WrapperProps = {
|
||||
color?: 'client' | 'productOwner' | 'developer' | 'admin';
|
||||
};
|
||||
|
||||
export const Wrapper = styled.div<WrapperProps>`
|
||||
.empty {
|
||||
fill: ${({ theme, color }) =>
|
||||
color ? theme.colors[color].main : theme.colors.client.main};
|
||||
}
|
||||
`;
|
||||
@@ -1,548 +0,0 @@
|
||||
import * as Yup from 'yup';
|
||||
import { useFormik } from 'formik';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { useMutation, useQuery, useReactiveVar } from '@apollo/client';
|
||||
import { useState } from 'react';
|
||||
import { roleVar, tokenVar, userVar } from '../../graphql/state';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Text,
|
||||
SectionSelector,
|
||||
Input,
|
||||
Select,
|
||||
Alert,
|
||||
Spinner,
|
||||
Modal,
|
||||
} from '../../components';
|
||||
import { Wrapper } from './styles';
|
||||
import { ArrowLeft, Profile, Security } from '../../assets';
|
||||
import {
|
||||
UpdateUserInfoMutation,
|
||||
UpdateUserPasswordMutation,
|
||||
UpdateUserInfoMutationVariables,
|
||||
UpdateUserPasswordMutationVariables,
|
||||
GetCountryCodesQuery,
|
||||
GetCountryCodesQueryVariables,
|
||||
DeleteUserMutation,
|
||||
DeleteUserMutationVariables,
|
||||
} from '../../graphql/types';
|
||||
import {
|
||||
DELETE_USER,
|
||||
GET_COUNTRY_CODES,
|
||||
UPDATE_USER_INFO,
|
||||
UPDATE_USER_PASSWORD,
|
||||
} from '../../graphql/auth.api';
|
||||
|
||||
const Settings = () => {
|
||||
const navigate = useNavigate();
|
||||
const role = useReactiveVar(roleVar);
|
||||
const currentUser = useReactiveVar(userVar);
|
||||
const { data: countryCodes, loading: countryCodesLoading } = useQuery<
|
||||
GetCountryCodesQuery,
|
||||
GetCountryCodesQueryVariables
|
||||
>(GET_COUNTRY_CODES);
|
||||
|
||||
const [selectedSection, setSelectedSection] = useState<
|
||||
'general' | 'security'
|
||||
>('general');
|
||||
const [error, setError] = useState<string>('');
|
||||
const [success, setSuccess] = useState<boolean>(false);
|
||||
const [deleteAccountModal, setDeleteAccountModal] = useState<boolean>(false);
|
||||
|
||||
const [updateUserInfo, { loading: generalLoading }] = useMutation<
|
||||
UpdateUserInfoMutation,
|
||||
UpdateUserInfoMutationVariables
|
||||
>(UPDATE_USER_INFO, {
|
||||
onCompleted({ updateUserInfo: user }) {
|
||||
userVar(user);
|
||||
generalForm.setFieldValue('firstName', user.firstName);
|
||||
generalForm.setFieldValue('lastName', user.lastName);
|
||||
generalForm.setFieldValue('prefix', user.phone.prefix);
|
||||
generalForm.setFieldValue('number', user.phone.number);
|
||||
generalForm.setFieldValue('place', user.address.place);
|
||||
generalForm.setFieldValue('city', user.address.city);
|
||||
generalForm.setFieldValue('zip', user.address.zip);
|
||||
generalForm.setFieldValue('country', user.address.country);
|
||||
setSuccess(true);
|
||||
setTimeout(() => setSuccess(false), 3000);
|
||||
},
|
||||
onError({ graphQLErrors }) {
|
||||
setError(graphQLErrors[0]?.extensions?.info as string);
|
||||
setTimeout(() => setError(''), 3000);
|
||||
},
|
||||
});
|
||||
|
||||
const generalForm = useFormik({
|
||||
initialValues: {
|
||||
firstName: currentUser?.firstName || '',
|
||||
lastName: currentUser?.lastName || '',
|
||||
prefix: currentUser?.phone.prefix || '',
|
||||
number: currentUser?.phone.number || '',
|
||||
place: currentUser?.address.place || '',
|
||||
city: currentUser?.address.city || '',
|
||||
zip: currentUser?.address.zip || '',
|
||||
country: currentUser?.address.country || '',
|
||||
},
|
||||
validationSchema: Yup.object().shape({
|
||||
firstName: Yup.string().required('First Name is required'),
|
||||
lastName: Yup.string().required('Last Name is required'),
|
||||
prefix: Yup.string().required('Prefix is required'),
|
||||
// prettier-ignore
|
||||
number: Yup.number().typeError('Phone must be a number').required('Phone is required'),
|
||||
place: Yup.string().required('Address is required'),
|
||||
city: Yup.string().required('City is required'),
|
||||
country: Yup.string().required('Country is required'),
|
||||
// prettier-ignore
|
||||
zip: Yup.number().typeError('Zip must be a number').required('Zip is required'),
|
||||
}),
|
||||
onSubmit: ({
|
||||
firstName,
|
||||
lastName,
|
||||
prefix,
|
||||
number,
|
||||
place,
|
||||
city,
|
||||
country,
|
||||
zip,
|
||||
}) =>
|
||||
updateUserInfo({
|
||||
variables: {
|
||||
user: {
|
||||
id: currentUser?.id!,
|
||||
email: currentUser?.email!,
|
||||
firstName,
|
||||
lastName,
|
||||
phone: { prefix, number },
|
||||
address: { place, city, country, zip },
|
||||
role: currentUser?.role!,
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const [updateUserPassword, { loading: securityLoading }] = useMutation<
|
||||
UpdateUserPasswordMutation,
|
||||
UpdateUserPasswordMutationVariables
|
||||
>(UPDATE_USER_PASSWORD, {
|
||||
onCompleted({ updateUserPassword: user }) {
|
||||
userVar(user);
|
||||
setSuccess(true);
|
||||
setTimeout(() => setSuccess(false), 3000);
|
||||
},
|
||||
onError({ graphQLErrors }) {
|
||||
setError(graphQLErrors[0]?.extensions?.info as string);
|
||||
setTimeout(() => setError(''), 3000);
|
||||
},
|
||||
});
|
||||
|
||||
const securityForm = useFormik({
|
||||
initialValues: {
|
||||
oldPassword: '',
|
||||
newPassword: '',
|
||||
confirmNewPassword: '',
|
||||
},
|
||||
validationSchema: Yup.object().shape({
|
||||
oldPassword: Yup.string()
|
||||
.required('Old password is required')
|
||||
.min(6, 'Old password is 6 characters minimum'),
|
||||
newPassword: Yup.string()
|
||||
.required('New password is required')
|
||||
.notOneOf(
|
||||
[Yup.ref('oldPassword')],
|
||||
'New password should not be old password'
|
||||
)
|
||||
.required('New password is required')
|
||||
.min(6, 'New password is 6 characters minimum'),
|
||||
confirmNewPassword: Yup.string()
|
||||
.required('Confirm new password is required')
|
||||
.oneOf(
|
||||
[Yup.ref('newPassword')],
|
||||
"Confirm new password doesn't match with new password"
|
||||
),
|
||||
}),
|
||||
onSubmit: ({ oldPassword, newPassword }) =>
|
||||
updateUserPassword({
|
||||
variables: {
|
||||
id: currentUser?.id!,
|
||||
password: { oldPassword, newPassword },
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const [deleteUser] = useMutation<
|
||||
DeleteUserMutation,
|
||||
DeleteUserMutationVariables
|
||||
>(DELETE_USER, {
|
||||
onCompleted() {
|
||||
localStorage.removeItem('token');
|
||||
tokenVar(undefined);
|
||||
userVar(undefined);
|
||||
roleVar(undefined);
|
||||
setDeleteAccountModal(false);
|
||||
navigate('/signup');
|
||||
},
|
||||
onError({ graphQLErrors }) {
|
||||
setDeleteAccountModal(false);
|
||||
setError(graphQLErrors[0]?.extensions?.info as string);
|
||||
setTimeout(() => setError(''), 3000);
|
||||
},
|
||||
});
|
||||
|
||||
const deleteAccountForm = useFormik({
|
||||
initialValues: {
|
||||
password: '',
|
||||
},
|
||||
validationSchema: Yup.object().shape({
|
||||
password: Yup.string()
|
||||
.required('Password is required')
|
||||
.min(6, 'Password is 6 characters minimum'),
|
||||
}),
|
||||
onSubmit: ({ password }, { resetForm }) => {
|
||||
try {
|
||||
deleteUser({ variables: { id: currentUser?.id!, password } });
|
||||
} finally {
|
||||
resetForm();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Box>
|
||||
<Button
|
||||
text='Back'
|
||||
color={role || 'client'}
|
||||
size='small'
|
||||
onClick={() => navigate(-1)}
|
||||
iconLeft={<ArrowLeft />}
|
||||
/>
|
||||
<Text variant='headline' weight='bold'>
|
||||
Settings
|
||||
</Text>
|
||||
</Box>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='0.5fr 2fr'
|
||||
columnGap='25px'
|
||||
marginTop='1rem'
|
||||
>
|
||||
<Box display='grid' rowGap='0.5rem' gridTemplateRows='50px'>
|
||||
<SectionSelector
|
||||
icon={<Profile />}
|
||||
color={role || 'client'}
|
||||
text='General'
|
||||
selected={selectedSection === 'general'}
|
||||
onClick={() => setSelectedSection('general')}
|
||||
/>
|
||||
<SectionSelector
|
||||
icon={<Security />}
|
||||
color={role || 'client'}
|
||||
text='Security'
|
||||
selected={selectedSection === 'security'}
|
||||
onClick={() => setSelectedSection('security')}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
background='white'
|
||||
boxShadow='1px 1px 10px 0px rgba(50, 59, 105, 0.25)'
|
||||
borderRadius='10px'
|
||||
width='100%'
|
||||
padding='30px'
|
||||
>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='auto 1fr'
|
||||
columnGap='1rem'
|
||||
alignItems='center'
|
||||
marginBottom='50px'
|
||||
>
|
||||
<Text variant='subheader' weight='bold'>
|
||||
{selectedSection === 'general' ? 'General' : 'Security'}
|
||||
</Text>
|
||||
{error && <Alert color='error' text={error} />}
|
||||
{success && (
|
||||
<Alert color='success' text='Account updated successfully' />
|
||||
)}
|
||||
</Box>
|
||||
{selectedSection === 'general' && (
|
||||
<>
|
||||
{!countryCodesLoading ? (
|
||||
<form onSubmit={generalForm.handleSubmit}>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='auto'
|
||||
rowGap='0.5rem'
|
||||
position='relative'
|
||||
>
|
||||
<Input
|
||||
name='firstName'
|
||||
label='First Name'
|
||||
color={role || 'client'}
|
||||
value={generalForm.values.firstName}
|
||||
onChange={generalForm.handleChange}
|
||||
onBlur={generalForm.handleBlur}
|
||||
error={
|
||||
generalForm.touched.firstName &&
|
||||
!!generalForm.errors.firstName
|
||||
}
|
||||
errorMessage={generalForm.errors.firstName}
|
||||
/>
|
||||
<Input
|
||||
name='lastName'
|
||||
label='Last Name'
|
||||
color={role || 'client'}
|
||||
value={generalForm.values.lastName}
|
||||
onChange={generalForm.handleChange}
|
||||
onBlur={generalForm.handleBlur}
|
||||
error={
|
||||
generalForm.touched.lastName &&
|
||||
!!generalForm.errors.lastName
|
||||
}
|
||||
errorMessage={generalForm.errors.lastName}
|
||||
/>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='1fr 1.5fr'
|
||||
columnGap='10px'
|
||||
>
|
||||
<Select
|
||||
name='prefix'
|
||||
label='Country Code'
|
||||
color={role || 'client'}
|
||||
options={
|
||||
countryCodes?.getCountryCode
|
||||
? countryCodes.getCountryCode.map(
|
||||
({ prefix, country }) => ({
|
||||
value: prefix,
|
||||
label: `+${prefix} (${country})`,
|
||||
})
|
||||
)
|
||||
: [{ value: '216', label: '+216' }]
|
||||
}
|
||||
onChange={generalForm.handleChange}
|
||||
onBlur={generalForm.handleBlur}
|
||||
value={generalForm.values.prefix}
|
||||
select={generalForm.values.prefix}
|
||||
error={
|
||||
generalForm.touched.prefix &&
|
||||
!!generalForm.errors.prefix
|
||||
}
|
||||
errorMessage={generalForm.errors.prefix}
|
||||
/>
|
||||
<Input
|
||||
name='number'
|
||||
type='tel'
|
||||
label='Phone'
|
||||
color={role || 'client'}
|
||||
onChange={generalForm.handleChange}
|
||||
onBlur={generalForm.handleBlur}
|
||||
value={generalForm.values.number}
|
||||
error={
|
||||
generalForm.touched.number &&
|
||||
!!generalForm.errors.number
|
||||
}
|
||||
errorMessage={generalForm.errors.number}
|
||||
/>
|
||||
</Box>
|
||||
<Input
|
||||
name='place'
|
||||
label='Address'
|
||||
color={role || 'client'}
|
||||
onChange={generalForm.handleChange}
|
||||
onBlur={generalForm.handleBlur}
|
||||
value={generalForm.values.place}
|
||||
error={
|
||||
generalForm.touched.place && !!generalForm.errors.place
|
||||
}
|
||||
errorMessage={generalForm.errors.place}
|
||||
/>
|
||||
<Input
|
||||
name='city'
|
||||
label='City'
|
||||
color={role || 'client'}
|
||||
onChange={generalForm.handleChange}
|
||||
onBlur={generalForm.handleBlur}
|
||||
value={generalForm.values.city}
|
||||
error={
|
||||
generalForm.touched.city && !!generalForm.errors.city
|
||||
}
|
||||
errorMessage={generalForm.errors.city}
|
||||
/>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='2fr 1fr'
|
||||
columnGap='10px'
|
||||
>
|
||||
<Select
|
||||
name='country'
|
||||
label='Country'
|
||||
color={role || 'client'}
|
||||
options={
|
||||
countryCodes?.getCountryCode
|
||||
? countryCodes.getCountryCode.map(
|
||||
({ country }) => ({
|
||||
value: country,
|
||||
label: country,
|
||||
})
|
||||
)
|
||||
: [{ value: 'Tunisia', label: 'Tunisia' }]
|
||||
}
|
||||
onChange={generalForm.handleChange}
|
||||
onBlur={generalForm.handleBlur}
|
||||
value={generalForm.values.country}
|
||||
select={generalForm.values.country}
|
||||
error={
|
||||
generalForm.touched.country &&
|
||||
!!generalForm.errors.country
|
||||
}
|
||||
errorMessage={generalForm.errors.country}
|
||||
/>
|
||||
<Input
|
||||
name='zip'
|
||||
label='Zip Code'
|
||||
color={role || 'client'}
|
||||
onChange={generalForm.handleChange}
|
||||
onBlur={generalForm.handleBlur}
|
||||
value={generalForm.values.zip}
|
||||
error={
|
||||
generalForm.touched.zip && !!generalForm.errors.zip
|
||||
}
|
||||
errorMessage={generalForm.errors.zip}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
marginTop='0.5rem'
|
||||
display='grid'
|
||||
gridTemplateColumns='repeat(2, auto)'
|
||||
justifyContent='flex-end'
|
||||
>
|
||||
<Button
|
||||
variant='primary-action'
|
||||
color={role || 'client'}
|
||||
text='Save'
|
||||
type='submit'
|
||||
loading={generalLoading}
|
||||
disabled={generalLoading}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</form>
|
||||
) : (
|
||||
<Box display='grid' alignItems='center' justifyContent='center'>
|
||||
<Spinner color={role || 'client'} />
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{selectedSection === 'security' && (
|
||||
<>
|
||||
{deleteAccountModal && (
|
||||
<Modal
|
||||
color={role || 'client'}
|
||||
title='Delete Account'
|
||||
description='Enter password to confirm account deletion.
|
||||
If you delete your account you cannot recover any of your projects.'
|
||||
onClose={() => setDeleteAccountModal(false)}
|
||||
onConfirm={deleteAccountForm.handleSubmit}
|
||||
>
|
||||
<Input
|
||||
type='password'
|
||||
placeholder='Password'
|
||||
name='password'
|
||||
value={deleteAccountForm.values.password}
|
||||
onChange={deleteAccountForm.handleChange}
|
||||
onBlur={deleteAccountForm.handleBlur}
|
||||
color={role || 'client'}
|
||||
error={
|
||||
deleteAccountForm.touched.password &&
|
||||
!!deleteAccountForm.errors.password
|
||||
}
|
||||
errorMessage={deleteAccountForm.errors.password}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
<form onSubmit={securityForm.handleSubmit}>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='auto'
|
||||
rowGap='0.5rem'
|
||||
position='relative'
|
||||
>
|
||||
<Input
|
||||
name='oldPassword'
|
||||
label='Old Password'
|
||||
color={role || 'client'}
|
||||
type='password'
|
||||
value={securityForm.values.oldPassword}
|
||||
onChange={securityForm.handleChange}
|
||||
onBlur={securityForm.handleBlur}
|
||||
error={
|
||||
securityForm.touched.oldPassword &&
|
||||
!!securityForm.errors.oldPassword
|
||||
}
|
||||
errorMessage={securityForm.errors.oldPassword}
|
||||
/>
|
||||
<Input
|
||||
name='newPassword'
|
||||
label='New Password'
|
||||
color={role || 'client'}
|
||||
type='password'
|
||||
value={securityForm.values.newPassword}
|
||||
onChange={securityForm.handleChange}
|
||||
onBlur={securityForm.handleBlur}
|
||||
error={
|
||||
securityForm.touched.newPassword &&
|
||||
!!securityForm.errors.newPassword
|
||||
}
|
||||
errorMessage={securityForm.errors.newPassword}
|
||||
/>
|
||||
<Input
|
||||
name='confirmNewPassword'
|
||||
label='Confirm New Password'
|
||||
color={role || 'client'}
|
||||
type='password'
|
||||
value={securityForm.values.confirmNewPassword}
|
||||
onChange={securityForm.handleChange}
|
||||
onBlur={securityForm.handleBlur}
|
||||
error={
|
||||
securityForm.touched.confirmNewPassword &&
|
||||
!!securityForm.errors.confirmNewPassword
|
||||
}
|
||||
errorMessage={securityForm.errors.confirmNewPassword}
|
||||
/>
|
||||
<Box
|
||||
marginTop='0.5rem'
|
||||
display='flex'
|
||||
justifyContent={
|
||||
role === 'client' ? 'space-between' : 'flex-end'
|
||||
}
|
||||
>
|
||||
{role === 'client' && (
|
||||
<Button
|
||||
variant='text'
|
||||
color='error'
|
||||
text='Delete Account'
|
||||
onClick={() => setDeleteAccountModal(true)}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
variant='primary-action'
|
||||
color={role || 'client'}
|
||||
text='Save'
|
||||
type='submit'
|
||||
loading={securityLoading}
|
||||
disabled={securityLoading}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
@@ -1,5 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
padding: 35px 45px 35px 120px;
|
||||
`;
|
||||
@@ -1,329 +0,0 @@
|
||||
import * as Yup from 'yup';
|
||||
import { useFormik } from 'formik';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { useReactiveVar } from '@apollo/client';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { roleVar, userVar } from '../../graphql/state';
|
||||
import { Wrapper } from './styles';
|
||||
import {
|
||||
CreateThreadMutation,
|
||||
CreateThreadMutationVariables,
|
||||
GetThreadByIdQuery,
|
||||
GetThreadByIdQueryVariables,
|
||||
MessagesQuery,
|
||||
MessagesQueryVariables,
|
||||
MessagesSubscription,
|
||||
SendMsgMutation,
|
||||
SendMsgMutationVariables,
|
||||
Support as SupportType,
|
||||
UserMessages,
|
||||
} from '../../graphql/types.support';
|
||||
import { Box, Button, Input, TextArea, Text } from '../../components';
|
||||
import { Send, ThreadClient, ThreadProductOwner } from '../../assets';
|
||||
import {
|
||||
CREATE_THREAD,
|
||||
GET_THREAD_BY_ID,
|
||||
MESSAGES,
|
||||
MESSAGES_SUBSCRIPTION,
|
||||
SEND_MSG,
|
||||
} from '../../graphql/chat.api.support';
|
||||
import { theme } from '../../themes';
|
||||
import { clientSupport } from '../..';
|
||||
|
||||
const Support = () => {
|
||||
const { projectId, threadId } = useParams<{ projectId: string, threadId: string }>();
|
||||
const role = useReactiveVar(roleVar);
|
||||
const currentUser = useReactiveVar(userVar);
|
||||
const navigate = useNavigate();
|
||||
const [thread, setThread] = useState<SupportType>();
|
||||
const [messages, setMessages] = useState<Array<UserMessages>>([]);
|
||||
const [addedMessages, setAddedMessages] = useState<Array<UserMessages>>([]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (threadId) {
|
||||
const threadResult = await clientSupport.query<
|
||||
GetThreadByIdQuery,
|
||||
GetThreadByIdQueryVariables
|
||||
>({
|
||||
query: GET_THREAD_BY_ID,
|
||||
variables: {
|
||||
threadId: threadId!,
|
||||
},
|
||||
});
|
||||
|
||||
setThread(threadResult?.data?.thread);
|
||||
|
||||
const messagesResult = await clientSupport.query<
|
||||
MessagesQuery,
|
||||
MessagesQueryVariables
|
||||
>({
|
||||
query: MESSAGES,
|
||||
variables: {
|
||||
threadId: threadId!,
|
||||
}
|
||||
});
|
||||
setMessages(
|
||||
Array.from(messagesResult?.data?.messages).map(
|
||||
(message: UserMessages) => ({
|
||||
text: message.text,
|
||||
username: message.username,
|
||||
id: message.id,
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
clientSupport.subscribe<
|
||||
MessagesSubscription
|
||||
>({
|
||||
query: MESSAGES_SUBSCRIPTION,
|
||||
}).subscribe(({ data }) => {
|
||||
setAddedMessages((prevMessages) => [
|
||||
...prevMessages,
|
||||
{
|
||||
id: data?.messages?.userMessages?.id!,
|
||||
username: data?.messages?.userMessages?.username!,
|
||||
text: data?.messages?.userMessages?.text!,
|
||||
},
|
||||
]);
|
||||
});
|
||||
}
|
||||
})();
|
||||
}, [threadId]);
|
||||
|
||||
const createThreadForm = useFormik({
|
||||
initialValues: {
|
||||
title: '',
|
||||
description: '',
|
||||
},
|
||||
validationSchema: Yup.object().shape({
|
||||
title: Yup.string().required('Title is required'),
|
||||
description: Yup.string().required('Description is required'),
|
||||
}),
|
||||
onSubmit: async ({ title, description }) => {
|
||||
const createdThread = await clientSupport.mutate<
|
||||
CreateThreadMutation,
|
||||
CreateThreadMutationVariables
|
||||
>({
|
||||
mutation: CREATE_THREAD,
|
||||
variables: {
|
||||
projectId: projectId as string,
|
||||
title,
|
||||
threadDescription: description,
|
||||
},
|
||||
});
|
||||
navigate(`/support/${projectId}/${createdThread.data?.createThread.id}`);
|
||||
},
|
||||
});
|
||||
|
||||
const sendMsgForm = useFormik({
|
||||
initialValues: {
|
||||
msg: '',
|
||||
},
|
||||
validationSchema: Yup.object().shape({
|
||||
msg: Yup.string().required('Message is required'),
|
||||
}),
|
||||
onSubmit: async ({ msg }, { resetForm }) => {
|
||||
await clientSupport.mutate<SendMsgMutation, SendMsgMutationVariables>({
|
||||
mutation: SEND_MSG,
|
||||
variables: {
|
||||
threadId: threadId as string,
|
||||
username: `${currentUser?.firstName} ${currentUser?.lastName}`,
|
||||
text: msg,
|
||||
},
|
||||
});
|
||||
resetForm();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Box padding='35px 45px 30px 120px'>
|
||||
{!thread ? (
|
||||
<>
|
||||
<Box
|
||||
background='#F9FAFA'
|
||||
boxShadow='1px 1px 10px rgba(50, 59, 105, 0.25)'
|
||||
borderRadius='10px'
|
||||
padding='50px'
|
||||
width='100%'
|
||||
display='flex'
|
||||
alignItems='center'
|
||||
justifyContent='center'
|
||||
marginBottom='20px'
|
||||
>
|
||||
{role === 'client' ? <ThreadClient /> : <ThreadProductOwner />}
|
||||
</Box>
|
||||
<form onSubmit={createThreadForm.handleSubmit}>
|
||||
<Box
|
||||
background='#F9FAFA'
|
||||
boxShadow='1px 1px 10px rgba(50, 59, 105, 0.25)'
|
||||
borderRadius='10px'
|
||||
padding='15px'
|
||||
display='grid'
|
||||
gridTemplateColumns='auto'
|
||||
alignItems='center'
|
||||
rowGap='15px'
|
||||
>
|
||||
<Input
|
||||
name='title'
|
||||
placeholder='Title'
|
||||
color={role || 'client'}
|
||||
value={createThreadForm.values.title}
|
||||
onChange={createThreadForm.handleChange}
|
||||
onBlur={createThreadForm.handleBlur}
|
||||
error={
|
||||
createThreadForm.touched.title &&
|
||||
!!createThreadForm.errors.title
|
||||
}
|
||||
errorMessage={createThreadForm.errors.title}
|
||||
/>
|
||||
<TextArea
|
||||
name='description'
|
||||
placeholder='Description'
|
||||
color={role || 'client'}
|
||||
value={createThreadForm.values.description}
|
||||
onChange={createThreadForm.handleChange}
|
||||
onBlur={createThreadForm.handleBlur}
|
||||
error={
|
||||
createThreadForm.touched.description &&
|
||||
!!createThreadForm.errors.description
|
||||
}
|
||||
errorMessage={createThreadForm.errors.description}
|
||||
/>
|
||||
<Button
|
||||
text='Send'
|
||||
type='submit'
|
||||
color={role || 'client'}
|
||||
variant='primary-action'
|
||||
iconLeft={<Send />}
|
||||
/>
|
||||
</Box>
|
||||
</form>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Box
|
||||
background='white'
|
||||
boxShadow='1px 1px 10px rgba(50, 59, 105, 0.25)'
|
||||
borderRadius='10px'
|
||||
padding='15px'
|
||||
marginBottom='20px'
|
||||
>
|
||||
<Box marginBottom='10px'>
|
||||
<Text variant='title' weight='bold' gutterBottom>
|
||||
{thread.title}
|
||||
</Text>
|
||||
</Box>
|
||||
<Text variant='body'>{thread.threadDescription}</Text>
|
||||
</Box>
|
||||
<Box
|
||||
background='#F9FAFA'
|
||||
boxShadow='1px 1px 10px rgba(50, 59, 105, 0.25)'
|
||||
borderRadius='10px'
|
||||
padding='15px'
|
||||
display='flex'
|
||||
flexDirection='column'
|
||||
marginBottom='20px'
|
||||
>
|
||||
{messages.map((msg) => (
|
||||
<Box
|
||||
borderRadius='10px'
|
||||
boxShadow='1px 1px 10px rgba(50, 59, 105, 0.25)'
|
||||
marginBottom='15px'
|
||||
padding='10px'
|
||||
color={
|
||||
msg.username ===
|
||||
`${currentUser?.firstName} ${currentUser?.lastName}`
|
||||
? 'white'
|
||||
: 'initial'
|
||||
}
|
||||
key={msg.id}
|
||||
background={
|
||||
msg.username ===
|
||||
`${currentUser?.firstName} ${currentUser?.lastName}`
|
||||
? theme.colors[role || 'client'].main
|
||||
: 'white'
|
||||
}
|
||||
alignSelf={
|
||||
msg.username ===
|
||||
`${currentUser?.firstName} ${currentUser?.lastName}`
|
||||
? 'flex-end'
|
||||
: 'flex-start'
|
||||
}
|
||||
>
|
||||
<Box marginBottom='5px'>
|
||||
<Text variant='body'>{msg.text}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
{addedMessages.map((msg) => (
|
||||
<Box
|
||||
borderRadius='10px'
|
||||
boxShadow='1px 1px 10px rgba(50, 59, 105, 0.25)'
|
||||
marginBottom='15px'
|
||||
padding='10px'
|
||||
color={
|
||||
msg.username ===
|
||||
`${currentUser?.firstName} ${currentUser?.lastName}`
|
||||
? 'white'
|
||||
: 'initial'
|
||||
}
|
||||
key={msg.id}
|
||||
background={
|
||||
msg.username ===
|
||||
`${currentUser?.firstName} ${currentUser?.lastName}`
|
||||
? theme.colors[role || 'client'].main
|
||||
: 'white'
|
||||
}
|
||||
alignSelf={
|
||||
msg.username ===
|
||||
`${currentUser?.firstName} ${currentUser?.lastName}`
|
||||
? 'flex-end'
|
||||
: 'flex-start'
|
||||
}
|
||||
>
|
||||
<Box marginBottom='5px'>
|
||||
<Text variant='body'>{msg.text}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
<form onSubmit={sendMsgForm.handleSubmit}>
|
||||
<Box
|
||||
background='#F9FAFA'
|
||||
boxShadow='1px 1px 10px rgba(50, 59, 105, 0.25)'
|
||||
borderRadius='10px'
|
||||
padding='15px'
|
||||
display='grid'
|
||||
gridTemplateColumns='auto'
|
||||
alignItems='center'
|
||||
rowGap='15px'
|
||||
>
|
||||
<TextArea
|
||||
name='msg'
|
||||
placeholder='Message'
|
||||
color={role || 'client'}
|
||||
value={sendMsgForm.values.msg}
|
||||
onChange={sendMsgForm.handleChange}
|
||||
onBlur={sendMsgForm.handleBlur}
|
||||
error={sendMsgForm.touched.msg && !!sendMsgForm.errors.msg}
|
||||
errorMessage={sendMsgForm.errors.msg}
|
||||
/>
|
||||
<Button
|
||||
text='Send'
|
||||
type='submit'
|
||||
color={role || 'client'}
|
||||
variant='primary-action'
|
||||
iconLeft={<Send />}
|
||||
/>
|
||||
</Box>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Support;
|
||||
@@ -1,7 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
type WrapperProps = {
|
||||
color?: 'client' | 'productOwner' | 'developer' | 'admin';
|
||||
};
|
||||
|
||||
export const Wrapper = styled.div<WrapperProps>``;
|
||||
@@ -1,252 +0,0 @@
|
||||
import { useReactToPrint } from 'react-to-print';
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import { useLazyQuery, useReactiveVar } from '@apollo/client';
|
||||
import { Navigate } from 'react-router';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { roleVar } from '../../graphql/state';
|
||||
import { Design, Empty, Settings, Specification } from '../../assets';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
FeatureCard,
|
||||
Text,
|
||||
Spinner,
|
||||
Link,
|
||||
Specification as SpecificationPrint,
|
||||
Chip,
|
||||
} from '../../components';
|
||||
import { Wrapper } from './styles';
|
||||
import {
|
||||
CategoryOutput,
|
||||
GetAllTemplatesQuery,
|
||||
GetAllTemplatesQueryVariables,
|
||||
GetCategoryByIdQuery,
|
||||
GetCategoryByIdQueryVariables,
|
||||
GetTemplateByIdQuery,
|
||||
GetTemplateByIdQueryVariables,
|
||||
TemplateOutput,
|
||||
} from '../../graphql/types';
|
||||
import {
|
||||
GET_ALL_TEMPLATES,
|
||||
GET_TEMPLATE_BY_ID,
|
||||
} from '../../graphql/template.api';
|
||||
import { GET_CATEGORY_BY_ID } from '../../graphql/category.api';
|
||||
|
||||
const Template = () => {
|
||||
const role = useReactiveVar(roleVar);
|
||||
const navigate = useNavigate();
|
||||
const printRef = useRef<HTMLDivElement>(null);
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const [template, setTemplate] = useState<TemplateOutput>();
|
||||
const [category, setCategory] = useState<CategoryOutput>();
|
||||
|
||||
const [getCategory, { loading: categoryLoading, error: categoryError }] = useLazyQuery<
|
||||
GetCategoryByIdQuery,
|
||||
GetCategoryByIdQueryVariables
|
||||
>(GET_CATEGORY_BY_ID, {
|
||||
onCompleted({ getCategoryById }) {
|
||||
setCategory(getCategoryById);
|
||||
}
|
||||
});
|
||||
|
||||
const [getTemplates, { loading: templatesLoading, error: templatesError }] = useLazyQuery<
|
||||
GetAllTemplatesQuery,
|
||||
GetAllTemplatesQueryVariables
|
||||
>(GET_ALL_TEMPLATES, {
|
||||
onCompleted({ getAllTemplates }) {
|
||||
if (getAllTemplates.length > 0) {
|
||||
setTemplate(getAllTemplates[0]);
|
||||
getCategory({ variables: { id: getAllTemplates[0]?.category! } });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const [getTemplate, { loading: templateLoading, error: templateError }] = useLazyQuery<
|
||||
GetTemplateByIdQuery,
|
||||
GetTemplateByIdQueryVariables
|
||||
>(GET_TEMPLATE_BY_ID, {
|
||||
onCompleted({ getTemplateById }) {
|
||||
setTemplate(getTemplateById);
|
||||
getCategory({ variables: { id: getTemplateById?.category! } });
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
getTemplate({ variables: { id } });
|
||||
} else {
|
||||
getTemplates();
|
||||
}
|
||||
|
||||
return () => {
|
||||
setTemplate(undefined);
|
||||
setCategory(undefined);
|
||||
};
|
||||
}, [id]);
|
||||
|
||||
const handlePrint = useReactToPrint({
|
||||
content: () => printRef.current,
|
||||
});
|
||||
|
||||
if (role !== 'productOwner' && role !== 'developer') return (
|
||||
<>
|
||||
{role === 'admin' && <Navigate to='/clients' />}
|
||||
{role === 'client' && <Navigate to='/project' />}
|
||||
</>
|
||||
);
|
||||
|
||||
if (templatesLoading || templateLoading || categoryLoading) return (
|
||||
<Spinner fullScreen color={role || 'client'} />
|
||||
);
|
||||
|
||||
if (templatesError || templateError || categoryError || !template) return (
|
||||
<Wrapper color={role}>
|
||||
<Box
|
||||
width='100%'
|
||||
height='100vh'
|
||||
display='grid'
|
||||
alignItems='center'
|
||||
justifyContent='center'
|
||||
>
|
||||
<Box>
|
||||
<Empty />
|
||||
</Box>
|
||||
</Box>
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Box padding='35px 45px 0px 120px'>
|
||||
<Box
|
||||
display='flex'
|
||||
flexDirection='row'
|
||||
alignItems='center'
|
||||
marginBottom='20px'
|
||||
>
|
||||
<Box
|
||||
flexGrow='1'
|
||||
display='flex'
|
||||
flexDirection='row'
|
||||
alignItems='center'
|
||||
>
|
||||
<Text variant='headline' weight='bold'>
|
||||
{template.name}
|
||||
</Text>
|
||||
{category && (
|
||||
<Box marginLeft='20px'>
|
||||
<Chip text={category.name} color={role} />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<Box
|
||||
marginRight={role === 'productOwner' ? '20px' : undefined}
|
||||
>
|
||||
<Button
|
||||
color={role || 'client'}
|
||||
variant='primary-action'
|
||||
text='Prototype'
|
||||
iconLeft={<Design />}
|
||||
disabled={
|
||||
role === 'productOwner'
|
||||
}
|
||||
onClick={() =>
|
||||
navigate(`/prototype/${id || template.id}`)
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
{role === 'productOwner' && (
|
||||
<Box>
|
||||
<Button
|
||||
color={role || 'client'}
|
||||
variant='primary-action'
|
||||
text='Settings'
|
||||
iconLeft={<Settings />}
|
||||
onClick={() =>
|
||||
navigate(`/template-settings/${id || template.id}`)
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<Box marginBottom='30px'>
|
||||
<Text variant='headline' gutterBottom>
|
||||
Description
|
||||
</Text>
|
||||
<Text>{template.description}</Text>
|
||||
</Box>
|
||||
{template.features && (
|
||||
<Box
|
||||
display='flex'
|
||||
flexDirection='column'
|
||||
marginBottom='30px'
|
||||
>
|
||||
<Box marginBottom='10px'>
|
||||
<Text variant='headline' gutterBottom>
|
||||
Features
|
||||
</Text>
|
||||
</Box>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='repeat(3, 1fr)'
|
||||
columnGap='40px'
|
||||
rowGap='45px'
|
||||
alignItems='stretch'
|
||||
>
|
||||
{template.features.map((feature) => (
|
||||
<FeatureCard feature={feature} key={feature.id} />
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
{template.specification && (
|
||||
<Box
|
||||
display='flex'
|
||||
flexDirection='column'
|
||||
marginBottom='30px'
|
||||
>
|
||||
<Box marginBottom='10px'>
|
||||
<Text variant='headline' gutterBottom>
|
||||
Deliverables
|
||||
</Text>
|
||||
</Box>
|
||||
<Box
|
||||
display='flex'
|
||||
flexDirection='row'
|
||||
alignItems='center'
|
||||
justifyContent='space-between'
|
||||
padding='35px 20px'
|
||||
boxShadow='1px 1px 10px rgba(50, 59, 105, 0.25)'
|
||||
borderRadius='10px'
|
||||
>
|
||||
<Box
|
||||
display='flex'
|
||||
flexDirection='row'
|
||||
alignItems='center'
|
||||
>
|
||||
<Box marginRight='10px'>
|
||||
<Specification />
|
||||
</Box>
|
||||
<Text variant='title'>Specification</Text>
|
||||
</Box>
|
||||
<Link href='#' color={role} onClick={handlePrint}>
|
||||
Download
|
||||
</Link>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
{template.specification && template.features && (
|
||||
<Box display='none'>
|
||||
<SpecificationPrint
|
||||
ref={printRef}
|
||||
specification={template.specification}
|
||||
features={template.features}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Template;
|
||||
@@ -1,12 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
type WrapperProps = {
|
||||
color?: 'client' | 'productOwner' | 'developer' | 'admin';
|
||||
};
|
||||
|
||||
export const Wrapper = styled.div<WrapperProps>`
|
||||
.empty {
|
||||
fill: ${({ theme, color }) =>
|
||||
color ? theme.colors[color].main : theme.colors.client.main};
|
||||
}
|
||||
`;
|
||||
@@ -1,5 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
padding: 35px 45px 35px 120px;
|
||||
`;
|
||||
@@ -1,233 +0,0 @@
|
||||
import * as Yup from 'yup';
|
||||
import { useFormik } from 'formik';
|
||||
import { useNavigate, useParams } from 'react-router';
|
||||
import { useLazyQuery, useMutation, useReactiveVar } from '@apollo/client';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { roleVar } from '../../graphql/state';
|
||||
import { Wrapper } from './styles';
|
||||
import { Alert, Box, Button, Input, Spinner, Text } from '../../components';
|
||||
import {
|
||||
GetProjectByIdQuery,
|
||||
GetProjectByIdQueryVariables,
|
||||
ProjectOutput,
|
||||
UpdateProjectMutation,
|
||||
UpdateProjectMutationVariables,
|
||||
} from '../../graphql/types';
|
||||
import { GET_PROJECT_BY_ID, UPDATE_PROJECT } from '../../graphql/project.api';
|
||||
import { ArrowLeft, Empty } from '../../assets';
|
||||
import { theme } from '../../themes';
|
||||
|
||||
const UpdateProject = () => {
|
||||
const navigate = useNavigate();
|
||||
const role = useReactiveVar(roleVar);
|
||||
const [error, setError] = useState<string>('');
|
||||
const [project, setProject] = useState<ProjectOutput>();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
|
||||
const [getProject, { loading: projectLoading, error: projectError }] = useLazyQuery<
|
||||
GetProjectByIdQuery,
|
||||
GetProjectByIdQueryVariables
|
||||
>(GET_PROJECT_BY_ID, {
|
||||
onCompleted({ getProjectById }) {
|
||||
setProject(getProjectById);
|
||||
}
|
||||
});
|
||||
|
||||
const [updateProject, { loading: updateProjectLoading }] = useMutation<
|
||||
UpdateProjectMutation,
|
||||
UpdateProjectMutationVariables
|
||||
>(UPDATE_PROJECT, {
|
||||
onCompleted() {
|
||||
navigate(`/project/${id}`);
|
||||
},
|
||||
onError({ graphQLErrors }) {
|
||||
setError(graphQLErrors[0].extensions?.info as string);
|
||||
setTimeout(() => setError(''), 3000);
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
getProject({ variables: { id } });
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
const basicInfoForm = useFormik({
|
||||
initialValues: {
|
||||
name: project?.name || '',
|
||||
imageName: project?.image.name || '',
|
||||
imageSource: project?.image.src || '',
|
||||
},
|
||||
validationSchema: Yup.object().shape({
|
||||
name: Yup.string().required('Name is required'),
|
||||
imageName: Yup.string().required('Image is required'),
|
||||
imageSource: Yup.string().required('Image is required'),
|
||||
}),
|
||||
onSubmit: ({ name, imageName, imageSource }) => {
|
||||
updateProject({
|
||||
variables: {
|
||||
id: id as string,
|
||||
name,
|
||||
image: { name: imageName, src: imageSource },
|
||||
},
|
||||
});
|
||||
},
|
||||
enableReinitialize: true,
|
||||
});
|
||||
|
||||
if (projectLoading) return (
|
||||
<Spinner fullScreen color={role || 'client'} />
|
||||
);
|
||||
|
||||
if (projectError || !project) return (
|
||||
<Wrapper color={role}>
|
||||
<Box
|
||||
width='100%'
|
||||
height='100vh'
|
||||
display='grid'
|
||||
alignItems='center'
|
||||
justifyContent='center'
|
||||
>
|
||||
<Box>
|
||||
<Empty />
|
||||
</Box>
|
||||
</Box>
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Box padding='35px 45px 30px 120px'>
|
||||
<Box
|
||||
display='flex'
|
||||
flexDirection='row'
|
||||
alignItems='center'
|
||||
marginBottom='20px'
|
||||
>
|
||||
<Box flexGrow='1' marginRight='20px'>
|
||||
<Button
|
||||
text='Back'
|
||||
color={role || 'client'}
|
||||
size='small'
|
||||
onClick={() => navigate(-1)}
|
||||
iconLeft={<ArrowLeft />}
|
||||
/>
|
||||
<Text variant='headline' weight='bold'>
|
||||
Update Project
|
||||
</Text>
|
||||
</Box>
|
||||
{error && <Alert color='error' text={error} />}
|
||||
<Box
|
||||
display='flex'
|
||||
flexDirection='row'
|
||||
alignItems='center'
|
||||
marginLeft='20px'
|
||||
>
|
||||
<Button
|
||||
text='Update'
|
||||
loading={updateProjectLoading}
|
||||
color={role || 'client'}
|
||||
variant='primary-action'
|
||||
onClick={basicInfoForm.handleSubmit}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='0.7fr 0.5fr'
|
||||
columnGap='45px'
|
||||
alignItems='stretch'
|
||||
>
|
||||
<form onSubmit={basicInfoForm.handleSubmit}>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='auto'
|
||||
rowGap='0.5rem'
|
||||
position='relative'
|
||||
>
|
||||
<Input
|
||||
name='name'
|
||||
label='Name'
|
||||
color={role || 'client'}
|
||||
value={basicInfoForm.values.name}
|
||||
onChange={basicInfoForm.handleChange}
|
||||
onBlur={basicInfoForm.handleBlur}
|
||||
error={
|
||||
basicInfoForm.touched.name && !!basicInfoForm.errors.name
|
||||
}
|
||||
errorMessage={basicInfoForm.errors.name}
|
||||
/>
|
||||
<Input
|
||||
type='file'
|
||||
label='Image'
|
||||
color={role || 'client'}
|
||||
onChange={async (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
const formData = new FormData();
|
||||
|
||||
if (event.target.files && event.target.files[0]) {
|
||||
formData.append('file', event.target.files[0]);
|
||||
formData.append('upload_preset', 'xofll5kc');
|
||||
|
||||
basicInfoForm.setFieldValue('imageName', '');
|
||||
basicInfoForm.setFieldValue('imageSource', '');
|
||||
|
||||
const data = await (
|
||||
await fetch(`${import.meta.env.VITE_CLOUDINARY_URL}`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
).json();
|
||||
|
||||
const filename = data.original_filename;
|
||||
const filesource = data.secure_url;
|
||||
|
||||
basicInfoForm.setFieldValue('imageName', filename);
|
||||
basicInfoForm.setFieldValue('imageSource', filesource);
|
||||
}
|
||||
}}
|
||||
error={
|
||||
basicInfoForm.touched.imageName &&
|
||||
(!!basicInfoForm.errors.imageName ||
|
||||
!!basicInfoForm.errors.imageSource)
|
||||
}
|
||||
errorMessage={basicInfoForm.errors.imageName}
|
||||
/>
|
||||
</Box>
|
||||
</form>
|
||||
<Box
|
||||
display='flex'
|
||||
flexDirection='column'
|
||||
alignItems='center'
|
||||
justifyContent='center'
|
||||
padding='100px'
|
||||
background='#ffffff'
|
||||
boxShadow='1px 1px 10px rgba(50, 59, 105, 0.25)'
|
||||
borderRadius='10px'
|
||||
>
|
||||
<Box
|
||||
className='project-image'
|
||||
background={
|
||||
(basicInfoForm.values.imageSource &&
|
||||
`url(${basicInfoForm.values.imageSource})`) ||
|
||||
theme.colors.client.light
|
||||
}
|
||||
width='150px'
|
||||
height='150px'
|
||||
borderRadius='50%'
|
||||
marginBottom='20px'
|
||||
></Box>
|
||||
<Box>
|
||||
<Text variant='headline' weight='bold'>
|
||||
{basicInfoForm.values.name || 'Project Name'}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default UpdateProject;
|
||||
@@ -1,18 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
type WrapperProps = {
|
||||
color?: 'client' | 'productOwner' | 'developer' | 'admin';
|
||||
};
|
||||
|
||||
export const Wrapper = styled.div<WrapperProps>`
|
||||
.carousel-arrow {
|
||||
background: none;
|
||||
border: none;
|
||||
align-self: center;
|
||||
cursor: pointer;
|
||||
|
||||
svg {
|
||||
stroke: ${({ theme, color }) => theme.colors[color || 'client'].main};
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -1,506 +0,0 @@
|
||||
import * as Yup from 'yup';
|
||||
import { useFormik } from 'formik';
|
||||
import { Navigate, useNavigate, useParams } from 'react-router';
|
||||
import { useMutation, useQuery, useReactiveVar } from '@apollo/client';
|
||||
import { useState } from 'react';
|
||||
import { roleVar } from '../../graphql/state';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Text,
|
||||
SectionSelector,
|
||||
Input,
|
||||
Select,
|
||||
Alert,
|
||||
Spinner,
|
||||
} from '../../components';
|
||||
import { Wrapper } from './styles';
|
||||
import { ArrowLeft, Empty, Profile, Security } from '../../assets';
|
||||
import {
|
||||
UpdateUserInfoMutation,
|
||||
UpdateUserPasswordMutation,
|
||||
UpdateUserInfoMutationVariables,
|
||||
UpdateUserPasswordMutationVariables,
|
||||
GetCountryCodesQuery,
|
||||
GetCountryCodesQueryVariables,
|
||||
GetUserByIdQuery,
|
||||
GetUserByIdQueryVariables,
|
||||
UserOutput,
|
||||
} from '../../graphql/types';
|
||||
import {
|
||||
GET_COUNTRY_CODES,
|
||||
GET_USER_BY_ID,
|
||||
UPDATE_USER_INFO,
|
||||
UPDATE_USER_PASSWORD,
|
||||
} from '../../graphql/auth.api';
|
||||
|
||||
const UserSettings = () => {
|
||||
const navigate = useNavigate();
|
||||
const role = useReactiveVar(roleVar);
|
||||
const [userToEdit, setUserToEdit] = useState<UserOutput>();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const [selectedSection, setSelectedSection] = useState<
|
||||
'general' | 'security'
|
||||
>('general');
|
||||
const [error, setError] = useState<string>('');
|
||||
const [success, setSuccess] = useState<boolean>(false);
|
||||
|
||||
const { data: countryCodes, loading: countryCodesLoading } = useQuery<
|
||||
GetCountryCodesQuery,
|
||||
GetCountryCodesQueryVariables
|
||||
>(GET_COUNTRY_CODES);
|
||||
const { loading: userInfoLoading, error: userInforError } = useQuery<
|
||||
GetUserByIdQuery,
|
||||
GetUserByIdQueryVariables
|
||||
>(GET_USER_BY_ID, {
|
||||
variables: {
|
||||
id: id as string,
|
||||
},
|
||||
onCompleted({ getUserById }) {
|
||||
setUserToEdit(getUserById);
|
||||
},
|
||||
});
|
||||
|
||||
const [updateUserInfo, { loading: generalLoading }] = useMutation<
|
||||
UpdateUserInfoMutation,
|
||||
UpdateUserInfoMutationVariables
|
||||
>(UPDATE_USER_INFO, {
|
||||
onCompleted({ updateUserInfo: user }) {
|
||||
setUserToEdit(user);
|
||||
generalForm.setFieldValue('firstName', user.firstName);
|
||||
generalForm.setFieldValue('lastName', user.lastName);
|
||||
generalForm.setFieldValue('prefix', user.phone.prefix);
|
||||
generalForm.setFieldValue('number', user.phone.number);
|
||||
generalForm.setFieldValue('place', user.address.place);
|
||||
generalForm.setFieldValue('city', user.address.city);
|
||||
generalForm.setFieldValue('zip', user.address.zip);
|
||||
generalForm.setFieldValue('country', user.address.country);
|
||||
setSuccess(true);
|
||||
setTimeout(() => setSuccess(false), 3000);
|
||||
},
|
||||
onError({ graphQLErrors }) {
|
||||
setError(graphQLErrors[0]?.extensions?.info as string);
|
||||
setTimeout(() => setError(''), 3000);
|
||||
},
|
||||
});
|
||||
|
||||
const generalForm = useFormik({
|
||||
initialValues: {
|
||||
firstName: userToEdit?.firstName || '',
|
||||
lastName: userToEdit?.lastName || '',
|
||||
prefix: userToEdit?.phone.prefix || '',
|
||||
number: userToEdit?.phone.number || '',
|
||||
place: userToEdit?.address.place || '',
|
||||
city: userToEdit?.address.city || '',
|
||||
zip: userToEdit?.address.zip || '',
|
||||
country: userToEdit?.address.country || '',
|
||||
},
|
||||
validationSchema: Yup.object().shape({
|
||||
firstName: Yup.string().required('First Name is required'),
|
||||
lastName: Yup.string().required('Last Name is required'),
|
||||
prefix: Yup.string().required('Prefix is required'),
|
||||
// prettier-ignore
|
||||
number: Yup.number().typeError('Phone must be a number').required('Phone is required'),
|
||||
place: Yup.string().required('Address is required'),
|
||||
city: Yup.string().required('City is required'),
|
||||
country: Yup.string().required('Country is required'),
|
||||
// prettier-ignore
|
||||
zip: Yup.number().typeError('Zip must be a number').required('Zip is required'),
|
||||
}),
|
||||
onSubmit: ({
|
||||
firstName,
|
||||
lastName,
|
||||
prefix,
|
||||
number,
|
||||
place,
|
||||
city,
|
||||
country,
|
||||
zip,
|
||||
}) =>
|
||||
updateUserInfo({
|
||||
variables: {
|
||||
user: {
|
||||
id: userToEdit?.id!,
|
||||
email: userToEdit?.email!,
|
||||
firstName,
|
||||
lastName,
|
||||
phone: { prefix, number },
|
||||
address: { place, city, country, zip },
|
||||
role: userToEdit?.role!,
|
||||
},
|
||||
},
|
||||
}),
|
||||
enableReinitialize: true,
|
||||
});
|
||||
|
||||
const [updateUserPassword, { loading: securityLoading }] = useMutation<
|
||||
UpdateUserPasswordMutation,
|
||||
UpdateUserPasswordMutationVariables
|
||||
>(UPDATE_USER_PASSWORD, {
|
||||
onCompleted({ updateUserPassword: user }) {
|
||||
setUserToEdit(user);
|
||||
setSuccess(true);
|
||||
setTimeout(() => setSuccess(false), 3000);
|
||||
},
|
||||
onError({ graphQLErrors }) {
|
||||
setError(graphQLErrors[0]?.extensions?.info as string);
|
||||
setTimeout(() => setError(''), 3000);
|
||||
},
|
||||
});
|
||||
|
||||
const securityForm = useFormik({
|
||||
initialValues: {
|
||||
oldPassword: '',
|
||||
newPassword: '',
|
||||
confirmNewPassword: '',
|
||||
},
|
||||
validationSchema: Yup.object().shape({
|
||||
oldPassword: Yup.string()
|
||||
.required('Old password is required')
|
||||
.min(6, 'Old password is 6 characters minimum'),
|
||||
newPassword: Yup.string()
|
||||
.required('New password is required')
|
||||
.notOneOf(
|
||||
[Yup.ref('oldPassword')],
|
||||
'New password should not be old password'
|
||||
)
|
||||
.required('New password is required')
|
||||
.min(6, 'New password is 6 characters minimum'),
|
||||
confirmNewPassword: Yup.string()
|
||||
.required('Confirm new password is required')
|
||||
.oneOf(
|
||||
[Yup.ref('newPassword')],
|
||||
"Confirm new password doesn't match with new password"
|
||||
),
|
||||
}),
|
||||
onSubmit: ({ oldPassword, newPassword }) =>
|
||||
updateUserPassword({
|
||||
variables: {
|
||||
id: userToEdit?.id!,
|
||||
password: { oldPassword, newPassword },
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
if (role !== 'admin') return (
|
||||
<Navigate to='/' />
|
||||
)
|
||||
|
||||
if (userInforError || !userToEdit) return (
|
||||
<Wrapper color={role}>
|
||||
<Box
|
||||
width='100%'
|
||||
height='100vh'
|
||||
display='grid'
|
||||
alignItems='center'
|
||||
justifyContent='center'
|
||||
>
|
||||
<Box>
|
||||
<Empty />
|
||||
</Box>
|
||||
</Box>
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Box>
|
||||
<Button
|
||||
text='Back'
|
||||
color={role || 'client'}
|
||||
size='small'
|
||||
onClick={() => navigate(-1)}
|
||||
iconLeft={<ArrowLeft />}
|
||||
/>
|
||||
<Text variant='headline' weight='bold'>
|
||||
Edit
|
||||
</Text>
|
||||
</Box>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='0.5fr 2fr'
|
||||
columnGap='25px'
|
||||
marginTop='1rem'
|
||||
>
|
||||
<Box display='grid' rowGap='0.5rem' gridTemplateRows='50px'>
|
||||
<SectionSelector
|
||||
icon={<Profile />}
|
||||
color={role || 'client'}
|
||||
text='General'
|
||||
selected={selectedSection === 'general'}
|
||||
onClick={() => setSelectedSection('general')}
|
||||
/>
|
||||
<SectionSelector
|
||||
icon={<Security />}
|
||||
color={role || 'client'}
|
||||
text='Security'
|
||||
selected={selectedSection === 'security'}
|
||||
onClick={() => setSelectedSection('security')}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
background='white'
|
||||
boxShadow='1px 1px 10px 0px rgba(50, 59, 105, 0.25)'
|
||||
borderRadius='10px'
|
||||
width='100%'
|
||||
padding='30px'
|
||||
>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='auto 1fr'
|
||||
columnGap='1rem'
|
||||
alignItems='center'
|
||||
marginBottom='50px'
|
||||
>
|
||||
<Text variant='subheader' weight='bold'>
|
||||
{selectedSection === 'general' ? 'General' : 'Security'}
|
||||
</Text>
|
||||
{error && <Alert color='error' text={error} />}
|
||||
{success && (
|
||||
<Alert color='success' text='Account updated successfully' />
|
||||
)}
|
||||
</Box>
|
||||
{selectedSection === 'general' && (
|
||||
<>
|
||||
{!countryCodesLoading && !userInfoLoading ? (
|
||||
<form onSubmit={generalForm.handleSubmit}>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='auto'
|
||||
rowGap='0.5rem'
|
||||
position='relative'
|
||||
>
|
||||
<Input
|
||||
name='firstName'
|
||||
label='First Name'
|
||||
color={role || 'client'}
|
||||
value={generalForm.values.firstName}
|
||||
onChange={generalForm.handleChange}
|
||||
onBlur={generalForm.handleBlur}
|
||||
error={
|
||||
generalForm.touched.firstName &&
|
||||
!!generalForm.errors.firstName
|
||||
}
|
||||
errorMessage={generalForm.errors.firstName}
|
||||
/>
|
||||
<Input
|
||||
name='lastName'
|
||||
label='Last Name'
|
||||
color={role || 'client'}
|
||||
value={generalForm.values.lastName}
|
||||
onChange={generalForm.handleChange}
|
||||
onBlur={generalForm.handleBlur}
|
||||
error={
|
||||
generalForm.touched.lastName &&
|
||||
!!generalForm.errors.lastName
|
||||
}
|
||||
errorMessage={generalForm.errors.lastName}
|
||||
/>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='1fr 1.5fr'
|
||||
columnGap='10px'
|
||||
>
|
||||
<Select
|
||||
name='prefix'
|
||||
label='Country Code'
|
||||
color={role || 'client'}
|
||||
options={
|
||||
countryCodes?.getCountryCode
|
||||
? countryCodes.getCountryCode.map(
|
||||
({ prefix, country }) => ({
|
||||
value: prefix,
|
||||
label: `+${prefix} (${country})`,
|
||||
})
|
||||
)
|
||||
: [{ value: '216', label: '+216' }]
|
||||
}
|
||||
onChange={generalForm.handleChange}
|
||||
onBlur={generalForm.handleBlur}
|
||||
value={generalForm.values.prefix}
|
||||
select={generalForm.values.prefix}
|
||||
error={
|
||||
generalForm.touched.prefix &&
|
||||
!!generalForm.errors.prefix
|
||||
}
|
||||
errorMessage={generalForm.errors.prefix}
|
||||
/>
|
||||
<Input
|
||||
name='number'
|
||||
type='tel'
|
||||
label='Phone'
|
||||
color={role || 'client'}
|
||||
onChange={generalForm.handleChange}
|
||||
onBlur={generalForm.handleBlur}
|
||||
value={generalForm.values.number}
|
||||
error={
|
||||
generalForm.touched.number &&
|
||||
!!generalForm.errors.number
|
||||
}
|
||||
errorMessage={generalForm.errors.number}
|
||||
/>
|
||||
</Box>
|
||||
<Input
|
||||
name='place'
|
||||
label='Address'
|
||||
color={role || 'client'}
|
||||
onChange={generalForm.handleChange}
|
||||
onBlur={generalForm.handleBlur}
|
||||
value={generalForm.values.place}
|
||||
error={
|
||||
generalForm.touched.place && !!generalForm.errors.place
|
||||
}
|
||||
errorMessage={generalForm.errors.place}
|
||||
/>
|
||||
<Input
|
||||
name='city'
|
||||
label='City'
|
||||
color={role || 'client'}
|
||||
onChange={generalForm.handleChange}
|
||||
onBlur={generalForm.handleBlur}
|
||||
value={generalForm.values.city}
|
||||
error={
|
||||
generalForm.touched.city && !!generalForm.errors.city
|
||||
}
|
||||
errorMessage={generalForm.errors.city}
|
||||
/>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='2fr 1fr'
|
||||
columnGap='10px'
|
||||
>
|
||||
<Select
|
||||
name='country'
|
||||
label='Country'
|
||||
color={role || 'client'}
|
||||
options={
|
||||
countryCodes?.getCountryCode
|
||||
? countryCodes.getCountryCode.map(
|
||||
({ country }) => ({
|
||||
value: country,
|
||||
label: country,
|
||||
})
|
||||
)
|
||||
: [{ value: 'Tunisia', label: 'Tunisia' }]
|
||||
}
|
||||
onChange={generalForm.handleChange}
|
||||
onBlur={generalForm.handleBlur}
|
||||
value={generalForm.values.country}
|
||||
select={generalForm.values.country}
|
||||
error={
|
||||
generalForm.touched.country &&
|
||||
!!generalForm.errors.country
|
||||
}
|
||||
errorMessage={generalForm.errors.country}
|
||||
/>
|
||||
<Input
|
||||
name='zip'
|
||||
label='Zip Code'
|
||||
color={role || 'client'}
|
||||
onChange={generalForm.handleChange}
|
||||
onBlur={generalForm.handleBlur}
|
||||
value={generalForm.values.zip}
|
||||
error={
|
||||
generalForm.touched.zip && !!generalForm.errors.zip
|
||||
}
|
||||
errorMessage={generalForm.errors.zip}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
marginTop='0.5rem'
|
||||
display='grid'
|
||||
gridTemplateColumns='repeat(2, auto)'
|
||||
justifyContent='flex-end'
|
||||
>
|
||||
<Button
|
||||
variant='primary-action'
|
||||
color={role || 'client'}
|
||||
text='Save'
|
||||
type='submit'
|
||||
loading={generalLoading}
|
||||
disabled={generalLoading}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</form>
|
||||
) : (
|
||||
<Box display='grid' alignItems='center' justifyContent='center'>
|
||||
<Spinner color={role || 'client'} />
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{selectedSection === 'security' && (
|
||||
<form onSubmit={securityForm.handleSubmit}>
|
||||
<Box
|
||||
display='grid'
|
||||
gridTemplateColumns='auto'
|
||||
rowGap='0.5rem'
|
||||
position='relative'
|
||||
>
|
||||
<Input
|
||||
name='oldPassword'
|
||||
label='Old Password'
|
||||
color={role || 'client'}
|
||||
type='password'
|
||||
value={securityForm.values.oldPassword}
|
||||
onChange={securityForm.handleChange}
|
||||
onBlur={securityForm.handleBlur}
|
||||
error={
|
||||
securityForm.touched.oldPassword &&
|
||||
!!securityForm.errors.oldPassword
|
||||
}
|
||||
errorMessage={securityForm.errors.oldPassword}
|
||||
/>
|
||||
<Input
|
||||
name='newPassword'
|
||||
label='New Password'
|
||||
color={role || 'client'}
|
||||
type='password'
|
||||
value={securityForm.values.newPassword}
|
||||
onChange={securityForm.handleChange}
|
||||
onBlur={securityForm.handleBlur}
|
||||
error={
|
||||
securityForm.touched.newPassword &&
|
||||
!!securityForm.errors.newPassword
|
||||
}
|
||||
errorMessage={securityForm.errors.newPassword}
|
||||
/>
|
||||
<Input
|
||||
name='confirmNewPassword'
|
||||
label='Confirm New Password'
|
||||
color={role || 'client'}
|
||||
type='password'
|
||||
value={securityForm.values.confirmNewPassword}
|
||||
onChange={securityForm.handleChange}
|
||||
onBlur={securityForm.handleBlur}
|
||||
error={
|
||||
securityForm.touched.confirmNewPassword &&
|
||||
!!securityForm.errors.confirmNewPassword
|
||||
}
|
||||
errorMessage={securityForm.errors.confirmNewPassword}
|
||||
/>
|
||||
<Box
|
||||
marginTop='0.5rem'
|
||||
display='flex'
|
||||
justifyContent='flex-end'
|
||||
>
|
||||
<Button
|
||||
variant='primary-action'
|
||||
color={role || 'client'}
|
||||
text='Save'
|
||||
type='submit'
|
||||
loading={securityLoading}
|
||||
disabled={securityLoading}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</form>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserSettings;
|
||||
@@ -1,5 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
padding: 35px 45px 35px 120px;
|
||||
`;
|
||||
@@ -1,322 +0,0 @@
|
||||
import * as Yup from 'yup';
|
||||
import { useFormik } from 'formik';
|
||||
import { useMutation, useLazyQuery, useReactiveVar } from '@apollo/client';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Navigate, useNavigate, useLocation } from 'react-router';
|
||||
import { roleVar } from '../../graphql/state';
|
||||
import { Add, Delete, Edit, Empty } from '../../assets';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Spinner,
|
||||
Text,
|
||||
Modal,
|
||||
Input,
|
||||
Alert
|
||||
} from '../../components';
|
||||
import { Wrapper } from './styles';
|
||||
import {
|
||||
DeleteUserMutation,
|
||||
DeleteUserMutationVariables,
|
||||
GetAllUsersQuery,
|
||||
GetAllUsersQueryVariables,
|
||||
UserOutput,
|
||||
} from '../../graphql/types';
|
||||
import { GET_ALL_USERS } from '../../graphql/admin.api';
|
||||
import { DELETE_USER } from '../../graphql/auth.api';
|
||||
|
||||
const Users = () => {
|
||||
const role = useReactiveVar(roleVar);
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [users, setUsers] = useState<Array<UserOutput>>();
|
||||
const [userToDelete, setUserToDelete] = useState<UserOutput>();
|
||||
const [error, setError] = useState<string>('');
|
||||
const [deleteAccountModal, setDeleteAccountModal] = useState<boolean>(false);
|
||||
|
||||
const [getUsers, { loading, error: usersError }] = useLazyQuery<
|
||||
GetAllUsersQuery,
|
||||
GetAllUsersQueryVariables
|
||||
>(GET_ALL_USERS, {
|
||||
onCompleted({ getAllUsers }) {
|
||||
const userRole =
|
||||
location.pathname === '/clients'
|
||||
? 'Client'
|
||||
: location.pathname === '/product-owners'
|
||||
? 'ProductOwner'
|
||||
: 'Developer';
|
||||
setUsers(getAllUsers.filter((user) => user.role === userRole));
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
getUsers();
|
||||
}, [location.pathname]);
|
||||
|
||||
const [deleteUser] = useMutation<
|
||||
DeleteUserMutation,
|
||||
DeleteUserMutationVariables
|
||||
>(DELETE_USER, {
|
||||
onCompleted() {
|
||||
setUsers(users?.filter((user) => user.id !== userToDelete?.id));
|
||||
setUserToDelete(undefined);
|
||||
setDeleteAccountModal(false);
|
||||
},
|
||||
onError({ graphQLErrors }) {
|
||||
setDeleteAccountModal(false);
|
||||
setError(graphQLErrors[0]?.extensions?.info as string);
|
||||
setTimeout(() => setError(''), 3000);
|
||||
},
|
||||
});
|
||||
|
||||
const deleteAccountForm = useFormik({
|
||||
initialValues: {
|
||||
password: '',
|
||||
},
|
||||
validationSchema: Yup.object().shape({
|
||||
password: Yup.string()
|
||||
.required('Password is required')
|
||||
.min(6, 'Password is 6 characters minimum'),
|
||||
}),
|
||||
onSubmit: ({ password }, { resetForm }) => {
|
||||
try {
|
||||
deleteUser({ variables: { id: userToDelete?.id!, password } });
|
||||
} finally {
|
||||
resetForm();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (role !== 'admin') return (
|
||||
<Navigate to='/' />
|
||||
)
|
||||
|
||||
if (loading) return (
|
||||
<Spinner fullScreen color={role || 'client'} />
|
||||
);
|
||||
|
||||
if (usersError || !users) return (
|
||||
<Wrapper color={role}>
|
||||
<Box
|
||||
width='100%'
|
||||
height='100vh'
|
||||
display='grid'
|
||||
alignItems='center'
|
||||
justifyContent='center'
|
||||
>
|
||||
<Box>
|
||||
<Empty />
|
||||
</Box>
|
||||
</Box>
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{deleteAccountModal && (
|
||||
<Modal
|
||||
color={role || 'client'}
|
||||
title='Delete Account'
|
||||
description='Enter password to confirm account deletion.
|
||||
If you delete your account you cannot recover any of your projects.'
|
||||
onClose={() => setDeleteAccountModal(false)}
|
||||
onConfirm={deleteAccountForm.handleSubmit}
|
||||
>
|
||||
<Input
|
||||
type='password'
|
||||
placeholder='Password'
|
||||
name='password'
|
||||
value={deleteAccountForm.values.password}
|
||||
onChange={deleteAccountForm.handleChange}
|
||||
onBlur={deleteAccountForm.handleBlur}
|
||||
color={role || 'client'}
|
||||
error={
|
||||
deleteAccountForm.touched.password &&
|
||||
!!deleteAccountForm.errors.password
|
||||
}
|
||||
errorMessage={deleteAccountForm.errors.password}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
{users && users.length > 0 ? (
|
||||
<Wrapper color={role} empty={false}>
|
||||
<Box width='100%' height='100vh' alignItems='center'>
|
||||
<Box
|
||||
display='flex'
|
||||
flexDirection='row'
|
||||
alignItems='center'
|
||||
marginBottom='20px'
|
||||
>
|
||||
<Box flexGrow={!error ? '1' : undefined}>
|
||||
<Text variant='headline' weight='bold'>
|
||||
{location.pathname === '/clients'
|
||||
? 'Clients'
|
||||
: location.pathname === '/product-owners'
|
||||
? 'Product Owners'
|
||||
: 'Developers'}
|
||||
</Text>
|
||||
</Box>
|
||||
{error && (
|
||||
<Box flexGrow='1' marginLeft='55px' marginRight='55px'>
|
||||
<Alert color='error' text={error} />
|
||||
</Box>
|
||||
)}
|
||||
<Button
|
||||
color={role || 'client'}
|
||||
variant='primary-action'
|
||||
text={`New ${
|
||||
location.pathname === '/clients'
|
||||
? 'Client'
|
||||
: location.pathname === '/product-owners'
|
||||
? 'Product Owner'
|
||||
: 'Developer'
|
||||
}`}
|
||||
iconLeft={<Add />}
|
||||
onClick={() =>
|
||||
navigate(
|
||||
`/create-user/${
|
||||
location.pathname === '/clients'
|
||||
? 'Client'
|
||||
: location.pathname === '/product-owners'
|
||||
? 'ProductOwner'
|
||||
: 'Developer'
|
||||
}`
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
padding='15px 20px'
|
||||
borderRadius='10px'
|
||||
boxShadow='1px 1px 10px 0px rgba(50, 59, 105, 0.25)'
|
||||
display='grid'
|
||||
gridTemplateColumns='repeat(5, 1fr)'
|
||||
alignItems='center'
|
||||
justifyContent='flex-start'
|
||||
className='table-head'
|
||||
marginBottom='20px'
|
||||
columnGap='3rem'
|
||||
>
|
||||
<Text variant='title'>First Name</Text>
|
||||
<Text variant='title'>Last Name</Text>
|
||||
<Text variant='title'>Email </Text>
|
||||
<Text variant='title'>Phone </Text>
|
||||
<Box justifySelf='flex-end'>
|
||||
<Text variant='title'>Actions</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box padding='10px 0px'>
|
||||
{users.map((user) => (
|
||||
<Box
|
||||
key={user.id}
|
||||
padding='15px 20px'
|
||||
borderRadius='10px'
|
||||
boxShadow='1px 1px 10px 0px rgba(50, 59, 105, 0.25)'
|
||||
display='grid'
|
||||
gridTemplateColumns='repeat(5, 1fr)'
|
||||
alignItems='center'
|
||||
justifyContent='flex-start'
|
||||
marginBottom='20px'
|
||||
columnGap='3rem'
|
||||
>
|
||||
<Text variant='headline' weight='bold'>
|
||||
{user.firstName}
|
||||
</Text>
|
||||
<Text variant='headline' weight='bold'>
|
||||
{user.lastName}
|
||||
</Text>
|
||||
<Text variant='headline' weight='bold'>
|
||||
{user.email}
|
||||
</Text>
|
||||
<Text variant='headline' weight='bold'>
|
||||
+{user.phone.prefix}
|
||||
{user.phone.number}
|
||||
</Text>
|
||||
<Box
|
||||
display='flex'
|
||||
flexDirection='row'
|
||||
alignItems='center'
|
||||
justifySelf='flex-end'
|
||||
>
|
||||
<Box
|
||||
onClick={() => navigate(`/user-settings/${user.id}`)}
|
||||
marginRight='15px'
|
||||
cursor='pointer'
|
||||
>
|
||||
<Edit />
|
||||
</Box>
|
||||
<Box
|
||||
onClick={() => {
|
||||
setUserToDelete(user);
|
||||
setDeleteAccountModal(true);
|
||||
}}
|
||||
cursor='pointer'
|
||||
>
|
||||
<Delete />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</Wrapper>
|
||||
) : (
|
||||
<Wrapper color={role} empty>
|
||||
<Box
|
||||
display='flex'
|
||||
flexDirection='row'
|
||||
alignItems='center'
|
||||
marginBottom='20px'
|
||||
padding='35px 45px 0px 120px'
|
||||
>
|
||||
<Box flexGrow='1'>
|
||||
<Text variant='headline' weight='bold'>
|
||||
{location.pathname === '/clients'
|
||||
? 'Clients'
|
||||
: location.pathname === '/product-owners'
|
||||
? 'Product Owners'
|
||||
: 'Developers'}
|
||||
</Text>
|
||||
</Box>
|
||||
<Button
|
||||
color={role || 'client'}
|
||||
variant='primary-action'
|
||||
text={`New ${
|
||||
location.pathname === '/clients'
|
||||
? 'Client'
|
||||
: location.pathname === '/product-owners'
|
||||
? 'Product Owner'
|
||||
: 'Developer'
|
||||
}`}
|
||||
iconLeft={<Add />}
|
||||
onClick={() =>
|
||||
navigate(
|
||||
`/create-user/${
|
||||
location.pathname === '/clients'
|
||||
? 'Client'
|
||||
: location.pathname === '/product-owners'
|
||||
? 'ProductOwner'
|
||||
: 'Developer'
|
||||
}`
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
width='100%'
|
||||
height='100vh'
|
||||
display='grid'
|
||||
alignItems='center'
|
||||
justifyContent='center'
|
||||
>
|
||||
<Box>
|
||||
<Empty />
|
||||
</Box>
|
||||
</Box>
|
||||
</Wrapper>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Users;
|
||||
@@ -1,24 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
type WrapperProps = {
|
||||
color?: 'client' | 'productOwner' | 'developer' | 'admin';
|
||||
empty?: boolean;
|
||||
};
|
||||
|
||||
export const Wrapper = styled.div<WrapperProps>`
|
||||
padding: ${({ empty }) => (empty ? '0px' : '35px 45px 35px 120px')};
|
||||
|
||||
.table-head {
|
||||
p {
|
||||
background: ${({ theme }) => theme.colors.gray.dark};
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.empty {
|
||||
fill: ${({ theme, color }) =>
|
||||
color ? theme.colors[color].main : theme.colors.client.main};
|
||||
}
|
||||
`;
|
||||
@@ -1,51 +0,0 @@
|
||||
import Login from './Auth/Login';
|
||||
import Signup from './Auth/Signup';
|
||||
import AdditionalInfo from './Auth/AdditionalInfo';
|
||||
import ForgotPassword from './Auth/ForgotPassword';
|
||||
import RecoverAccount from './Auth/RecoverAccount';
|
||||
import Project from './Project';
|
||||
import Template from './Template';
|
||||
import Feature from './Feature';
|
||||
import Category from './Category';
|
||||
import Prototype from './Prototype';
|
||||
import Users from './Users';
|
||||
import CreateUser from './CreateUser';
|
||||
import Settings from './Settings';
|
||||
import UserSettings from './UserSettings';
|
||||
import AddCategory from './AddCategory';
|
||||
import AddFeature from './AddFeature';
|
||||
import AddTemplate from './AddTemplate';
|
||||
import CategorySettings from './CategorySettings';
|
||||
import FeatureSettings from './FeatureSettings';
|
||||
import TemplateSettings from './TemplateSettings';
|
||||
import AddProject from './AddProject';
|
||||
import UpdateProject from './UpdateProject';
|
||||
import Payments from './Payments';
|
||||
import Support from './Support';
|
||||
|
||||
export {
|
||||
Login,
|
||||
Signup,
|
||||
AdditionalInfo,
|
||||
ForgotPassword,
|
||||
RecoverAccount,
|
||||
Project,
|
||||
Template,
|
||||
Feature,
|
||||
Category,
|
||||
Prototype,
|
||||
Users,
|
||||
CreateUser,
|
||||
Settings,
|
||||
UserSettings,
|
||||
AddCategory,
|
||||
AddFeature,
|
||||
AddTemplate,
|
||||
CategorySettings,
|
||||
FeatureSettings,
|
||||
TemplateSettings,
|
||||
AddProject,
|
||||
UpdateProject,
|
||||
Payments,
|
||||
Support,
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
/// <reference types="react-scripts" />
|
||||
@@ -1,15 +0,0 @@
|
||||
import { ReportHandler } from 'web-vitals';
|
||||
|
||||
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|
||||
@@ -1,5 +0,0 @@
|
||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
||||
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -1,22 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"types": ["node", "vite/client"],
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
|
||||
/* Bundler mode */
|
||||
"baseUrl": "src",
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
"allowJs": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"rewrites": [{ "source": "/(.*)", "destination": "/" }]
|
||||
}
|
||||
@@ -3,5 +3,5 @@ import react from "@vitejs/plugin-react";
|
||||
import svgr from "vite-plugin-svgr";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [svgr(), react()],
|
||||
})
|
||||
plugins: [react(), svgr()],
|
||||
})
|
||||
|
||||