Converting app into a library WIP

This commit is contained in:
Hazem Krimi
2025-03-17 17:35:18 +01:00
parent 6176ed357d
commit 13f88a09b3
99 changed files with 3213 additions and 23132 deletions
-6
View File
@@ -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
View File
@@ -1 +0,0 @@
.eslintrc.js
-65
View File
@@ -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',
},
],
},
};
+3 -5
View File
@@ -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
+20
View File
@@ -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;
+30
View File
@@ -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;
-30
View File
@@ -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)
+35
View File
@@ -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,
}
];
-27
View File
@@ -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>
+39 -56
View File
@@ -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"
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

-25
View File
@@ -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"
}
-3
View File
@@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

-352
View File
@@ -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;
+36 -36
View File
@@ -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,
+26
View File
@@ -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'
},
};
-19
View File
@@ -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;
-19
View File
@@ -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;
-204
View File
@@ -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;
-5
View File
@@ -1,5 +0,0 @@
import styled from 'styled-components';
export const Wrapper = styled.div`
padding: 1rem;
`;
-65
View File
@@ -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,
};
-67
View File
@@ -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
}
}
`;
-192
View File
@@ -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
}
}
`;
-71
View File
@@ -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
}
}
}
`;
-76
View File
@@ -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
}
}
}
`;
-155
View File
@@ -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
}
}
`;
File diff suppressed because it is too large Load Diff
-106
View File
@@ -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
}
}
}
}
}
`;
-10
View File
@@ -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);
-460
View File
@@ -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
}
}
}
`;
-180
View File
@@ -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'
>
>;
};
};
-2904
View File
File diff suppressed because it is too large Load Diff
+60 -107
View File
@@ -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,
};
-201
View File
@@ -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;
-5
View File
@@ -1,5 +0,0 @@
import styled from 'styled-components';
export const Wrapper = styled.div`
padding: 35px 45px 35px 120px;
`;
-489
View File
@@ -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;
-11
View File
@@ -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;
}
`;
File diff suppressed because it is too large Load Diff
-28
View File
@@ -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%;
}
}
`;
-833
View File
@@ -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;
-14
View File
@@ -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};
}
`;
-279
View File
@@ -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;
-5
View File
@@ -1,5 +0,0 @@
import styled from 'styled-components';
export const Wrapper = styled.div`
background: ${({ theme }) => theme.colors.client.main};
`;
-161
View File
@@ -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'>
Dont 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;
-3
View File
@@ -1,3 +0,0 @@
import styled from 'styled-components';
export const Wrapper = styled.div``;
-180
View File
@@ -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'>
Dont 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;
-7
View File
@@ -1,7 +0,0 @@
import styled from 'styled-components';
export const Wrapper = styled.div`
button svg path {
stroke: transparent !important;
}
`;
-162
View File
@@ -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;
-3
View File
@@ -1,3 +0,0 @@
import styled from 'styled-components';
export const Wrapper = styled.div``;
-170
View File
@@ -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;
-7
View File
@@ -1,7 +0,0 @@
import styled from 'styled-components';
export const Wrapper = styled.div`
button svg path {
stroke: transparent !important;
}
`;
-112
View File
@@ -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;
-12
View File
@@ -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};
}
`;
-294
View File
@@ -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;
-5
View File
@@ -1,5 +0,0 @@
import styled from 'styled-components';
export const Wrapper = styled.div`
padding: 35px 45px 35px 120px;
`;
-480
View File
@@ -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;
-9
View File
@@ -1,9 +0,0 @@
import styled from 'styled-components';
export const Wrapper = styled.div`
padding: 35px 45px 35px 120px;
div {
cursor: default;
}
`;
-179
View File
@@ -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;
-12
View File
@@ -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};
}
`;
-586
View File
@@ -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;
-12
View File
@@ -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;
}
`;
-503
View File
@@ -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;
-8
View File
@@ -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};
}
`;
-800
View File
@@ -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;
-18
View File
@@ -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;
}
}
`;
-355
View File
@@ -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;
-12
View File
@@ -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};
}
`;
-548
View File
@@ -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;
-5
View File
@@ -1,5 +0,0 @@
import styled from 'styled-components';
export const Wrapper = styled.div`
padding: 35px 45px 35px 120px;
`;
-329
View File
@@ -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;
-7
View File
@@ -1,7 +0,0 @@
import styled from 'styled-components';
type WrapperProps = {
color?: 'client' | 'productOwner' | 'developer' | 'admin';
};
export const Wrapper = styled.div<WrapperProps>``;
-252
View File
@@ -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;
-12
View File
@@ -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};
}
`;
File diff suppressed because it is too large Load Diff
-5
View File
@@ -1,5 +0,0 @@
import styled from 'styled-components';
export const Wrapper = styled.div`
padding: 35px 45px 35px 120px;
`;
-233
View File
@@ -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;
-18
View File
@@ -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};
}
}
`;
-506
View File
@@ -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;
-5
View File
@@ -1,5 +0,0 @@
import styled from 'styled-components';
export const Wrapper = styled.div`
padding: 35px 45px 35px 120px;
`;
-322
View File
@@ -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;
-24
View File
@@ -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};
}
`;
-51
View File
@@ -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
View File
@@ -1 +0,0 @@
/// <reference types="react-scripts" />
-15
View File
@@ -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;
-5
View File
@@ -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';
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
+18 -13
View File
@@ -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" }]
}
+10
View File
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
-3
View File
@@ -1,3 +0,0 @@
{
"rewrites": [{ "source": "/(.*)", "destination": "/" }]
}
+2 -2
View File
@@ -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()],
})
+2933 -5320
View File
File diff suppressed because it is too large Load Diff