Initial commit from ZIP

This commit is contained in:
kusz committed 2024-11-27 13:18:16 +01:00
commit 378d4692a9
454 files changed
+40367

No files matched your search

+10
View File
@@ -0,0 +1,10 @@
{
"plugins": [
[
"@babel/plugin-proposal-optional-chaining"
],
[
"add-react-displayname"
]
]
}
+44
View File
@@ -0,0 +1,44 @@
{
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "./tsconfig.eslint.json",
"tsconfigRootDir": "./",
"ecmaFeatures": {
"jsx": true
}
},
"plugins": ["@typescript-eslint", "prettier"],
"extends": [
"prettier",
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking",
],
"rules": {
"no-nested-ternary": 0,
"no-console": 0,
"react/prop-types": 0,
"react/jsx-filename-extension": [
1,
{
"extensions": [".js", ".jsx", ".tsx", ".ts"]
}
],
"prettier/prettier": [
"error",
{
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 120
}
],
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
},
"settings": {
"react": {
"version": "detect"
}
}
}
+26
View File
@@ -0,0 +1,26 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
.vscode/*
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# JetBrains
.idea
+13
View File
@@ -0,0 +1,13 @@
{
"printWidth": 120,
"singleQuote": true,
"trailingComma": "es5",
"useTabs": false,
"tabWidth": 2,
"jsxSingleQuote": false,
"jsxBracketSameLine": false,
"bracketSpacing": true,
"arrowParens": "always",
"parser": "typescript",
"endOfLine": "lf"
}
+44
View File
@@ -0,0 +1,44 @@
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.<br>
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.<br>
You will also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.<br>
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.<br>
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.<br>
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
+2
View File
@@ -0,0 +1,2 @@
const { useBabelRc, override, useEslintRc } = require('customize-cra');
module.exports = override(useBabelRc());
Vendored Executable
+10
View File
@@ -0,0 +1,10 @@
declare module '*.svg' {
const value: string;
export default value;
function createRef<T>(): RefObject<T>;
interface RefObject<T> {
// immutable
readonly current: T | null;
}
}
+31
View File
@@ -0,0 +1,31 @@
module.exports = {
// The root of your source code, typically /src
// `<rootDir>` is a token Jest substitutes
roots: ['<rootDir>/src'],
modulePaths: ['<rootDir>/src/'],
// Jest transformations -- this adds support for TypeScript
// using ts-jest
transform: {
'^.+\\.tsx?$': 'ts-jest',
},
// Runs special logic, such as cleaning up components
// when using React Testing Library and adds special
// extended assertions to Jest
setupFilesAfterEnv: ['@testing-library/react/cleanup-after-each', '@testing-library/jest-dom/extend-expect'],
// Test spec file resolution pattern
// Matches parent folder `__tests__` and filename
// should contain `test` or `spec`.
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$',
modulePathIgnorePatterns: ['node_modules'],
// Module file extensions for importing
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
moduleNameMapper: {
'\\.(jpg|ico|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
'<rootDir>/__mocks__/fileMock.js',
'\\.(css|less)$': '<rootDir>/mocks/fileMock.js',
},
collectCoverage: true,
mapCoverage: true,
};
+103
View File
@@ -0,0 +1,103 @@
{
"name": "evg-box-web",
"version": "0.1.0",
"private": true,
"dependencies": {
"@react-pdf/renderer": "^2.0.13",
"@sentry/browser": "^5.25.0",
"@sentry/react": "^5.25.0",
"@testing-library/jest-dom": "^4.2.4",
"@types/flat": "^0.0.28",
"@types/jest": "^24.0.23",
"@types/lodash.debounce": "^4.0.6",
"@types/lodash.isequal": "^4.5.5",
"@types/lodash.max": "^4.0.6",
"@types/logrocket-react": "^3.0.0",
"@types/node": "12.7.4",
"@types/react": "^16.9.2",
"@types/react-datepicker": "^2.9.2",
"@types/react-dom": "16.9.0",
"@types/react-redux": "^7.1.2",
"@types/react-router-dom": "^5.1.2",
"@types/redux-mock-store": "^1.0.1",
"@types/yup": "^0.26.24",
"axios": "^0.19.0",
"axios-retry": "^3.1.2",
"babel-plugin-add-react-displayname": "^0.0.5",
"babel-plugin-react-remove-properties": "^0.3.0",
"bulma": "^0.7.5",
"bulma-steps-component": "^0.5.3",
"bulma-tooltip": "^2.0.2",
"classnames": "^2.2.6",
"date-fns": "^2.4.1",
"filesize": "^5.0.1",
"flat": "^5.0.0",
"formik": "^2.0.3",
"immer": "^5.0.0",
"jest": "^24.9.0",
"less": "^3.10.3",
"lodash.debounce": "4.0.8",
"lodash.isequal": "^4.5.0",
"lodash.max": "^4.0.1",
"logrocket": "^1.0.14",
"logrocket-react": "4",
"node-sass": "^4.12.0",
"polish-plurals": "^1.1.0",
"query-string": "^6.10.1",
"react": "^16.14.0",
"react-datepicker": "^2.9.6",
"react-dom": "^16.9.0",
"react-image-lightbox": "^5.1.0",
"react-redux": "^7.1.1",
"react-router-dom": "^5.1.2",
"react-scripts": "3.1.2",
"react-spinners": "^0.6.1",
"react-toastify": "^5.4.1",
"react-use": "^15.0.2",
"redux": "^4.0.4",
"redux-devtools-extension": "^2.13.8",
"redux-mock-store": "^1.5.3",
"reselect": "^4.0.0",
"ts-jest": "^24.2.0",
"typesafe-actions": "^5.1.0",
"typescript": "3.7.2",
"use-onclickoutside": "^0.3.1",
"yup": "^0.27.0"
},
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test --env=jsdom",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@babel/plugin-proposal-optional-chaining": "^7.6.0",
"@testing-library/react": "^9.3.2",
"@types/classnames": "^2.2.9",
"@typescript-eslint/eslint-plugin": "^2.7.0",
"@typescript-eslint/parser": "^2.7.0",
"customize-cra": "^0.9.1",
"eslint": "^6.4.0",
"eslint-config-prettier": "^6.2.0",
"eslint-plugin-prettier": "^3.1.0",
"eslint-plugin-react": "^7.14.3",
"eslint-plugin-react-hooks": "^1.6.1",
"prettier": "^1.18.2",
"react-app-rewired": "^2.1.5"
}
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+51
View File
@@ -0,0 +1,51 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Outbo"
/>
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-72520484-8"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'UA-72520484-8');
</script>
<link rel="apple-touch-icon" href="logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Outbo</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<div id="modal-root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
+25
View File
@@ -0,0 +1,25 @@
{
"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"
}
+2
View File
@@ -0,0 +1,2 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
+115
View File
@@ -0,0 +1,115 @@
import axios, { AxiosError } from 'axios';
import { history } from '../App';
import { toast } from 'react-toastify';
import store from 'store/store';
import { setProgress, clearProgress } from 'modules/progress/actions';
export const TOKEN = 'outbo-token-key';
let baseURL = process.env.REACT_APP_BACKEND_URL;
if (!baseURL) {
// throw new Error('API URL not set')
// baseURL = 'https://api.outbo.pl/';
baseURL = 'https://outbo-api.stage.ermlab.com';
// baseURL = 'https://f101aae2d6a5.ngrok.io/';
}
const api = axios.create({
baseURL,
timeout: 60000,
validateStatus: (status) => status >= 200 && status < 300,
});
api.defaults.transformResponse = [
function transformResponse(data) {
/*eslint no-param-reassign:0*/
const ISO_8601 = /(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2}.\d{3,6})[+-]\d{2}:\d{2}/;
const reviver = (key: string, value: any) => {
if (typeof value === 'string' && ISO_8601.test(value)) {
return new Date(value);
}
return value;
};
if (typeof data === 'string') {
try {
data = JSON.parse(data, reviver);
} catch (e) {
/* Ignore */
}
}
return data;
},
];
api.interceptors.request.use(
(config) => {
if (localStorage.getItem(TOKEN)) {
config.headers['Authorization'] = `Bearer ${localStorage.getItem(TOKEN)}`;
}
return config;
},
async (error) => Promise.reject(error)
);
api.interceptors.request.use(
(config) => {
// Do something before request is sent
store.dispatch(setProgress(0));
return config;
},
async (error: AxiosError) => Promise.reject(error)
);
api.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
if (error.response && [401].includes(error.response.status)) {
toast.error('Zaloguj się ponownie.');
localStorage.removeItem(TOKEN);
history.push('/login');
}
return Promise.reject(error);
}
);
//PROGRSS BAR
const calculatePercentage = (loaded: any, total: any) => Math.floor(loaded * 1.0) / total;
api.defaults.onUploadProgress = (e) => {
const percentage = calculatePercentage(e.loaded, e.total);
store.dispatch(setProgress(percentage));
};
api.interceptors.response.use(
(response) => {
store.dispatch(clearProgress());
return response;
},
async (error: AxiosError) => {
store.dispatch(clearProgress());
return Promise.reject(error);
}
);
function retryFailedRequest(err: any) {
if (err.status === 500 && err.config && !err.config.__isRetryRequest) {
err.config.__isRetryRequest = true;
return axios(err.config);
}
throw err;
}
axios.interceptors.response.use(undefined, retryFailedRequest);
// axios.interceptors.response.use(null, (error) => {
// if (error.config && error.response && error.response.status === 401) {
// return updateToken().then((token) => {
// error.config.headers.xxxx <= set the token
// return axios.request(error.onfig);
// });
// }
// return Promise.reject(error);
// });
// axiosRetry(api, { retries: 3 });
export default api;
+116
View File
@@ -0,0 +1,116 @@
import API from './axiosConfig';
import objectToFormDataConverter from 'helpers/objectToFormDataConverter';
import { BriefRequest, BriefResponse } from 'interfaces/API/products';
import { ReactText } from 'react';
const getBriefById = async (briefId: number): Promise<BriefResponse | null> => {
try {
const response = await API.get<BriefResponse>(`/briefs/${briefId}/`);
return response.data;
} catch (err) {
return null;
}
};
const patchBriefById = async (briefId: number, data: BriefRequest): Promise<BriefResponse | null> => {
try {
const { packagingType, ...rest } = data;
let formData;
if (packagingType === null) {
formData = objectToFormDataConverter(rest);
} else {
formData = objectToFormDataConverter(data);
}
const response = await API.patch<BriefResponse>(`/briefs/${briefId}/`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return response.data;
} catch (err) {
return null;
}
};
const acceptBrief = async (briefId: number): Promise<BriefResponse | null> => {
try {
const response = await API.patch<BriefResponse>(`/briefs/${briefId}/`, { accept: true });
return response.data;
} catch (err) {
return null;
}
};
const rejectBrief = async (briefId: number): Promise<BriefResponse | null> => {
try {
const response = await API.patch<BriefResponse>(`/briefs/${briefId}/`, { accept: false });
return response.data;
} catch (err) {
return null;
}
};
const publishBrief = async (briefId: number): Promise<BriefResponse | null> => {
try {
const response = await API.patch<BriefResponse>(`/briefs/${briefId}/`, { publish: true });
return response.data;
} catch (err) {
console.error(err);
return null;
}
};
const deleteBlanking = async (blankingId: number): Promise<number | null> => {
try {
const response = await API.delete<void>(`/blankings/${blankingId}/`);
return response.status;
} catch (err) {
console.error(err);
return null;
}
};
const deletePhoto = async (photoId: number): Promise<number | null> => {
try {
const response = await API.delete<void>(`/photos/${photoId}/`);
return response.status;
} catch (err) {
console.error(err);
return null;
}
};
const cloneBrief = async (copyFrom: number, briefId: number): Promise<BriefResponse | null> => {
try {
const response = await API.patch<BriefResponse>(`/briefs/${briefId}/`, { cloneFrom: copyFrom });
return response.data;
} catch (err) {
console.error(err);
return null;
}
};
const exportBriefToPdf = async (briefId: ReactText, filename: string): Promise<void | null> => {
try {
const response = await API.get(`/briefs/${briefId}/export-to-pdf/`, { responseType: 'blob' });
const file = new Blob([response.data], { type: 'application/pdf' });
const fileURL = URL.createObjectURL(file);
const link = document.createElement('a');
link.href = fileURL;
link.download = filename;
document.body.append(link);
link.click();
link.remove();
} catch (err) {
return null;
}
};
export default {
exportBriefToPdf,
cloneBrief,
deleteBlanking,
deletePhoto,
getBriefById,
acceptBrief,
rejectBrief,
patchBriefById,
publishBrief,
};
+57
View File
@@ -0,0 +1,57 @@
import API from './axiosConfig';
import { DesignStageResponse, DesignStageFileRequest, DesignStageFileResponse } from 'interfaces/API/draft';
import objectToFormDataConverter from 'helpers/objectToFormDataConverter';
import { AxiosResponse } from 'axios';
const getDraftById = async (draftId: number): Promise<DesignStageResponse | null> => {
try {
const response = await API.get<DesignStageResponse>(`/drafts/${draftId}/`);
return response.data;
} catch (err) {
return null;
}
};
const addDraftFile = async (draftId: number, data: DesignStageFileRequest): Promise<DesignStageFileResponse | null> => {
try {
const formData = objectToFormDataConverter(data);
const response = await API.post<DesignStageFileResponse>(`/drafts/${draftId}/files/`, formData);
return response.data;
} catch (err) {
return null;
}
};
const acceptDraft = async (draftId: number): Promise<DesignStageResponse | null> => {
try {
const response = await API.patch<DesignStageResponse>(`/drafts/${draftId}/`, { accept: true });
return response.data;
} catch (err) {
return null;
}
};
const openDraft = async (draftId: number) => {
try {
const response = await API.get<AxiosResponse>(`/draft-files/${draftId}/open-file/`);
return response.data;
} catch (err) {
return null;
}
};
export default {
openDraft,
getDraftById,
addDraftFile,
acceptDraft,
};
// export const fetchDrafts = async (productId: number): Promise<DraftResponse[] | []> => {
// try {
// const response = await API.get<DraftResponse[]>('');
// return response.data;
// } catch (err) {
// console.error(err);
// return [];
// }
// };
+10
View File
@@ -0,0 +1,10 @@
import API from './axiosConfig';
export const handlePDFdownload = async (url: string, filename: string): Promise<void> => {
const response = await API({ url, method: 'GET', responseType: 'blob' });
window.open(response.config.url, '_blank');
// if (response) {
// const blobUrl = URL.createObjectURL(new Blob([response.data], { type: 'application/pdf' }));
// window.open(blobUrl);
// }
};
+47
View File
@@ -0,0 +1,47 @@
import API from './axiosConfig';
import {
FinalPhotoBriefResponse,
FinalPhotoBriefRequest,
FinalPhotoBriefPhotoType,
} from 'interfaces/API/finalPhotoBrief';
import objectToFormDataConverter from 'helpers/objectToFormDataConverter';
import { FinalPhotoBrief } from 'organisms/FinalPhotoBrief/FinalPhotoBrief';
const getFinalPhotoBriefById = async (finalPhotoBriefId: number): Promise<FinalPhotoBriefResponse | null> => {
try {
const response = await API.get<FinalPhotoBriefResponse>(`/final-photobriefing/${finalPhotoBriefId}/`);
return response.data;
} catch (err) {
return null;
}
};
const removeFinalPhotoBriefPhoto = async (url: string): Promise<number | null> => {
try {
const response = await API.delete<void>(url);
return response.status;
} catch (err) {
return null;
}
};
const addFinalPhotoBriefItems = async (
data: FinalPhotoBriefRequest,
finalPhotoBriefId: number
): Promise<FinalPhotoBriefResponse | null> => {
try {
const formData = objectToFormDataConverter(data);
const response = await API.patch<FinalPhotoBriefResponse>(`/final-photobriefing/${finalPhotoBriefId}/`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return response.data;
} catch (err) {
return null;
}
};
export default {
removeFinalPhotoBriefPhoto,
getFinalPhotoBriefById,
addFinalPhotoBriefItems,
};
+60
View File
@@ -0,0 +1,60 @@
import API from './axiosConfig';
import { DesignStageResponse, DesignStageFileRequest, DesignStageFileResponse } from 'interfaces/API/draft';
import objectToFormDataConverter from 'helpers/objectToFormDataConverter';
import { AxiosResponse } from 'axios';
const getFirstFinalById = async (firstFinalId: number): Promise<DesignStageResponse | null> => {
try {
const response = await API.get<DesignStageResponse>(`/ffs/${firstFinalId}/`);
return response.data;
} catch (err) {
return null;
}
};
const addFirstFinalFile = async (
firstFinalId: number,
data: DesignStageFileRequest
): Promise<DesignStageFileResponse | null> => {
try {
const formData = objectToFormDataConverter(data);
const response = await API.post<DesignStageFileResponse>(`/ffs/${firstFinalId}/files/`, formData);
return response.data;
} catch (err) {
return null;
}
};
const acceptFirstFinalFile = async (firstFinalId: number): Promise<DesignStageResponse | null> => {
try {
const response = await API.patch<DesignStageResponse>(`/ffs/${firstFinalId}/`, { accept: true });
return response.data;
} catch (err) {
return null;
}
};
const openFirstFinal = async (draftId: number) => {
try {
const response = await API.get<AxiosResponse>(`/first-final-files/${draftId}/open-file/`);
return response.data;
} catch (err) {
return null;
}
};
export default {
openFirstFinal,
getFirstFinalById,
addFirstFinalFile,
acceptFirstFinalFile,
};
// export const fetchDrafts = async (productId: number): Promise<DraftResponse[] | []> => {
// try {
// const response = await API.get<DraftResponse[]>('');
// return response.data;
// } catch (err) {
// console.error(err);
// return [];
// }
// };
+61
View File
@@ -0,0 +1,61 @@
import API from './axiosConfig';
export enum MessageType {
USER = 'USER',
SYSTEM_SUCCESS = 'Success',
SYSTEM_FAILURE = 'Failure',
}
export type MessageActivityLogItem = {
messageType: MessageType;
message: string;
stage: string;
createdBy: number;
createdAt: Date;
};
export const fetchProductActivityLog = async (productId: number): Promise<MessageActivityLogItem[] | []> => {
try {
const response = await API.get<MessageActivityLogItem[]>(`/products/${productId}/messages/`);
return response.data;
} catch (err) {
console.error(err);
return [];
}
};
export const fetchProjectActivityLog = async (projectId: number): Promise<MessageActivityLogItem[] | []> => {
try {
const response = await API.get<MessageActivityLogItem[]>(`/projects/${projectId}/messages/`);
return response.data;
} catch (err) {
console.error(err);
return [];
}
};
export const postNewProductMessage = async (
message: string,
productId: number
): Promise<MessageActivityLogItem | null> => {
try {
const response = await API.post<MessageActivityLogItem>(`/products/${productId}/messages/`, { message });
return response.data;
} catch (err) {
console.error(err);
return null;
}
};
export const postNewProjectMessage = async (
message: string,
projectId: number
): Promise<MessageActivityLogItem | null> => {
try {
const response = await API.post<MessageActivityLogItem>(`/projects/${projectId}/messages/`, { message });
return response.data;
} catch (err) {
console.error(err);
return null;
}
};
+98
View File
@@ -0,0 +1,98 @@
import API from './axiosConfig';
import {
PhotoBriefResponse,
PhotoBriefCategoryResponse,
PhotoBriefItemRequest,
PhotoBriefItemResponse,
PhotoBriefChoiceRequest,
PhotoBriefChoiceResponse,
} from 'interfaces/API/photoBrief';
import objectToFormDataConverter from 'helpers/objectToFormDataConverter';
const getPhotoBriefById = async (photoBriefId: number): Promise<PhotoBriefResponse | null> => {
try {
const response = await API.get<PhotoBriefResponse>(`/photobriefing/${photoBriefId}/`);
return response.data;
} catch (err) {
return null;
}
};
const getPhotoBriefCategories = async (): Promise<PhotoBriefCategoryResponse[]> => {
try {
const response = await API.get<PhotoBriefCategoryResponse[]>('/photobriefing-categories/');
return response.data;
} catch (err) {
console.error(err);
return [];
}
};
const addPhotoBriefItem = async (
data: PhotoBriefItemRequest,
photoBriefId: number
): Promise<PhotoBriefItemResponse | null> => {
try {
const formData = objectToFormDataConverter(data);
const response = await API.post<PhotoBriefItemResponse>(`/photobriefing/${photoBriefId}/items/`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return response.data;
} catch (err) {
console.error(err);
return null;
}
};
const acceptPhotoBriefItem = async (
data: PhotoBriefChoiceRequest,
photoBriefId: number
): Promise<PhotoBriefResponse | null> => {
try {
const response = await API.patch<PhotoBriefResponse>(`/photobriefing/${photoBriefId}/`, { choice: data });
return response.data;
} catch (err) {
console.error(err);
return null;
}
};
const publishPhotoBrief = async (photoBriefId: number): Promise<PhotoBriefResponse | null> => {
try {
const response = await API.patch<PhotoBriefResponse>(`/photobriefing/${photoBriefId}/`, { publish: true });
return response.data;
} catch (err) {
console.error(err);
return null;
}
};
const deletePhotoBriefItem = async (itemId: number): Promise<number | null> => {
try {
const response = await API.delete<number>(`/photobriefing-items/${itemId}/`);
return response.status;
} catch (err) {
console.error(err);
return null;
}
};
const finishPhotoBrief = async (photoBriefId: number): Promise<PhotoBriefResponse | null> => {
try {
const response = await API.patch<PhotoBriefResponse>(`/photobriefing/${photoBriefId}/`, { complete: true });
return response.data;
} catch (err) {
console.error(err);
return null;
}
};
export default {
finishPhotoBrief,
deletePhotoBriefItem,
publishPhotoBrief,
acceptPhotoBriefItem,
addPhotoBriefItem,
getPhotoBriefById,
getPhotoBriefCategories,
};
+60
View File
@@ -0,0 +1,60 @@
import API from './axiosConfig';
import { DesignStageResponse, DesignStageFileRequest, DesignStageFileResponse } from 'interfaces/API/draft';
import objectToFormDataConverter from 'helpers/objectToFormDataConverter';
import { AxiosResponse } from 'axios';
const getPrintingFileById = async (printingFileId: number): Promise<DesignStageResponse | null> => {
try {
const response = await API.get<DesignStageResponse>(`/pfs/${printingFileId}/`);
return response.data;
} catch (err) {
return null;
}
};
const addPrintingFilesFile = async (
printingFileId: number,
data: DesignStageFileRequest
): Promise<DesignStageFileResponse | null> => {
try {
const formData = objectToFormDataConverter(data);
const response = await API.post<DesignStageFileResponse>(`/pfs/${printingFileId}/files/`, formData);
return response.data;
} catch (err) {
return null;
}
};
const acceptPrintingFilesFile = async (printingFileId: number): Promise<DesignStageResponse | null> => {
try {
const response = await API.patch<DesignStageResponse>(`/pfs/${printingFileId}/`, { accept: true });
return response.data;
} catch (err) {
return null;
}
};
const openPrintingFile = async (pringintFileId: number) => {
try {
const response = await API.get<AxiosResponse>(`/printing-files-files/${pringintFileId}/open-file/`);
return response.data;
} catch (err) {
return null;
}
};
export default {
openPrintingFile,
getPrintingFileById,
addPrintingFilesFile,
acceptPrintingFilesFile,
};
// export const fetchDrafts = async (productId: number): Promise<DraftResponse[] | []> => {
// try {
// const response = await API.get<DraftResponse[]>('');
// return response.data;
// } catch (err) {
// console.error(err);
// return [];
// }
// };
+49
View File
@@ -0,0 +1,49 @@
import API from './axiosConfig';
import { ProductResponse, SimpleProductResponse, AnyProductStore } from 'interfaces/API/products';
export enum MessageType {
USER = 'USER',
SYSTEM_SUCCESS = 'SYSTEM_SUCCESS',
SYSTEM_FAILURE = 'SYSTEM_FAILURE',
}
export type ProductActivityLogItem = {
messageType: MessageType;
message: string;
stage: string;
createdBy: number;
createdAt: Date;
};
const getProducts = async (): Promise<SimpleProductResponse[] | null> => {
try {
const response = await API.get<SimpleProductResponse[]>('/products/');
return response.data;
} catch (err) {
return null;
}
};
//TODO: USED??
const getProductById = async (id: number): Promise<ProductResponse | null> => {
try {
const response = await API.get<ProductResponse>(`/products/${id}/`);
return response.data;
} catch (err) {
return null;
}
};
//TODO: USED??
const getProductsByURL = async (url: string): Promise<AnyProductStore[] | null> => {
try {
const response = await API.get<AnyProductStore[]>(url);
return response.data;
} catch (err) {
return null;
}
};
export default {
getProductById,
getProducts,
getProductsByURL,
};
+75
View File
@@ -0,0 +1,75 @@
import API from './axiosConfig';
import { ProductStage, ProductStageGroup } from 'interfaces/API/projects';
export interface ProductStageResponse {
id: ProductStage;
label: string;
group: ProductStageGroup;
description: string;
}
export interface DropdownItem {
id: number;
name: string;
}
export type PackagingType = {
value: number;
label: string;
};
export type Brand = {
id: number;
name: string;
};
export type Category = {
id: number;
name: string;
};
const fetchCategories = async (): Promise<Category[]> => {
try {
const response = await API.get<Category[]>('/categories/');
return response.data;
} catch (err) {
console.error(err);
return [];
}
};
const fetchBrands = async (): Promise<Brand[]> => {
try {
const response = await API.get<Brand[]>('/brands/');
return response.data;
} catch (err) {
console.error(err);
return [];
}
};
const fetchPackagingTypes = async (): Promise<PackagingType[]> => {
try {
const response = await API.get<PackagingType[]>('/packaging-types/');
return response.data;
} catch (err) {
console.error(err);
return [];
}
};
const fetchProductStages = async (): Promise<ProductStageResponse[]> => {
try {
const response = await API.get<ProductStageResponse[]>('/stages/');
return response.data;
} catch (err) {
console.error(err);
return [];
}
};
export default {
fetchCategories,
fetchBrands,
fetchPackagingTypes,
fetchProductStages,
};
+56
View File
@@ -0,0 +1,56 @@
import API from './axiosConfig';
import { ProjectResponse, ProjectRequest, SimpleProject } from 'interfaces/API/projects';
import { toast } from 'react-toastify';
import { ProjectsQueryParams } from '../interfaces/API/projects';
type RequireAtLeastOne<T, Keys extends keyof T = keyof T> = Pick<T, Exclude<keyof T, Keys>> &
{
[K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>>;
}[Keys];
const getProjects = async (params: RequireAtLeastOne<ProjectsQueryParams>): Promise<SimpleProject[] | null> => {
try {
const response = await API.get<ProjectResponse[]>('/projects/', { params });
return response.data;
} catch (err) {
toast.error('Nie udało się pobrać listy projektów.');
return null;
}
};
const getProjectById = async (projectId: number): Promise<ProjectResponse | null> => {
try {
const response = await API.get<ProjectResponse>(`projects/${projectId}/`);
return response.data;
} catch (err) {
toast.error('Wystąpił problem podczas pobierania projektu.');
return null;
}
};
const createNewProject = async (data: ProjectRequest): Promise<ProjectResponse | null> => {
try {
const response = await API.post('/projects/', data);
return response.data;
} catch (err) {
toast.error('Wystąpił problem podczas tworzenia projektu.');
return null;
}
};
const publishProject = async (projectId: number): Promise<ProjectResponse | null> => {
try {
const response = await API.patch<ProjectResponse>(`/projects/${projectId}/`, { publish: true });
return response.data;
} catch (err) {
toast.error('Publikacja projektu nie powiodła się');
return null;
}
};
export default {
getProjects,
getProjectById,
createNewProject,
publishProject,
};
+689
View File
@@ -0,0 +1,689 @@
import API from './axiosConfig';
export enum ProductStageGroup {
INVALID = 0,
GROUP_1 = 1,
GROUP_2 = 2,
GROUP_3 = 3,
GROUP_4 = 4,
GROUP_5 = 5,
}
export enum ProductStage {
STAGE_1 = 'waiting_for_brief',
STAGE_1A = 'waiting_for_brief_accept_by_agency',
STAGE_1B = 'waiting_for_brief_accept_by_client',
STAGE_2A = 'waiting_for_draft',
STAGE_2B = 'waiting_for_draft_accept_by_supplier',
STAGE_2C = 'waiting_for_draft_accept_by_client',
STAGE_3A = 'waiting_for_ff',
STAGE_3B = 'waiting_for_ff_accept_by_supplier',
STAGE_3C = 'waiting_for_ff_accept_by_client',
STAGE_4A = 'waiting_for_pf',
STAGE_4B = 'waiting_for_pf_accept_by_supplier',
STAGE_4C = 'waiting_for_pf_accept_by_client',
STAGE_5 = 'completed',
}
export enum PhotoBriefStage {
PUBLISHED = 'published',
COMPLETED = 'completed',
CREATED = 'created',
}
export enum PhotoBriefPhotoType {
ARRANGEMENT_PHOTO = 'arrangement_photo',
PACKSHOT = 'packshot',
CLIPPING = 'clipping',
EXTRA_PHOTO = 'extra_photo',
}
export enum PhotoBriefCategory {
MODELS = 'models',
STYLING = 'styling',
SCENERY = 'scenery',
}
export type PhotoBriefRequest = {
sessionDate: string;
conceptDescription: string;
};
export type PhotoBriefResponse = {
id: number;
sessionDate: string;
conceptDescription: string;
stage: PhotoBriefStage;
items: PhotoBriefItemResponse[];
photos: PhotoBriefPhotoResponse[];
acceptable: boolean;
editable: boolean;
};
export type PhotoBriefItemRequest = {
category: PhotoBriefCategory;
title: string;
description: string;
};
export type PhotoBriefItemResponse = {
id: number;
category: PhotoBriefCategory;
photos: PhotoBriefItemPhotoResponse[];
title: string;
description: string;
isAcceptedByClient: boolean;
isAcceptedBySupplier: boolean;
editable: boolean;
};
export type PhotoBriefPhotoRequest = {
file: File;
type: PhotoBriefPhotoType;
};
export type PhotoBriefPhotoResponse = {
url: string;
type: PhotoBriefPhotoType;
filename: string;
};
export type PhotoBriefItemPhotoRequest = {
file: File;
};
export type PhotoBriefItemPhotoResponse = {
url: string;
filename: string;
};
export interface Photo {
file?: File;
id?: number;
url: string;
filename: string;
}
export interface PhotoResponse {
url: string;
id: number;
filename: string;
}
export interface PhotoRequest {
file: File;
}
export interface Blanking {
file?: File;
filename: string;
size: string;
url: string;
id?: number;
}
export interface BlankingRequest {
file: File;
}
export interface BlankingResponse {
url: string;
id: number;
}
export type PrintingFile = {
id?: number;
file?: File;
url: string;
uploadedAt: Date;
uploadedBy: number;
filename: string;
size: string;
description: string;
version: string;
};
export type PrintingFileResponse = {
id: number;
url: string;
uploadedAt: Date;
uploadedBy: number;
description: string;
version: string;
filename: string;
size: string;
};
export type FileListItemProps = {
id?: number;
file?: File;
url: string;
uploadedAt: Date;
uploadedBy: number;
filename: string;
size: string;
description: string;
version: string;
};
export type PrintingFileRequest = {
file: File;
description: string;
};
export type FirstFinalFile = {
id?: number;
file?: File;
url: string;
uploadedAt: Date;
uploadedBy: number;
filename: string;
size: string;
description: string;
version: string;
};
export type FirstFinalFileResponse = {
id: number;
url: string;
uploadedAt: Date;
uploadedBy: number;
description: string;
version: string;
filename: string;
size: string;
};
export type FirstFinalFileRequest = {
file: File;
description: string;
};
export type DraftFile = {
id?: number;
file?: File;
url: string;
uploadedAt: Date;
uploadedBy: number;
filename: string;
size: string;
description: string;
version: string;
};
export type DraftFileResponse = {
id: number;
url: string;
uploadedAt: Date;
uploadedBy: number;
description: string;
version: string;
filename: string;
size: string;
};
export type ProductFileStageFileRequest = {
file: File;
description: string;
};
export type DraftFileRequest = {
file: File;
description: string;
};
// export type Photo = PhotoRequest | PhotoResponse;
// export type Blanking = BlankingRequest | BlankingResponse;
export interface ProductRequest {
productNamePol: string;
productNameEng: string;
modelNo: string;
batchNo: string;
ean: string;
mainDescription: string;
extraDescription: string;
material: string;
cleaningMethod: string;
countryOfOrigin: string;
warning: string;
arrangementPhotos: string;
packshot: string;
extraPhoto: string;
clippings: string;
illustration: string;
manual: string;
packagingType: number | null;
hasPhotos: boolean;
hasIllustrations: boolean;
isCleaningMethod: boolean;
isClipping: boolean;
certificate: string;
capabilitiesIcons: string;
productIcons: string;
informationSigns: string;
photos: Photo[];
blankings: Blanking[];
isCountryOfOrigin: boolean;
isDescriptionEng: boolean;
isDescriptionPol: boolean;
isDescriptionPor: boolean;
isDescriptionRus: boolean;
isExtraPhoto: boolean;
isManual: boolean;
isMaterial: boolean;
isPackshot: boolean;
isWarnings: boolean;
}
export interface ProductResponse {
acceptable: boolean;
editable: boolean;
publishable: boolean;
id: number;
productNamePol: string;
productNameEng: string;
modelNo: string;
batchNo: string;
ean: string;
mainDescription: string;
extraDescription: string;
material: string;
cleaningMethod: string;
countryOfOrigin: string;
warning: string;
arrangementPhotos: string;
packshot: string;
extraPhoto: string;
clippings: string;
illustration: string;
manual: string;
stage: ProductStage;
photos: Photo[];
blankings: Blanking[];
draftFiles: DraftFile[];
firstFinalFiles: FirstFinalFile[];
printingFiles: PrintingFile[];
photoBrief: PhotoBriefResponse;
packagingType: number | null;
certificate: string;
capabilitiesIcons: string;
productIcons: string;
informationSigns: string;
hasPhotos: boolean;
hasIllustrations: boolean;
isCleaningMethod: boolean;
isClipping: boolean;
isCountryOfOrigin: boolean;
isDescriptionEng: boolean;
isDescriptionPol: boolean;
isDescriptionPor: boolean;
isDescriptionRus: boolean;
isExtraPhoto: boolean;
isManual: boolean;
isMaterial: boolean;
isPackshot: boolean;
isWarnings: boolean;
}
export interface ProjectResponse {
maxProductStage: string;
isPublished: boolean;
publishable: boolean;
slug: string;
name: string;
description: string;
products: ProductResponse[];
category: number;
brand: number;
printingFilesDate: string;
promoDate: string;
id: number;
}
export interface ProjectRequest {
name: string;
description: string;
category: number | null;
brand: number | null;
productsAmount: number;
printingFilesDate: string;
promoDate: string;
}
export const fetchProjects = async (): Promise<ProjectResponse[] | null> => {
try {
const response = await API.get<ProjectResponse[]>('/projects/');
return response.data;
} catch (err) {
console.error(err);
return null;
}
};
export const fetchProjectById = async (projectId: number) => {
try {
const response = await API.get<ProjectResponse>(`/projects/${projectId}/`);
return response.data;
} catch (err) {
console.error(err);
return null;
}
};
export const postNewProject = async (data: ProjectRequest): Promise<ProjectResponse | null> => {
try {
const response = await API.post<ProjectResponse>('/projects/', data);
return response.data;
} catch (err) {
console.error(err);
return null;
}
};
const uploadProductPhoto = async (id: number, photo: PhotoRequest): Promise<void | null> => {
try {
const data = new FormData();
data.append('file', photo.file);
const response = await API.post<void>(`products/${id}/photos/`, data, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return response.data;
} catch (err) {
console.error(err);
return null;
}
};
export const removeProductImage = async (photoId: number): Promise<boolean> => {
try {
await API.delete<void>(`/photos/${photoId}/`);
return true;
} catch (err) {
console.error(err);
return false;
}
};
const uploadProductBlanking = async (productId: number, blanking: BlankingRequest): Promise<void | null> => {
try {
const data = new FormData();
data.append('file', blanking.file);
const response = await API.post<void>(`products/${productId}/blankings/`, data, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return response.data;
} catch (err) {
console.error(err);
return null;
}
};
export const removeProductBlanking = async (blankingId: number): Promise<boolean> => {
try {
await API.delete<void>(`/blankings/${blankingId}/`);
return true;
} catch (err) {
return false;
}
};
export const patchProduct = async (id: number, data: Partial<ProductRequest>): Promise<ProductResponse | null> => {
try {
const { photos, blankings, ...values } = data;
if (photos && photos.length > 0) {
// TODO: investigate & refactor
await photos
.filter((photo) => 'file' in photo && photo.file !== undefined)
.map(({ file }) => ({ file } as PhotoRequest))
.reduce(async (previous, photo) => {
await previous;
return uploadProductPhoto(id, photo);
}, Promise.resolve<void | null>(null));
}
if (blankings && blankings.length > 0) {
// TODO: investigate & refactor
await blankings
.filter((blanking) => 'file' in blanking)
.map(({ file }) => ({ file } as BlankingRequest))
.reduce(async (previous, blanking) => {
await previous;
return uploadProductBlanking(id, blanking);
}, Promise.resolve<void | null>(null));
}
const response = await API.patch<ProductResponse>(`/products/${id}/`, values);
return response.data;
} catch (err) {
console.error(err);
return null;
}
};
export const approveProduct = async (id: number): Promise<ProductResponse | null> => {
try {
const response = await API.patch<ProductResponse>(`/products/${id}/`, { accept: true });
return response.data;
} catch (err) {
console.error(err);
return null;
}
};
export const declineProduct = async (id: number): Promise<ProductResponse | null> => {
try {
const response = await API.patch<ProductResponse>(`/products/${id}/`, { accept: false });
return response.data;
} catch (err) {
console.error(err);
return null;
}
};
export const approveProject = async (projectId: number): Promise<ProjectResponse | null> => {
try {
const response = await API.patch<ProjectResponse>(`/projects/${projectId}/`, { publish: true });
return response.data;
} catch (err) {
console.error(err);
return null;
}
};
export const announceProduct = async (id: number): Promise<ProductResponse | null> => {
try {
const response = await API.patch<ProductResponse>(`/products/${id}/`, { publish: true });
return response.data;
} catch (err) {
console.error(err);
return null;
}
};
export const postDraft = async (draftId: number, draft: DraftFileRequest): Promise<DraftFileResponse | null> => {
try {
const data = new FormData();
data.append('file', draft.file);
data.append('description', draft.description);
const response = await API.patch<DraftFileResponse>(`/drafts/${draftId}/`, data, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return response.data;
} catch (err) {
console.error(err);
return null;
}
};
export const approveDraft = async (productId: number): Promise<ProductResponse | null> => {
try {
const response = await API.patch<ProductResponse>(`/products/${productId}/`, { accept: true });
return response.data;
} catch (err) {
console.error(err);
return null;
}
};
export const fetchProductById = async (productId: number): Promise<ProductResponse | null> => {
try {
const response = await API.get<ProductResponse>(`/products/${productId}/`);
return response.data;
} catch (err) {
console.error(err);
return null;
}
};
export const patchPhotoBrief = async (
productId: number,
photoBrief: PhotoBriefRequest
): Promise<PhotoBriefResponse | null> => {
try {
const response = await API.patch<PhotoBriefResponse>(`/products/${productId}/photo-briefing/`, photoBrief);
return response.data;
} catch (err) {
console.error(err);
return null;
}
};
export const approvePhotoBriefItem = async (
photoBriefId: number,
category: PhotoBriefCategory,
id: number | null
): Promise<PhotoBriefResponse | null> => {
try {
const response = await API.patch<PhotoBriefResponse>(`/photo-briefing/${photoBriefId}/items/`, {
category,
accept: id,
});
return response.data;
} catch (err) {
console.error(err);
return null;
}
};
export const approvePhotoBrief = async (productId: number) => {
try {
const response = await API.patch<PhotoBriefResponse>(`/products/${productId}/photo-briefing/`, {
accept: true,
});
return response.data;
} catch (err) {
console.error(err);
return null;
}
};
export const patchPublishPhotoBrief = async (productId: number): Promise<PhotoBriefResponse | null> => {
try {
const response = await API.patch<PhotoBriefResponse>(`/products/${productId}/photo-briefing/`, { publish: true });
return response.data;
} catch (err) {
console.error(err);
return null;
}
};
export const postPhotoBriefItem = async (
photoBriefId: number,
photoBriefItem: PhotoBriefItemRequest
): Promise<PhotoBriefItemResponse | null> => {
try {
const response = await API.post<PhotoBriefItemResponse>(`/photo-briefing/${photoBriefId}/items/`, photoBriefItem);
return response.data;
} catch (err) {
console.error(err);
return null;
}
};
export const postPhotoBriefItemPhoto = async (
itemId: number,
photo: PhotoBriefItemPhotoRequest
): Promise<PhotoBriefItemResponse | null> => {
try {
const data = new FormData();
data.append('file', photo.file);
const response = await API.post<PhotoBriefItemResponse>(`/photo-briefing-items/${itemId}/photos/`, data);
return response.data;
} catch (err) {
console.error(err);
return null;
}
};
// export const getProjectBrands = async (): Promise<ProjectResponse[]> => {};
export const postFirstFinal = async (
productId: number,
draft: FirstFinalFileRequest
): Promise<FirstFinalFileResponse | null> => {
try {
const data = new FormData();
data.append('file', draft.file);
data.append('description', draft.description);
const response = await API.post<FirstFinalFileResponse>(`/products/${productId}/first-final-files/`, data, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return response.data;
} catch (err) {
console.error(err);
return null;
}
};
export const approveFirstFinal = async (productId: number): Promise<ProductResponse | null> => {
try {
const response = await API.patch<ProductResponse>(`/products/${productId}/`, { accept: true });
return response.data;
} catch (err) {
console.error(err);
return null;
}
};
export const postPrintingFile = async (
productId: number,
draft: PrintingFileRequest
): Promise<PrintingFileResponse | null> => {
try {
const data = new FormData();
data.append('file', draft.file);
data.append('description', draft.description);
const response = await API.post<PrintingFileResponse>(`/products/${productId}/printing-files/`, data, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return response.data;
} catch (err) {
console.error(err);
return null;
}
};
export const approvePrintingFile = async (productId: number): Promise<ProductResponse | null> => {
try {
const response = await API.patch<ProductResponse>(`/products/${productId}/`, { accept: true });
return response.data;
} catch (err) {
console.error(err);
return null;
}
};
export const postPhotoBriefPhoto = async (
photoBriefId: number,
photoBriefPhoto: PhotoBriefPhotoRequest
): Promise<PhotoBriefPhotoResponse | null> => {
try {
const data = new FormData();
data.append('file', photoBriefPhoto.file);
data.append('type', photoBriefPhoto.type);
const response = await API.post<PhotoBriefPhotoResponse>(`/photo-briefing/${photoBriefId}/photos/`, data, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return response.data;
} catch (err) {
console.error(err);
return null;
}
};
+63
View File
@@ -0,0 +1,63 @@
import API, { TOKEN } from './axiosConfig';
import { history } from '../App';
import { toast } from 'react-toastify';
import {
AuthorizationRequest,
UserDataResponse,
AuthorizationResponse,
UserListItem,
} from 'interfaces/API/userAuthorization';
const userAuthorization = async ({ username, password }: AuthorizationRequest): Promise<boolean> => {
try {
const response = await API.post<AuthorizationResponse>('/token/', { username, password });
const token = response.data.token;
localStorage.setItem(TOKEN, token);
return true;
} catch (err) {
if (err.response) {
toast.error(err.response.data.nonFieldErrors[0]);
} else {
toast.error('Wystąpił problem. Sprobuj ponownie.');
}
return false;
}
};
const fetchCurrentUser = async (): Promise<UserDataResponse | null> => {
try {
const response = await API.get<UserDataResponse>('/users/me/');
return response.data;
} catch (err) {
console.error(err);
return null;
}
};
const logout = async (): Promise<boolean> => {
try {
localStorage.removeItem(TOKEN);
history.push('/login');
return await Promise.resolve(true);
} catch (err) {
console.error(err);
return await Promise.resolve(false);
}
};
const fetchAllUsers = async (): Promise<UserListItem[] | []> => {
try {
const response = await API.get<UserListItem[]>('/users/');
return response.data;
} catch (err) {
console.error(err);
return [];
}
};
export default {
userAuthorization,
fetchCurrentUser,
logout,
fetchAllUsers,
};
+1067
View File
File diff suppressed because it is too large. Load diff
+35
View File
@@ -0,0 +1,35 @@
import React from 'react';
import { Provider } from 'react-redux';
import store from './store/store';
import './App.scss';
import { Switch, Router, Redirect } from 'react-router-dom';
import { Login, Main } from 'containers';
import { PrivateRoute, NotAuthorizedRoute } from 'helpers/PrivateRoute';
import { createBrowserHistory } from 'history';
import 'react-image-lightbox/style.css';
import { ToastContainer, toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
export const history = createBrowserHistory();
const App: React.FC = () => {
return (
<Router history={history}>
<Provider store={store}>
<Switch>
<NotAuthorizedRoute exact path="/login">
<ToastContainer position={toast.POSITION.BOTTOM_LEFT} autoClose={5000} />
<Login />
</NotAuthorizedRoute>
<PrivateRoute path={['/projects/:id/products/:pid', '/projects/new', '/projects/:id', '/']}>
<ToastContainer position={toast.POSITION.BOTTOM_RIGHT} autoClose={5000} />
<Main />
</PrivateRoute>
<Redirect to="/" />
</Switch>
</Provider>
</Router>
);
};
export default App;
+9
View File
@@ -0,0 +1,9 @@
const getProductById = jest.fn();
const getProducts = jest.fn();
const getProductsByURL = jest.fn();
export default {
getProductById,
getProducts,
getProductsByURL,
};
+11
View File
@@ -0,0 +1,11 @@
const getProjects = jest.fn();
const getProjectById = jest.fn();
const createNewProject = jest.fn();
const publishProject = jest.fn();
export default {
getProjects,
getProjectById,
createNewProject,
publishProject,
};
+7
View File
@@ -0,0 +1,7 @@
const userAuthorization = jest.fn().mockReturnValue(true);
const fetchCurrentUser = jest.fn();
export default {
userAuthorization,
fetchCurrentUser,
};
+1
View File
@@ -0,0 +1 @@
export default '';
+3
View File
@@ -0,0 +1,3 @@
const useCurrentBrief = jest.fn();
export default useCurrentBrief;
+3
View File
@@ -0,0 +1,3 @@
const useCurrentProject = jest.fn();
export default useCurrentProject;
+3
View File
@@ -0,0 +1,3 @@
const useNumberParams = jest.fn();
export default useNumberParams;
+9
View File
@@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from '../App';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<App />, div);
ReactDOM.unmountComponentAtNode(div);
});
+210
View File
@@ -0,0 +1,210 @@
import React from 'react';
import { cleanup, render, waitForElement, act, fireEvent } from '@testing-library/react';
import LeftSidebar from 'organisms/LeftSidebar';
import { createMemoryHistory, createLocation } from 'history';
import configureStore from 'redux-mock-store'; //ES6 modules
import { ProductStage, AnyProject } from 'interfaces/API/projects';
import { Provider } from 'react-redux';
import { Router, Route, match } from 'react-router-dom';
import { ProductStore } from 'interfaces/API/products';
import { ProductStageResponse, Category, Brand } from 'API/projectData';
import '@testing-library/jest-dom/extend-expect';
afterEach(cleanup);
const category: Category = {
id: 1,
name: 'toys',
};
const brand: Brand = {
id: 1,
name: 'boys',
};
const productStage: ProductStageResponse = {
id: ProductStage.STAGE_1,
label: '1',
group: 1,
description: 'Briefing',
};
const printingFilesDate = '2011-02-01';
const promoDate = '2011-01-01';
const renderWithProviderAndRoute = (ui: any, { routeOptions, history, store }: any): any => {
return {
...render(
<Provider store={store}>
<Router history={history}>
<Route {...routeOptions}>{ui}</Route>
</Router>
</Provider>
),
};
};
const createMockStore = (initialProjectValues: AnyProject[], initialProductValues: ProductStore) => {
const mockStore = configureStore();
const store = mockStore({
brief: { briefs: [] },
projects: {
projectsList: initialProjectValues,
},
products: {
currentStage: 0,
productsList: [{ ...initialProductValues }],
},
users: {
currentUser: {
id: 1,
organization: {
type: 'supplier',
},
},
usersList: [],
},
activityLog: { messages: [] },
drafts: { draftList: [] },
firstFinals: { firstFinalList: [] },
printingFiles: { printingFilesList: [] },
progress: { progress: null },
projectData: {
categories: [category],
brands: [brand],
productStages: [productStage],
packagingTypes: [],
},
});
return store;
};
const routeConfiguration = (path: string, params?: any) => {
const history = createMemoryHistory({ initialEntries: [path] });
const match: match = {
isExact: true,
path,
url: path,
params: {},
};
const location = createLocation(match.url);
return { history, match, location };
};
describe('LeftSidebar', () => {
const products: ProductStore = {
id: 1,
displayName: 'test / test',
project: 1,
stage: ProductStage.STAGE_1,
brief: '',
draft: '',
firstFinal: '',
printingFiles: '',
createdAt: new Date(),
updatedAt: new Date(),
};
const projects: AnyProject[] = [
{
id: 1,
slug: 'firstProjectSlug',
name: 'firstProjectName',
description: 'firstProjectDesc',
printingFilesDate: printingFilesDate,
productsAmount: 1,
promoDate: promoDate,
products: 'test',
maxProductStage: ProductStage.STAGE_1A,
publishable: false,
published: true,
brand: brand.id,
category: brand.id,
createdAt: new Date(),
updatedAt: new Date(),
},
];
const path = '/';
let { history, match, location } = routeConfiguration(path);
afterEach(() => (history = createMemoryHistory({ initialEntries: [path] })));
const store = createMockStore(projects, products);
it('should render without crash', () => {
renderWithProviderAndRoute(<LeftSidebar />, {
routeOptions: { exact: false, path },
history,
store,
});
});
it('should render project', async () => {
const { findByTestId } = renderWithProviderAndRoute(<LeftSidebar />, {
routeOptions: { exact: false, path },
history,
store,
});
const projectsList = await waitForElement(() => findByTestId('leftSidebarProjects'));
expect(projectsList.children.length).toBe(projects.length);
});
it('should render project properties', async () => {
const { getByText } = renderWithProviderAndRoute(<LeftSidebar />, {
routeOptions: { exact: false, path },
history,
store,
});
// const projects = await waitForElement(() => findByTestId('leftSidebarProjects'));
//FIND PROJECT SLUG
expect(getByText(/firstProjectSlug/i)).toBeInTheDocument();
//FIND PROJECT NAMES
expect(getByText(/FirstProjectName/i)).toBeInTheDocument();
});
it('should render product properties', async () => {
const { getByText } = renderWithProviderAndRoute(<LeftSidebar />, {
routeOptions: { exact: true, path },
history,
store,
});
const stamp = getByText(/1/i);
expect(getByText(/test \/ test/i)).toBeInTheDocument();
expect(stamp.parentElement).toHaveClass('is-background-stage-1');
});
it('should collapse product list', async () => {
const { getByTestId, queryByText } = renderWithProviderAndRoute(<LeftSidebar />, {
routeOptions: { exact: false, path },
history,
store,
});
const leftSidebarCaret = getByTestId('leftSidebarCaret');
expect(leftSidebarCaret).toHaveClass('caret--opened');
await act(async () => {
fireEvent.click(leftSidebarCaret);
});
expect(leftSidebarCaret).toHaveClass('caret--closed');
expect(leftSidebarCaret).not.toHaveClass('caret--opened');
expect(queryByText(/test \/ test/i)).not.toBeInTheDocument();
});
it('should redirect to project on project click', async () => {
const { getByText } = renderWithProviderAndRoute(<LeftSidebar />, {
routeOptions: { exact: false, path },
history,
store,
});
const projectSlug = getByText(/firstProjectSlug/i);
await act(async () => {
fireEvent.click(projectSlug);
});
expect(history.location.pathname).toBe('/projects/1');
});
it('should redirect to product on product click', async () => {
const { getByText } = renderWithProviderAndRoute(<LeftSidebar />, {
routeOptions: { exact: false, path },
history,
store,
});
const productName = getByText(/test \/ test/i);
await act(async () => {
fireEvent.click(productName);
});
expect(history.location.pathname).toBe('/projects/1/products/1');
});
});
+109
View File
@@ -0,0 +1,109 @@
import React from 'react';
import { createMemoryHistory } from 'history';
import LoginForm from 'organisms/LoginForm';
import '@testing-library/jest-dom/extend-expect';
import { render, cleanup, waitForElement, fireEvent, act, wait } from '@testing-library/react';
import userApi from 'API/userAuthorization';
afterEach(cleanup);
describe('Login', () => {
let history = createMemoryHistory({ initialEntries: ['/login'] });
afterEach(() => (history = createMemoryHistory({ initialEntries: ['/login'] })));
const dispatch = jest.fn();
it('renders login form properly', async () => {
const { findByTestId } = render(<LoginForm history={history} dispatch={dispatch} />);
const passwordField = await waitForElement(() => findByTestId('password-field'));
const usernameField = await waitForElement(() => findByTestId('username-field'));
const submitButton = await waitForElement(() => findByTestId('submit-button'));
expect(submitButton.firstChild?.textContent).toBe('Zaloguj');
expect(passwordField).toHaveValue('');
expect(usernameField).toHaveValue('');
});
it('should display a blank login form', async () => {
const { findByTestId } = render(<LoginForm history={history} dispatch={dispatch} />);
const loginForm = await findByTestId('login-form');
expect(loginForm).toHaveFormValues({
username: '',
password: '',
});
});
it('should allow entering username and password', async () => {
const { findByTestId } = render(<LoginForm history={history} dispatch={dispatch} />);
const passwordField = await waitForElement(() => findByTestId('password-field'));
const usernameField = await waitForElement(() => findByTestId('username-field'));
await act(async () => {
fireEvent.change(passwordField, { target: { value: 'password' } });
fireEvent.change(usernameField, { target: { value: 'username' } });
});
expect(passwordField).toHaveValue('password');
expect(usernameField).toHaveValue('username');
});
it('should not submit blank form', async () => {
const { findByTestId, getByText } = render(<LoginForm history={history} dispatch={dispatch} />);
const loginForm = await findByTestId('login-form');
fireEvent.submit(loginForm);
const usernameError = await waitForElement(() => getByText('Podaj nazwę użytkownika.'));
const passwordError = await waitForElement(() => getByText('Podaj hasło.'));
expect(usernameError.innerHTML).toEqual('Podaj nazwę użytkownika.');
expect(passwordError.innerHTML).toEqual('Podaj hasło.');
});
it('should submit form with given values and redirects', async () => {
const { findByTestId, getByText } = render(<LoginForm history={history} dispatch={dispatch} />);
const passwordField = await waitForElement(() => findByTestId('password-field'));
const usernameField = await waitForElement(() => findByTestId('username-field'));
const loginForm = await findByTestId('login-form');
await act(async () => {
fireEvent.change(passwordField, { target: { value: 'password' } });
fireEvent.change(usernameField, { target: { value: 'username' } });
});
await act(async () => {
fireEvent.submit(loginForm);
});
expect(userApi.userAuthorization).toHaveBeenCalledTimes(1);
expect(history.location.pathname).toBe('/');
});
it('should display spinner on submitting form', async () => {
const { findByTestId } = render(<LoginForm history={history} dispatch={dispatch} />);
const submitButton = await waitForElement(() => findByTestId('submit-button'));
const passwordField = await waitForElement(() => findByTestId('password-field'));
const usernameField = await waitForElement(() => findByTestId('username-field'));
const loginForm = await findByTestId('login-form');
await act(async () => {
fireEvent.change(usernameField, { target: { value: 'username' } });
});
await act(async () => {
fireEvent.change(passwordField, { target: { value: 'password' } });
});
fireEvent.submit(loginForm);
expect(submitButton).toHaveClass('is-loading');
await wait(() => expect(submitButton).not.toHaveClass('is-loading'));
});
it('should not redirect on invalid data', async () => {
userApi.userAuthorization.mockReturnValueOnce(false);
const { findByTestId } = render(<LoginForm history={history} dispatch={dispatch} />);
const passwordField = await waitForElement(() => findByTestId('password-field'));
const usernameField = await waitForElement(() => findByTestId('username-field'));
const loginForm = await findByTestId('login-form');
await act(async () => {
fireEvent.change(passwordField, { target: { value: 'password' } });
});
await act(async () => {
fireEvent.change(usernameField, { target: { value: 'username' } });
});
await act(async () => {
fireEvent.submit(loginForm);
});
expect(history.location.pathname).toBe('/login');
});
});
+216
View File
@@ -0,0 +1,216 @@
import React from 'react';
import { createMemoryHistory, createLocation } from 'history';
import { Provider } from 'react-redux';
import { Route, Router, match } from 'react-router';
import '@testing-library/jest-dom/extend-expect';
import { render, cleanup, waitForElement, fireEvent, act, wait } from '@testing-library/react';
import configureStore from 'redux-mock-store';
import ProductBrief from 'organisms/ProductBrief';
import useCurrentBrief from 'hooks/useCurrentBrief';
import useNumberParams from 'hooks/useNumberParams';
afterEach(cleanup);
const createMockStore = (initialValues: any) => {
const mockStore = configureStore();
const store = mockStore({
projectData: {
packagingTypes: [],
categories: [],
brands: [],
},
users: {
currentUser: {
id: 1,
organization: {
type: 'supplier',
},
},
usersList: [],
},
projects: {
projectsList: [
{
id: 1,
slug: '#WOIM1-4919',
name: 'Test123',
products: 'http://172.16.62.55:8000/products/?project=1',
printingFilesDate: '2019-12-12',
maxProductStage: 'waiting_for_draft',
published: true,
createdAt: '2019-12-12T10:29:02.860Z',
updatedAt: '2019-12-13T08:56:52.806Z',
},
],
},
products: {
productsList: [
{
id: 1,
displayName: 'test / test',
project: 1,
stage: 'waiting_for_draft',
brief: 'http://test/briefs/1/',
draft: 'http://test/drafts/1/',
firstFinal: null,
printingFiles: null,
photoBrief: 'http://test/photobriefing/1/',
createdAt: '2019-12-12T10:29:02.925Z',
updatedAt: '2019-12-13T08:56:52.788Z',
},
],
},
brief: {
briefs: [
{
initialValues,
},
],
},
});
return store;
};
const renderWithProviderAndRoute = (ui: any, { routeOptions, history, store }: any): any => {
return {
...render(
<Provider store={store}>
<Router history={history}>
<Route {...routeOptions}>{ui}</Route>
</Router>
</Provider>
),
};
};
const routeConfiguration = (path: string, params?: any) => {
const history = createMemoryHistory({ initialEntries: [path] });
const match: match<{ id: string; pid: string }> = {
isExact: true,
path,
url: path,
params: params ? params : {},
};
const location = createLocation(match.url);
return { history, match, location };
};
describe('ProductBrief', () => {
// let history = createMemoryHistory({ initialEntries: ['/login'] });
const initialValues = {
namePol: '',
nameEng: '',
modelNo: '',
batchNo: '',
ean: '',
packagingType: null,
mainDescription: '',
extraDescription: '',
photos: [],
blankings: [],
certificate: '',
informationSigns: '',
capabilitiesIcons: '',
productIcons: '',
hasDescriptionEng: false,
hasDescriptionPol: false,
hasDescriptionPor: false,
hasDescriptionRus: false,
hasDescriptionSpa: false,
illustration: '',
cleaningMethod: '',
countryOfOrigin: '',
manual: '',
material: '',
warning: '',
arrangementPhoto: '',
packshot: '',
clipping: '',
extraPhoto: '',
editable: true,
acceptable: false,
publishable: false,
createdAt: '2020-01-10T08:01:07.703Z',
updatedAt: '2020-01-10T08:01:07.712Z',
id: 1,
};
const path = `/projects/1/products/1`;
let { history, match, location } = routeConfiguration(path, { id: 1, pid: 1 });
afterEach(() => (history = createMemoryHistory({ initialEntries: [path] })));
const dispatch = jest.fn();
const store = createMockStore(initialValues);
it.only('renders basic product brief properly', async () => {
useCurrentBrief.mockReturnValue(initialValues);
useNumberParams.mockReturnValue({
projectId: 1,
productId: 1,
});
const { getByTestId, getByPlaceholderText, getByText, getByLabelText, getAllByText } = renderWithProviderAndRoute(
<ProductBrief dispatch={dispatch} history={history} match={match} location={location} />,
{
routeOptions: { exact: true, path },
history,
store,
}
);
const productPL = await waitForElement(() => getByPlaceholderText('Nazwa produktu - PL'));
const productEN = await waitForElement(() => getByPlaceholderText('Nazwa produktu - EN'));
const modelNr = await waitForElement(() => getByPlaceholderText('Model nr'));
const batchNo = await waitForElement(() => getByPlaceholderText('Nr. Partii'));
const ean = await waitForElement(() => getByPlaceholderText('EAN'));
const packagingType = await waitForElement(() => getByText('Rodzaj opakowania'));
const photosCheckbox = await waitForElement(() => getByLabelText('Zdjęcia'));
const illustrationCheckbox = await waitForElement(() => getByLabelText('Ilustracje'));
const manualCheckbox = await waitForElement(() => getByLabelText('Zdjęcia'));
const productPhotoPicker = await waitForElement(() => getByTestId('productPhotoPicker'));
const productBlankingPicker = await waitForElement(() => getByTestId('productBlankingPicker'));
const polishLang = await waitForElement(() => getByLabelText('Polski'));
const rusLang = await waitForElement(() => getByLabelText('Rosyjski'));
const angLang = await waitForElement(() => getByLabelText('Angielski'));
const ptLang = await waitForElement(() => getByLabelText('Portugalski'));
const espLang = await waitForElement(() => getByLabelText('Hiszpański'));
const material = await waitForElement(() => getByLabelText('Materiał'));
const cleaningMethod = await waitForElement(() => getByLabelText('Sposób czyszczenia'));
const countryOfOrigin = await waitForElement(() => getByLabelText('Kraj produkcji'));
const warning = await waitForElement(() => getByLabelText('Ostrzeżenia'));
const certificate = await waitForElement(() => getByPlaceholderText('Certyfikaty'));
const informationSigns = await waitForElement(() => getByPlaceholderText('Znaki informacyjne'));
const competenceIcons = await waitForElement(() => getByPlaceholderText('Ikony kompetencji'));
const productIcons = await waitForElement(() => getByPlaceholderText('Ikony produktowe'));
const previousButton = await waitForElement(() => getAllByText('Poprzedni'));
const nextButton = await waitForElement(() => getAllByText('Następny'));
expect(previousButton).toHaveLength(2);
expect(nextButton).toHaveLength(2);
expect(photosCheckbox.checked).toEqual(false);
expect(illustrationCheckbox.checked).toEqual(false);
expect(productPL).toBeInTheDocument();
expect(productEN).toBeInTheDocument();
expect(modelNr).toBeInTheDocument();
expect(batchNo).toBeInTheDocument();
expect(ean).toBeInTheDocument();
expect(packagingType).toBeInTheDocument();
expect(photosCheckbox).toBeInTheDocument();
expect(illustrationCheckbox).toBeInTheDocument();
expect(manualCheckbox).toBeInTheDocument();
expect(productPhotoPicker).toBeInTheDocument();
expect(productBlankingPicker).toBeInTheDocument();
expect(polishLang).toBeInTheDocument();
expect(rusLang).toBeInTheDocument();
expect(angLang).toBeInTheDocument();
expect(ptLang).toBeInTheDocument();
expect(espLang).toBeInTheDocument();
expect(material).toBeInTheDocument();
expect(cleaningMethod).toBeInTheDocument();
expect(countryOfOrigin).toBeInTheDocument();
expect(warning).toBeInTheDocument();
expect(certificate).toBeInTheDocument();
expect(informationSigns).toBeInTheDocument();
expect(competenceIcons).toBeInTheDocument();
expect(productIcons).toBeInTheDocument();
// const promoDate = await waitForElement(() => findByDisplayValue(initialValues.printingFilesDate));
// const pfDate = await waitForElement(() => findByDisplayValue(initialValues.promoDate));
});
});
+441
View File
@@ -0,0 +1,441 @@
import React from 'react';
import { createMemoryHistory, createLocation } from 'history';
import ProjectBrief from 'organisms/ProjectBrief/ProjectBrief';
import { cleanup, render, waitForElement, fireEvent, act, wait } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import { Provider } from 'react-redux';
import projectApi from 'API/projects';
import productApi from 'API/products';
import * as productActions from 'modules/products/actions';
import { Route, Router, match } from 'react-router';
import configureStore from 'redux-mock-store'; //ES6 modules
import { Brand, Category } from 'API/projectData';
import { format } from 'date-fns';
import useCurrentProject from 'hooks/useCurrentProject';
import ReactDOM from 'react-dom';
afterEach(cleanup);
const dispatch = jest.fn();
//MOCKED DATA
const category: Category = {
id: 1,
name: 'toys',
};
const brand: Brand = {
id: 1,
name: 'boys',
};
const printingFilesDate = '2011-02-01';
const promoDate = '2011-01-01';
const createMockStore = (initialValues: any) => {
const mockStore = configureStore();
const store = mockStore({
projectData: {
categories: [category],
brands: [brand],
},
users: {
currentUser: {
id: 1,
organization: {
type: 'supplier',
},
},
usersList: [],
},
projects: {
projectsList: [{ ...initialValues }],
},
});
return store;
};
const routeConfiguration = (path: string, params?: any) => {
const history = createMemoryHistory({ initialEntries: [path] });
const match: match = {
isExact: true,
path,
url: path,
params: params ? params : {},
};
const location = createLocation(match.url);
return { history, match, location };
};
const renderWithProviderAndRoute = (ui: any, { routeOptions, history, store }: any): any => {
return {
...render(
<Provider store={store}>
<Router history={history}>
<Route {...routeOptions}>{ui}</Route>
</Router>
</Provider>
),
};
};
describe('new project', () => {
const path = `/projects/new`;
let { history, match, location } = routeConfiguration(path);
beforeEach(() => (history = createMemoryHistory({ initialEntries: [path] })));
const initialValues = {
id: 1,
slug: 'test',
name: 'test',
products: 'test',
maxProductStage: 'max',
published: false,
updatedAt: new Date(),
createdAt: new Date(),
printingFilesDate,
publishable: false,
description: 'description',
category: category.id,
brand: brand.id,
promoDate,
productsAmount: 1,
};
const store = createMockStore(initialValues);
it('should render blank ProjectBrief on /projects/new ', async () => {
const { getByTestId } = renderWithProviderAndRoute(
<ProjectBrief dispatch={dispatch} history={history} match={match} location={location} />,
{ routeOptions: { exact: true, path }, history, store }
);
const categoryDropdown = await waitForElement(() => getByTestId('categoryDropdown'));
const brandDropdown = await waitForElement(() => getByTestId('brandDropdown'));
const printingFilesDateField = await waitForElement(() => getByTestId('printingFilesDatePicker'));
const promotionDateField = await waitForElement(() => getByTestId('poromoDateDatePicker'));
const saveButton = await waitForElement(() => getByTestId('projectSaveButton'));
const projectName = await waitForElement(() => getByTestId('projectName'));
const description = await waitForElement(() => getByTestId('description'));
expect(projectName).toBeInTheDocument();
expect(description).toBeInTheDocument();
expect(printingFilesDateField).toBeInTheDocument();
expect(promotionDateField).toBeInTheDocument();
expect(saveButton).toBeInTheDocument();
expect(brandDropdown).toBeInTheDocument();
expect(categoryDropdown).toBeInTheDocument();
});
it('should allow change form values', async () => {
const { findByTestId, findByText, findAllByDisplayValue } = renderWithProviderAndRoute(
<ProjectBrief dispatch={store.dispatch} history={history} match={match} location={location} />,
{ routeOptions: { exact: true, path }, history, store }
);
const brandDropdown = await waitForElement(() => findByTestId('brandDropdown'));
const categoryDropdown = await waitForElement(() => findByTestId('categoryDropdown'));
const projectName = await waitForElement(() => findByTestId('projectName'));
const description = await waitForElement(() => findByTestId('description'));
const dates = await waitForElement(() => findAllByDisplayValue(format(new Date(), 'yyyy-MM-dd')));
//CHECK IF DATE CHANGED
fireEvent.change(dates[0], { target: { value: '2011-02-02' } });
expect(dates[0]).toHaveValue('2011-02-02');
fireEvent.change(dates[1], { target: { value: '2011-02-03' } });
expect(dates[1]).toHaveValue('2011-02-03');
//CHECK IF PROJECT NAME CHANGED
fireEvent.change(projectName, { target: { value: 'projectName' } });
expect(projectName).toHaveValue('projectName');
//CHECK IF DESCRIPTION CHANGED
fireEvent.change(description, { target: { value: 'description' } });
expect(description).toHaveValue('description');
//CHECK IF BRAND DROPDOWN VALUE CHANGED
const brandValue = await waitForElement(() => findByTestId('brandValue'));
expect(brandValue.innerHTML).toEqual('Marka');
fireEvent.click(brandDropdown);
const brandItem = await waitForElement(() => findByText(brand.name));
expect(brandItem.innerHTML).toEqual(brand.name);
fireEvent.click(brandItem);
expect(brandValue.innerHTML).toEqual(brandItem.innerHTML);
//CHECK IF CATEGORY DROPDOWN VALUE CHANGED
const categoryValue = await waitForElement(() => findByTestId('categoryValue'));
expect(categoryValue.innerHTML).toEqual('Kategoria');
fireEvent.click(categoryDropdown);
const categoryItem = await waitForElement(() => findByText(category.name));
expect(categoryItem.innerHTML).toEqual(category.name);
fireEvent.click(categoryItem);
expect(categoryValue.innerHTML).toEqual(categoryItem.innerHTML);
});
it('publish button should not be visible if publishabled is false', async () => {
const { queryByTestId } = renderWithProviderAndRoute(
<ProjectBrief dispatch={store.dispatch} history={history} match={match} location={location} />,
{ routeOptions: { exact: true, path }, history, store }
);
expect(queryByTestId('publishButton')).toBe(null);
});
it('should not submit blank form', async () => {
const { findByTestId, queryAllByTestId } = renderWithProviderAndRoute(
<ProjectBrief dispatch={store.dispatch} history={history} match={match} location={location} />,
{ routeOptions: { exact: true, path }, history, store }
);
await act(async () => {
const saveButton = await waitForElement(() => findByTestId('projectSaveButton'));
fireEvent.click(saveButton);
});
const errors = queryAllByTestId('inputError');
expect(errors).toHaveLength(5);
});
it('should save proper form and redirect to /products/${pid}', async () => {
projectApi.createNewProject.mockReturnValueOnce({
id: 1,
});
productApi.getProductsByURL.mockReturnValueOnce([
{
id: 1,
},
]);
const { findByTestId, queryAllByTestId } = renderWithProviderAndRoute(
<ProjectBrief
dispatch={store.dispatch}
history={history}
match={match}
location={location}
currentProject={initialValues}
/>,
{ routeOptions: { exact: true, path }, history, store }
);
const saveButton = await waitForElement(() => findByTestId('projectSaveButton'));
await act(async () => {
fireEvent.click(saveButton);
const errors = queryAllByTestId('inputError');
expect(errors).toHaveLength(0);
});
expect(projectApi.createNewProject).toHaveBeenCalledTimes(1);
expect(productApi.getProductsByURL).toHaveBeenCalledTimes(1);
expect(history.location.pathname).toBe('/projects/1/products/1');
});
it('should setCurrentStage to 0', () => {
const spy = jest.spyOn(productActions, 'setCurrentStage');
renderWithProviderAndRoute(
<ProjectBrief dispatch={store.dispatch} history={history} match={match} location={location} />,
{ routeOptions: { exact: true, path: '/projects/new' }, history, store }
);
expect(spy).toHaveBeenCalledWith(0);
expect(spy).toHaveBeenCalledTimes(1);
});
});
describe('existing project not publishable', () => {
const path = `/projects/1`;
const { history, match, location } = routeConfiguration(path, { id: 1 });
const initialValues = {
id: 1,
slug: 'test',
name: 'test',
products: 'test',
maxProductStage: 'max',
published: false,
updatedAt: new Date(),
createdAt: new Date(),
printingFilesDate,
publishable: false,
description: 'description',
category: category.id,
brand: brand.id,
promoDate,
productsAmount: 1,
};
const store = createMockStore(initialValues);
it('should set initial values', async () => {
const { getByTestId, findByDisplayValue } = renderWithProviderAndRoute(
<ProjectBrief
dispatch={dispatch}
history={history}
match={match}
location={location}
currentProject={initialValues}
/>,
{ routeOptions: { exact: true, path }, history, store }
);
const projectName = await waitForElement(() => getByTestId('projectName'));
const projectDesc = await waitForElement(() => getByTestId('description'));
const categoryValue = await waitForElement(() => getByTestId('categoryValue'));
const brandValue = await waitForElement(() => getByTestId('brandValue'));
const promoDate = await waitForElement(() => findByDisplayValue(initialValues.printingFilesDate));
const pfDate = await waitForElement(() => findByDisplayValue(initialValues.promoDate));
expect(promoDate).toHaveValue(initialValues.printingFilesDate);
expect(pfDate).toHaveValue(initialValues.promoDate);
expect(categoryValue.innerHTML).toBe(category.name);
expect(brandValue.innerHTML).toBe(brand.name);
expect(projectName).toHaveValue(initialValues.name);
expect(projectDesc).toHaveValue(initialValues.description);
//TODO HOW TO CHECK FORMIK VALUES
});
it('should expand dropdown', async () => {
const { getByTestId } = renderWithProviderAndRoute(
<ProjectBrief dispatch={store.dispatch} history={history} match={match} location={location} />,
{ routeOptions: { exact: true, path }, history, store }
);
const brand = await waitForElement(() => getByTestId('brandDropdown'));
const category = await waitForElement(() => getByTestId('categoryDropdown'));
fireEvent.click(brand);
fireEvent.click(category);
expect(brand).toHaveClass('is-active');
expect(category).toHaveClass('is-active');
});
});
describe('existing project publishable', () => {
beforeAll(() => {
ReactDOM.createPortal = jest.fn((element, node) => {
return element;
});
});
afterEach(() => {
ReactDOM.createPortal.mockClear();
});
const path = `/projects/1`;
const { history, match, location } = routeConfiguration(path, { id: 1 });
const initialValues = {
id: 1,
slug: 'test',
name: 'test',
products: 'test',
maxProductStage: 'max',
published: false,
updatedAt: new Date(),
createdAt: new Date(),
printingFilesDate,
publishable: true,
description: 'description',
category: category.id,
brand: brand.id,
promoDate,
productsAmount: 1,
};
const store = createMockStore(initialValues);
it('publish button should be in DOM but disabled', async () => {
useCurrentProject.mockReturnValue({ ...initialValues, publishable: false });
const { findByTestId } = renderWithProviderAndRoute(
<ProjectBrief
dispatch={store.dispatch}
history={history}
match={match}
location={location}
currentProject={{ ...initialValues, publishable: false }}
/>,
{ routeOptions: { exact: true, path }, history, store }
);
const publishButton = await waitForElement(() => findByTestId('publishButton'));
expect(publishButton).toBeInTheDocument();
expect(publishButton).toBeDisabled();
});
it('publish button should be in DOM and not disabled', async () => {
useCurrentProject.mockReturnValue(initialValues);
const { findByTestId } = renderWithProviderAndRoute(
<ProjectBrief
dispatch={store.dispatch}
history={history}
match={match}
location={location}
currentProject={initialValues}
/>,
{ routeOptions: { exact: true, path }, history, store }
);
const publishButton = await waitForElement(() => findByTestId('publishButton'));
expect(publishButton).toBeInTheDocument();
expect(publishButton).not.toBeDisabled();
});
it('should open modal and spin spinner', async () => {
projectApi.publishProject.mockReturnValueOnce({ ...initialValues, published: true });
useCurrentProject.mockReturnValue(initialValues);
productApi.getProductsByURL.mockReturnValueOnce([
{
id: 1,
},
]);
const { findByTestId, findByText } = renderWithProviderAndRoute(
<ProjectBrief
dispatch={store.dispatch}
history={history}
match={match}
location={location}
currentProject={initialValues}
/>,
{ routeOptions: { exact: true, path }, history, store }
);
const publishButton = await waitForElement(() => findByTestId('publishButton'));
expect(publishButton).toBeInTheDocument();
await act(async () => {
fireEvent.click(publishButton);
});
const modal = await waitForElement(() => findByText('Publikacja projektu'));
const modalCancel = await waitForElement(() => findByTestId('modalCancel'));
const modalSubmit = await waitForElement(() => findByTestId('modalSubmit'));
expect(modal).toBeInTheDocument();
expect(modalSubmit).toBeInTheDocument();
expect(modalCancel).toBeInTheDocument();
fireEvent.click(modalSubmit);
expect(modalSubmit).toHaveClass('is-loading');
await wait(() => expect(modalSubmit).not.toHaveClass('is-loading'));
expect(modal).not.toBeInTheDocument();
});
describe('Published Project', () => {
const path = `/projects/1`;
const { history, match, location } = routeConfiguration(path, { id: 1 });
const initialValues = {
id: 1,
slug: 'test',
name: 'test',
products: 'test',
maxProductStage: 'max',
published: true,
updatedAt: new Date(),
createdAt: new Date(),
printingFilesDate,
publishable: false,
description: 'description',
category: category.id,
brand: brand.id,
promoDate,
productsAmount: 1,
};
const store = createMockStore(initialValues);
it('should not display publish button when published is true', async () => {
useCurrentProject.mockReturnValue(initialValues);
const { queryByTestId } = renderWithProviderAndRoute(
<ProjectBrief
dispatch={store.dispatch}
history={history}
match={match}
location={location}
currentProject={initialValues}
/>,
{ routeOptions: { exact: true, path }, history, store }
);
expect(queryByTestId('publishButton')).toBe(null);
});
it('should have disabled fields', async () => {
useCurrentProject.mockReturnValue(initialValues);
const { findByText, findByTestId, findByDisplayValue } = renderWithProviderAndRoute(
<ProjectBrief
dispatch={store.dispatch}
history={history}
match={match}
location={location}
currentProject={initialValues}
/>,
{ routeOptions: { exact: true, path }, history, store }
);
const brandValue = await waitForElement(() => findByTestId('brandValue'));
const categoryValue = await waitForElement(() => findByTestId('categoryValue'));
const projectName = await waitForElement(() => findByTestId('projectName'));
const description = await waitForElement(() => findByTestId('description'));
const promo = await waitForElement(() => findByDisplayValue(promoDate));
const pfDate = await waitForElement(() => findByDisplayValue(printingFilesDate));
expect(categoryValue.parentElement).toBeDisabled();
expect(brandValue.parentElement).toBeDisabled();
expect(promo).toBeDisabled();
expect(pfDate).toBeDisabled();
expect(projectName).toBeDisabled();
expect(description).toBeDisabled();
});
});
});
+11
View File
@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24.149" height="29.649" viewBox="0 0 24.149 29.649">
<g id="PDF_Icon_zaakceptowanu" transform="translate(0 0)">
<path id="Path_84" data-name="Path 84" d="M528.335,109.472l-1.323-1.54-1.259,1.081,1.973,2.3a.83.83,0,0,0,.629.289h.013a.829.829,0,0,0,.633-.309l3.416-4.24-1.292-1.041Z" transform="translate(-511.762 -85.918)"/>
<rect id="Rectangle_36" data-name="Rectangle 36" width="10.172" height="1.223" transform="translate(4.884 5.255)" />
<rect id="Rectangle_37" data-name="Rectangle 37" width="10.172" height="1.223" transform="translate(4.884 9.584)" />
<rect id="Rectangle_38" data-name="Rectangle 38" width="4.931" height="1.223" transform="translate(4.884 18.242)" />
<rect id="Rectangle_39" data-name="Rectangle 39" width="10.172" height="1.223" transform="translate(4.884 13.913)" />
<path id="Path_85" data-name="Path 85" d="M478.233,57.373a.983.983,0,0,1-.982-.983V36.48a.983.983,0,0,1,.982-.983H491.99a.983.983,0,0,1,.982.983V48.556a7.51,7.51,0,0,1,2.109.464V36.084a2.751,2.751,0,0,0-2.751-2.751H477.893a2.751,2.751,0,0,0-2.751,2.751v20.7a2.751,2.751,0,0,0,2.751,2.751h7.774a7.512,7.512,0,0,1-.716-2.164Z" transform="translate(-475.142 -33.333)" />
<path id="Path_86" data-name="Path 86" d="M519.644,90.857a6.874,6.874,0,1,0,6.874,6.873A6.881,6.881,0,0,0,519.644,90.857Zm0,12.087a5.214,5.214,0,1,1,5.214-5.214A5.22,5.22,0,0,1,519.644,102.944Z" transform="translate(-502.369 -74.955)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="6" height="10.001" viewBox="0 0 6 10.001">
<path id="arrow_icon" data-name="arrow icon" d="M5,6l5-3V.125L5,3.144,0,0V3Z" transform="translate(0 10.001) rotate(-90)"/>
</svg>

After

Width:  |  Height:  |  Size: 223 B

+8
View File
@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 36 36">
<g id="arrow_small_button" data-name="arrow small button" transform="translate(-11662 -1616)">
<circle id="placeholder" cx="18" cy="18" r="18" transform="translate(11662 1616)" fill="rgba(245,140,57,0.5)"/>
<g id="arrow_icon" data-name="arrow icon">
<path id="arrow_icon-2" data-name="arrow icon" d="M11682.009,1628.991l-7,7.428,1.726,1.535,4.264-4.86v9.916l2.034-.064v-9.852l4.284,4.86,1.69-1.651Z" transform="translate(-2 -2)" fill="#fff"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 566 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="29" height="31" viewBox="0 0 29 31"><g><g><g><path fill="#ccc" d="M4.73 7.05V5.728h11V7.05z"/></g><g><path fill="#ccc" d="M4.73 11.403V10.08h11v1.323z"/></g><g><path fill="#ccc" d="M4.73 20.107v-1.322h11v1.322z"/></g><g><path fill="#ccc" d="M4.73 15.755v-1.322h11v1.322z"/></g><g><path fill="#ccc" d="M26.753 4.916L20.22 3.398v2.235l5.606 1.302c.569.133.914.667.772 1.194l-4.227 15.586-.048.177-.382 1.41-.226.835-.36 1.324c-.143.527-.72.847-1.289.715l-2.81-.653a4.035 4.035 0 0 1-.766.074H7.96l11.892 2.764c1.594.37 3.21-.526 3.61-2.001l1.086-4.005.26-.957.35-1.288.08-.299L28.914 8.26c.4-1.476-.567-2.972-2.161-3.343z"/></g><g><path fill="#ccc" d="M3.343 24.451c-.588 0-1.063-.439-1.063-.981V3.587c0-.542.475-.981 1.063-.981h14.875c.587 0 1.063.44 1.063.981V23.47c0 .542-.476.981-1.063.981zM21.56 3.191c0-1.517-1.332-2.747-2.975-2.747H2.976C1.331.444 0 1.674 0 3.192v20.673c0 1.518 1.332 2.748 2.975 2.748h15.61c1.644 0 2.976-1.23 2.976-2.748z"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 1013 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" viewBox="0 0 18 20">
<path id="icon" d="M11138,984h5v5h-5Zm12,9h-14V982h14Zm0-16h-1v-2h-2v2h-8v-2h-2v2h-1a1.991,1.991,0,0,0-1.99,2l-.01,14a2,2,0,0,0,2,2h14a2.006,2.006,0,0,0,2-2V979A2.006,2.006,0,0,0,11150,977Z" transform="translate(-11134 -975)" fill="#ccc"/>
</svg>

After

Width:  |  Height:  |  Size: 333 B

+3
View File
@@ -0,0 +1,3 @@
<svg id="comment_icon" data-name="comment icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path id="comment_icon-2" data-name="comment icon" d="M16263,1136.8a.8.8,0,0,0-.8-.8h-10.4a.8.8,0,0,0-.8.8V1148l3.2-2.995h8a.8.8,0,0,0,.8-.8Zm1.011,2.2v7.021H16254l.01,2.165a.815.815,0,0,0,.813.813h8.938l3.241,3v-12.188a.815.815,0,0,0-.813-.812Z" transform="translate(-16251 -1136)" fill="#ccc"/>
</svg>

After

Width:  |  Height:  |  Size: 433 B

+8
View File
@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="21.632" height="29.662" viewBox="0 0 21.632 29.662">
<g id="PDF_Icon-poprawki" transform="translate(0 0)">
<rect id="Rectangle_40" data-name="Rectangle 40" width="10.011" height="1.204" transform="translate(4.807 5.172)" fill="#ccc"/>
<rect id="Rectangle_41" data-name="Rectangle 41" width="10.011" height="1.204" transform="translate(4.807 9.433)" fill="#ccc"/>
<path id="Path_87" data-name="Path 87" d="M482.352,388.384h-4.167a.967.967,0,0,1-.967-.967V367.822a.967.967,0,0,1,.967-.967h13.54a.967.967,0,0,1,.967.967v13.427c.035-.008.069-.018.1-.025a2.37,2.37,0,0,1,.457-.044,2.626,2.626,0,0,1,1.514.488V367.433a2.708,2.708,0,0,0-2.708-2.708H477.85a2.708,2.708,0,0,0-2.708,2.708v20.374a2.708,2.708,0,0,0,2.708,2.708h6.8l-.243-.2A11.231,11.231,0,0,1,482.352,388.384Z" transform="translate(-475.142 -364.725)" fill="#ccc"/>
<path id="Path_88" data-name="Path 88" d="M516.666,423.332a13.944,13.944,0,0,0-2.006-6,1.862,1.862,0,0,0-1.838-.738,1.387,1.387,0,0,0-.522.215,1.651,1.651,0,0,0-1.76-.8,1.335,1.335,0,0,0-.726.457,1.767,1.767,0,0,0-1.542-.5,1.467,1.467,0,0,0-.579.238l-1.03-2.708a1.619,1.619,0,0,0-1.242-1.089,1.517,1.517,0,0,0-1.414.537,1.62,1.62,0,0,0-.2,1.639l2.529,6.654c-1.56-.662-2.777-1.037-3.362-.5a1.348,1.348,0,0,0-.5,1.05c.026.877.963,1.638,2.261,2.691a15.3,15.3,0,0,1,4.077,4.365.453.453,0,0,0,.56.209l6.925-2.632a.453.453,0,0,0,.292-.43,11.243,11.243,0,0,1,.038-1.317A11.889,11.889,0,0,0,516.666,423.332Zm-.944,1.27c-.022.264-.045.536-.044,1.087l-6.267,2.382a16.861,16.861,0,0,0-4.107-4.292c-.984-.8-1.913-1.552-1.926-2.015,0-.044,0-.161.2-.35.027-.014.27-.108,1.2.2.652.215,1.38.538,1.965.8l.21.093a.453.453,0,0,0,.606-.575l-2.912-7.662a.723.723,0,0,1,.06-.745.617.617,0,0,1,.567-.215.723.723,0,0,1,.54.517l1.361,3.581.008.02.856,2.253a.453.453,0,1,0,.847-.322l-.825-2.171a.428.428,0,0,1,.373-.322c.342-.063.837.119,1.111.841l.646,1.7a.453.453,0,0,0,.847-.322l-.622-1.638c-.018-.3.1-.5.331-.551a.818.818,0,0,1,.887.616l.151.4h0l.477,1.257a.453.453,0,0,0,.847-.321l-.424-1.118a.378.378,0,0,1,.308-.239.965.965,0,0,1,.9.345,13.067,13.067,0,0,1,1.859,5.513A11.034,11.034,0,0,1,515.722,424.6Z" transform="translate(-495.036 -399.424)" fill="#ccc"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><g><g><g><g opacity=".3"><path d="M0 10C0 4.477 4.477 0 10 0s10 4.477 10 10-4.477 10-10 10S0 15.523 0 10z"/></g></g><g><g><path fill="#ccc" d="M13 7.604L12.396 7 10 9.396 7.605 7 7 7.604 9.396 10 7 12.396l.605.604L10 10.604 12.396 13l.604-.604L10.604 10z"/></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 362 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="15.999" height="20" viewBox="0 0 15.999 20">
<path id="doc_icon" data-name="doc icon" d="M12567,176v-5.5l5.5,5.5Zm-7-7a2,2,0,0,0-1.989,2l-.01,16a2,2,0,0,0,1.99,2H12572a2.005,2.005,0,0,0,2-2V175l-6-6Z" transform="translate(-12558 -169)" fill="#d1d1d1"/>
</svg>

After

Width:  |  Height:  |  Size: 309 B

+3
View File
@@ -0,0 +1,3 @@
<svg id="done_icon" data-name="done icon" xmlns="http://www.w3.org/2000/svg" width="13.135" height="10.732" viewBox="0 0 13.135 10.732">
<path id="done_icon-2" data-name="done icon" d="M16253.653,1056.216l-1.653,1.6,4.179,4.179,8.956-8.955-1.465-1.777-7.491,7.643Z" transform="translate(-16252 -1051.267)" fill="#50d166"/>
</svg>

After

Width:  |  Height:  |  Size: 332 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10">
<path id="icon" d="M6126,1482l-5-5.3,1.234-1.095,3.046,3.467V1472l1.453.046v7.028l3.059-3.467,1.208,1.178-5,5.216Z" transform="translate(-6120.999 -1472)" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 262 B

+9
View File
@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="22.559" height="29.647" viewBox="0 0 22.559 29.647">
<g id="PDF_icon_nowyplik" transform="translate(0 0)">
<rect id="Rectangle_42" data-name="Rectangle 42" width="11.508" height="1.384" transform="translate(5.525 5.946)" fill="#ccc"/>
<rect id="Rectangle_43" data-name="Rectangle 43" width="11.508" height="1.384" transform="translate(5.525 10.844)" fill="#ccc"/>
<rect id="Rectangle_44" data-name="Rectangle 44" width="11.508" height="1.384" transform="translate(5.525 20.639)" fill="#ccc"/>
<rect id="Rectangle_45" data-name="Rectangle 45" width="11.508" height="1.384" transform="translate(5.525 15.742)" fill="#ccc"/>
<path id="Path_89" data-name="Path 89" d="M70.876,33.333H54.543a3.113,3.113,0,0,0-3.113,3.113V59.867a3.113,3.113,0,0,0,3.113,3.113H70.876a3.113,3.113,0,0,0,3.113-3.113V36.446A3.113,3.113,0,0,0,70.876,33.333ZM71.6,55.054v4.365a1.112,1.112,0,0,1-1.112,1.112H54.927a1.112,1.112,0,0,1-1.112-1.112V36.894a1.112,1.112,0,0,1,1.112-1.112H70.492A1.112,1.112,0,0,1,71.6,36.894Z" transform="translate(-51.43 -33.333)" fill="#ccc"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="24" viewBox="0 0 20 24"><g><g><path fill="#fff" d="M15.554 5.092v1.454H4.444V5.092zm0 3.636v1.454H4.444V8.728zm0 3.637v1.453H4.444v-1.453zM9.63 16v1.453H4.444v-1.453zM20 22.544V1.458C20 .654 19.333 0 18.513 0H1.488C.668 0 0 .654 0 1.458v21.086C0 23.347.667 24 1.488 24h17.025c.82 0 1.487-.653 1.487-1.456z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 381 B

+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="5" height="5" viewBox="0 0 5 5">
<g id="Group_71" data-name="Group 71" transform="translate(-6146.5 -1465)">
<line id="Line_5" data-name="Line 5" x2="3" transform="translate(6146.5 1465.5)" fill="rgba(0,0,0,0)" stroke="#fff" stroke-width="1"/>
<line id="Line_6" data-name="Line 6" x2="4" transform="translate(6146.5 1467.5)" fill="rgba(0,0,0,0)" stroke="#fff" stroke-width="1"/>
<line id="Line_7" data-name="Line 7" x2="5" transform="translate(6146.5 1469.5)" fill="rgba(0,0,0,0)" stroke="#fff" stroke-width="1"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 589 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10">
<path id="icon" d="M6126,1482l-5-5.3,1.234-1.095,3.046,3.467V1472l1.453.046v7.028l3.059-3.467,1.208,1.178-5,5.216Z" transform="translate(-6120.999 -1472)" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 262 B

+26
View File
@@ -0,0 +1,26 @@
export { ReactComponent as OptionDots } from './menu.svg';
export { ReactComponent as Caret } from './arrow.svg';
export { ReactComponent as OutboRocket } from './outbo_rocket.svg';
export { ReactComponent as OutboLogo } from './outbo_logo.svg';
export { ReactComponent as PollIcon } from './poll.svg';
export { ReactComponent as DocIcon } from './doc.svg';
export { ReactComponent as PuzzelIcon } from './puzzel.svg';
export { ReactComponent as SearchIcon } from './search.svg';
export { ReactComponent as CirclePlus } from './plus_circle.svg';
export { ReactComponent as CalendarIcon } from './calendar.svg';
export { default as outboLoginBg } from './outbo_login_bg.png';
export { ReactComponent as LockIcon } from './lock_icon.svg';
export { ReactComponent as UserIcon } from './user_icon.svg';
export { ReactComponent as Delete } from './delete.svg';
export { ReactComponent as BlankingIcon } from './blanking_icon.svg';
export { ReactComponent as FileIcon } from './file_icon.svg';
export { ReactComponent as PhotoIcon } from './photo_icon.svg';
export { ReactComponent as DoneIcon } from './done_icon.svg';
export { ReactComponent as CommentIcon } from './comment_icon.svg';
export { ReactComponent as VerticalLine } from './vertical_line.svg';
export { ReactComponent as CircleArrow } from './arrow_circle.svg';
export { ReactComponent as CorrectedFileIcon } from './corrected_file_icon.svg';
export { ReactComponent as AcceptedFileIcon } from './accepted_file_icon.svg';
export { ReactComponent as DraftFileIcon } from './draft_file_icon.svg';
export { ReactComponent as DownloadIcon } from './download_icon.svg';
export { ReactComponent as FilterIcon } from './filter_icon.svg';
+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="17" viewBox="0 0 13 17">
<g id="lock_icon" data-name="lock icon" transform="translate(-17612 -267)">
<path id="lock_icon-2" data-name="lock icon" d="M17621,273h-5v-1.569a2.5,2.5,0,0,1,5,0Zm-2.5,7a1.5,1.5,0,1,1,1.5-1.5A1.5,1.5,0,0,1,17618.5,280Zm4.875-7.333h-.812v-1.619a4.063,4.063,0,0,0-8.125,0v1.619h-.814a1.627,1.627,0,0,0-1.623,1.619v8.1a1.627,1.627,0,0,0,1.623,1.619h9.752a1.627,1.627,0,0,0,1.625-1.619v-8.1A1.628,1.628,0,0,0,17623.375,272.667Z" fill="#e6e6e6"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 547 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="17.999" height="4" viewBox="0 0 17.999 4">
<path id="menu_icon" data-name="menu icon" d="M12577,287a2,2,0,1,0-2-2A2,2,0,0,0,12577,287Zm-7,0a2,2,0,1,0-2-2A2,2,0,0,0,12570,287Zm-7,0a2,2,0,1,0-2-2A2,2,0,0,0,12563,287Z" transform="translate(-12561 -283)" fill="#d1d1d1"/>
</svg>

After

Width:  |  Height:  |  Size: 324 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

+35
View File
@@ -0,0 +1,35 @@
<svg xmlns="http://www.w3.org/2000/svg" width="115.121" height="71.15" viewBox="0 0 115.121 71.15">
<g id="Logo_OUTBO" transform="translate(-54.904 -78.624)">
<g id="Group_62" data-name="Group 62" transform="translate(75.747 78.624)">
<path id="Path_36" data-name="Path 36" d="M147.366,79.1l-.162-.025a8.769,8.769,0,0,0-.154.969c-.08,1.6-.142,3.2-.211,4.8a4.324,4.324,0,0,0,.306,2.062.72.72,0,0,1,.013.322c-.03.54-.094,1.08-.09,1.619a11.476,11.476,0,0,1,.087,1.648A11.2,11.2,0,0,1,147,92.16a3.885,3.885,0,0,0,.064,1.249c.029.464.082.929.072,1.393-.021.908-.107,1.814-.108,2.722,0,.4.143.788-.1,1.165a.238.238,0,0,0,.03.227c.409.218.394.6.356.963a12.629,12.629,0,0,0-.229,1.431c.007.513-.281,1.035.028,1.55.054.09-.017.252-.025.382s-.008.272-.011.421a2.608,2.608,0,0,1,.359-.008c.18.024.5.034.51.1a.776.776,0,0,1-.085.611c-.645.684-1.332,1.329-2,1.992a2.85,2.85,0,0,0-.239.312l.106.108c.291-.243.578-.491.873-.729a3.2,3.2,0,0,1,.908-.682,2.354,2.354,0,0,0,1.222-1.054c.289-.4.495-.931.881-1.183a4.85,4.85,0,0,0,1.135-.8c.03-.035.107-.04.162-.041.748-.021,1.5-.056,2.244-.053a13.451,13.451,0,0,0,2.476.074c.1-.017.2-.016.292-.032a5.743,5.743,0,0,1,1.024-.172q3.37.054,6.739.159a9.435,9.435,0,0,1,1.627.174,1.287,1.287,0,0,1,.953.719c1.226.03,2.462.053,3.7.095.319.011.635.113.954.116.631.007,1.262-.027,1.893-.041a5.05,5.05,0,0,0,.6,0,.734.734,0,0,0,.442-.182c.053-.065-.051-.278-.113-.411-.121-.259-.269-.506-.395-.764s-.031-.386.249-.383.564.013.8.019l-.27-.789c-.547.007-.725-.371-.708-.926.026-.849.028-1.7.06-2.548a1.094,1.094,0,0,1,.161-.376,1.454,1.454,0,0,1-.157-.448c.008-.575.023-1.152.08-1.724.106-1.059.306-2.112.349-3.172a16.1,16.1,0,0,0-.146-3.116c-.053-.343.139-.638,0-.977a3.65,3.65,0,0,1-.134-.978c-.04-.359.025-.666.337-.76a3.948,3.948,0,0,1-.129-.738c.007-.417.09-.832.1-1.249,0-.281-.1-.562-.1-.843,0-1.021.043-2.041.028-3.061,0-.2-.167-.518-.312-.553a.945.945,0,0,0-.686.2,3.356,3.356,0,0,0-.708.752q-1.017,1.446-1.992,2.92c-1.066,1.6-2.112,3.222-3.186,4.821a3.358,3.358,0,0,1-.778.84.627.627,0,0,0-.33.693,1.993,1.993,0,0,1-.509-.513c-.187-.3-.271-.663-.668-.8-.086-.029-.147-.191-.181-.3-.049-.163-.04-.343-.093-.5-.373-1.117-.744-2.234-1.14-3.343a1.513,1.513,0,0,0-.764-.964,1.552,1.552,0,0,1-.61-.878c-.326-.993-.574-2.011-.871-3.014a.77.77,0,0,0-1.057-.587.423.423,0,0,0-.379.441,3.158,3.158,0,0,1-.173,1.01c-.459,1.083-.963,2.147-1.455,3.216-.531,1.152-1.047,2.311-1.608,3.448-.535,1.083-1.12,2.142-1.714,3.269-.13-.179-.235-.31-.325-.451-.462-.728-.918-1.46-1.38-2.188-.422-.665-.836-1.336-1.276-1.988-.5-.749-1.032-1.483-1.553-2.221-.579-.821-1.174-1.63-1.737-2.461C148.013,80.208,147.7,79.646,147.366,79.1Z" transform="translate(-145.624 -78.624)" fill="#fff"/>
<path id="Path_37" data-name="Path 37" d="M147.366,79.1c.334.548.647,1.11,1.006,1.64.563.831,1.158,1.64,1.737,2.461.521.738,1.048,1.472,1.553,2.221.44.653.854,1.323,1.276,1.988.462.728.918,1.46,1.38,2.188.089.141.2.272.325.451.593-1.127,1.179-2.185,1.714-3.269.562-1.137,1.078-2.3,1.608-3.448.492-1.069,1-2.133,1.455-3.216a3.158,3.158,0,0,0,.173-1.01.423.423,0,0,1,.379-.441.77.77,0,0,1,1.057.587c.3,1,.546,2.021.871,3.014a1.552,1.552,0,0,0,.61.878,1.513,1.513,0,0,1,.764.964c.4,1.109.767,2.226,1.14,3.343.054.16.045.341.093.5.034.112.095.274.181.3.4.134.48.5.668.8a1.993,1.993,0,0,0,.509.513.627.627,0,0,1,.33-.693,3.358,3.358,0,0,0,.778-.84c1.074-1.6,2.12-3.217,3.186-4.821q.978-1.472,1.992-2.92a3.356,3.356,0,0,1,.708-.752.945.945,0,0,1,.686-.2c.145.035.309.357.312.553.016,1.02-.027,2.041-.028,3.061,0,.281.108.562.1.843-.006.417-.089.832-.1,1.249a3.948,3.948,0,0,0,.129.738c-.311.094-.377.4-.337.76a3.65,3.65,0,0,0,.134.978c.141.339-.051.634,0,.977a16.1,16.1,0,0,1,.146,3.116c-.043,1.06-.244,2.113-.349,3.172-.057.572-.073,1.149-.08,1.724a1.454,1.454,0,0,0,.157.448,1.094,1.094,0,0,0-.161.376c-.032.849-.034,1.7-.06,2.548-.017.555.161.932.708.926l.27.789c-.235-.006-.516-.015-.8-.019s-.374.127-.249.383.273.5.395.764c.063.133.166.347.113.411a.734.734,0,0,1-.442.182,5.05,5.05,0,0,1-.6,0c-.631.014-1.262.048-1.893.041-.318,0-.635-.105-.954-.116-1.235-.042-2.471-.065-3.7-.095a1.287,1.287,0,0,0-.953-.719,9.435,9.435,0,0,0-1.627-.174q-3.369-.1-6.739-.159a5.743,5.743,0,0,0-1.024.172c-.1.016-.2.015-.292.032a13.451,13.451,0,0,1-2.476-.074c-.748,0-1.5.032-2.244.053-.056,0-.132.006-.162.041a4.85,4.85,0,0,1-1.135.8c-.386.252-.593.779-.881,1.183a2.354,2.354,0,0,1-1.222,1.054,3.2,3.2,0,0,0-.908.682c-.295.238-.582.486-.873.729l-.106-.108a2.85,2.85,0,0,1,.239-.312c.668-.663,1.355-1.308,2-1.992a.776.776,0,0,0,.085-.611c-.015-.07-.33-.08-.51-.1a2.608,2.608,0,0,0-.359.008c0-.15,0-.286.011-.421s.079-.292.025-.382c-.309-.515-.021-1.037-.028-1.55a12.629,12.629,0,0,1,.229-1.431c.037-.359.052-.744-.356-.963a.238.238,0,0,1-.03-.227c.242-.376.1-.769.1-1.165,0-.907.088-1.814.108-2.722.011-.463-.042-.929-.072-1.393A3.885,3.885,0,0,1,147,92.16a11.2,11.2,0,0,0,.154-1.672,11.476,11.476,0,0,0-.087-1.648c0-.539.06-1.079.09-1.619a.72.72,0,0,0-.013-.322,4.324,4.324,0,0,1-.306-2.062c.07-1.6.131-3.2.211-4.8a8.769,8.769,0,0,1,.154-.969Zm6.594,21.763c.274.007.544.007.813.022.154.008.307.038.46.061a1.213,1.213,0,0,0,.282.045c.357-.036.711-.119,1.067-.122s.736.094,1.1.1q2.094.011,4.189-.018c.278,0,.553-.1.832-.112a5.664,5.664,0,0,1,.83.022c.183.016.361.1.543.107q2.266.068,4.531.114c.994.021,1.988.031,2.983.042.368,0,.711-.064.783-.5a2.893,2.893,0,0,0,.057-.937,8.351,8.351,0,0,1-.156-2.322c.11-1.045.156-2.1.179-3.15s-.016-2.125-.023-3.188c0-.673.019-1.348,0-2.02-.015-.451-.093-.9-.137-1.349-.051-.514-.078-1.031-.158-1.54a.48.48,0,0,0-.336-.276,5.593,5.593,0,0,0-.832-.033l-.027-.135c.288-.081.582-.147.862-.25.111-.041.267-.161.272-.251.04-.755.049-1.512.067-2.268l-.126-.005c-.289.391-.587.776-.863,1.176s-.528.823-.793,1.233c-.42.651-.421.65.241,1.025.014.008.015.041.026.076-.106.046-.209.1-.314.136a3.733,3.733,0,0,0-1.945,1.608c-.159.272-.4.5-.541.777a11.691,11.691,0,0,0-1.306,2.481.357.357,0,0,1-.151.2c-.488.3-.547.613-.2,1.079.055.074.119.192.1.263-.107.338-.242.667-.373,1.015l-.357.034-1.16-1.63.541-.491c-.163-.119-.347-.183-.4-.3-.145-.327-.307-.264-.529-.1q-1.622,1.21-3.254,2.408a2.012,2.012,0,0,1-.411.179,11.4,11.4,0,0,1,1.074-1.079c.5-.468.978-.963,1.5-1.406.348-.3.761-.516,1.137-.781a.832.832,0,0,0,.037-1.286c-.156-.144-.372-.473-.317-.557.315-.477-.088-.523-.352-.677l.138-.087c-.354.042-.322-.2-.358-.439a6.468,6.468,0,0,0-.223-.832,29.52,29.52,0,0,0-2.256-4.782,4.981,4.981,0,0,0-.454-.626,59.177,59.177,0,0,0-4.568,9.841l-.387-.031c-.014.111-.019.2-.035.279-.039.206.067.485-.305.51-.341.023-.484-.1-.535-.45-.03-.2-.165-.508-.318-.556-.34-.106-.423-.358-.53-.614-.083-.2-.111-.422-.206-.612-.183-.367-.173-.916-.8-.9-.038,0-.081-.076-.116-.12-.556-.7-1.123-1.387-1.662-2.1-.564-.743-1.1-1.506-1.647-2.263-.151-.209-.282-.432-.437-.637-.142-.187-.3-.359-.523-.614-.062.674-.148,1.2-.148,1.724,0,.453.142.907.139,1.36-.01,1.913.031,3.831-.1,5.735a22.5,22.5,0,0,0,.112,4.773,6.512,6.512,0,0,1,.066,1.568c-.074.8-.063,1.608-.08,2.414-.011.535-.006.534.547.487.1-.008.2.008.3.01a5.307,5.307,0,0,0,3.536-.8l-.4.663,1.97-.121Zm-5.616,2.064-.023.942.107.022,1.031-1.3Zm22.305-.728-.012.176c.431.082.86.181,1.295.234.095.012.209-.138.314-.213-.09-.1-.18-.284-.269-.283C171.534,102.112,171.092,102.163,170.649,102.2Zm-.2.311,0-.2a3.2,3.2,0,0,0-1.76.153Z" transform="translate(-145.624 -78.624)"/>
<path id="Path_38" data-name="Path 38" d="M161.912,110.527l.165.191-1.97.121.4-.663a5.307,5.307,0,0,1-3.536.8c-.1,0-.2-.018-.3-.01-.552.047-.558.049-.547-.487.017-.805.006-1.614.08-2.414a6.512,6.512,0,0,0-.066-1.568,22.5,22.5,0,0,1-.112-4.773c.135-1.9.095-3.823.1-5.735,0-.453-.14-.907-.139-1.36,0-.525.086-1.05.148-1.724.219.255.381.427.523.614.155.2.287.428.437.637.546.757,1.083,1.52,1.647,2.263.539.71,1.106,1.4,1.662,2.1.035.044.078.121.116.12.623-.011.613.538.8.9.095.191.123.414.206.612.107.256.19.508.53.614.153.048.289.352.318.556.051.345.193.473.535.45.372-.025.267-.3.305-.51.016-.083.021-.168.035-.279l.387.031a59.177,59.177,0,0,1,4.568-9.841,4.98,4.98,0,0,1,.454.626,29.523,29.523,0,0,1,2.256,4.782,6.468,6.468,0,0,1,.223.832c.036.235,0,.481.358.439l-.138.087c.264.154.667.2.352.677-.055.084.16.413.317.557a.832.832,0,0,1-.037,1.286c-.376.265-.788.485-1.137.781-.522.443-1,.938-1.5,1.406a11.4,11.4,0,0,0-1.074,1.079,2.013,2.013,0,0,0,.411-.179q1.633-1.2,3.254-2.408c.222-.166.384-.229.529.1.054.122.237.186.4.3l-.541.491,1.16,1.63.357-.034c.131-.348.266-.677.373-1.015.023-.071-.041-.189-.1-.263-.345-.465-.286-.779.2-1.079a.357.357,0,0,0,.151-.2,11.692,11.692,0,0,1,1.306-2.481c.14-.279.383-.505.541-.777a3.733,3.733,0,0,1,1.945-1.608c.105-.041.208-.09.314-.136-.011-.035-.012-.068-.026-.076-.661-.375-.661-.374-.241-1.025.265-.411.516-.831.793-1.233s.574-.785.863-1.176l.126.005c-.018.756-.028,1.513-.067,2.268,0,.091-.162.211-.272.251-.28.1-.573.169-.862.25l.027.135a5.589,5.589,0,0,1,.832.033.48.48,0,0,1,.336.276c.08.509.107,1.027.158,1.54.045.45.122.9.137,1.349.023.673,0,1.347,0,2.02.007,1.063.047,2.126.023,3.188s-.069,2.105-.179,3.15a8.352,8.352,0,0,0,.156,2.322,2.892,2.892,0,0,1-.057.937c-.072.435-.415.5-.783.5-.994-.011-1.989-.021-2.983-.042q-2.266-.048-4.531-.114c-.182-.005-.36-.091-.543-.107a5.665,5.665,0,0,0-.83-.022c-.279.017-.554.109-.832.112q-2.094.027-4.189.018c-.368,0-.737-.1-1.1-.1s-.711.087-1.067.122a1.213,1.213,0,0,1-.282-.045c-.153-.023-.306-.052-.46-.061C162.455,110.534,162.185,110.533,161.912,110.527Z" transform="translate(-153.576 -88.29)" fill="#a72c88"/>
<path id="Path_39" data-name="Path 39" d="M157.386,183.269l1.115-.335-1.031,1.3-.107-.022Z" transform="translate(-154.666 -158.969)" fill="#fff"/>
<path id="Path_40" data-name="Path 40" d="M254.508,180.934c.443-.034.885-.085,1.328-.087.089,0,.179.184.269.283-.105.075-.22.225-.314.213-.435-.053-.864-.152-1.295-.234Z" transform="translate(-229.483 -157.361)" fill="#fff"/>
<path id="Path_41" data-name="Path 41" d="M247.766,181.674,246,181.631a3.2,3.2,0,0,1,1.76-.153Z" transform="translate(-222.941 -157.791)" fill="#fff"/>
</g>
<g id="Group_63" data-name="Group 63" transform="translate(54.904 100.326)">
<path id="Path_42" data-name="Path 42" d="M55.378,200.113l-.025.162a8.773,8.773,0,0,0,.969.154c1.6.08,3.2.142,4.8.211a4.322,4.322,0,0,0,2.062-.305.72.72,0,0,1,.322-.013c.54.03,1.08.094,1.619.09a11.472,11.472,0,0,1,1.648-.087,11.194,11.194,0,0,1,1.672.154,3.884,3.884,0,0,0,1.248-.064c.464-.029.929-.082,1.393-.072.908.021,1.815.107,2.722.108.4,0,.788-.143,1.165.1a.239.239,0,0,0,.227-.03c.218-.409.6-.394.963-.357a12.626,12.626,0,0,0,1.431.229c.513-.007,1.035.281,1.55-.028.09-.054.253.017.382.025s.272.008.421.011a2.6,2.6,0,0,1-.008-.359c.024-.18.034-.5.1-.51a.776.776,0,0,1,.611.085c.684.645,1.329,1.332,1.992,2a2.8,2.8,0,0,0,.312.239l.108-.106c-.243-.291-.491-.578-.729-.873a3.2,3.2,0,0,1-.682-.908,2.352,2.352,0,0,0-1.054-1.222c-.4-.289-.931-.495-1.183-.882a4.853,4.853,0,0,0-.8-1.135c-.035-.03-.04-.107-.041-.162-.021-.748-.056-1.5-.053-2.243a13.432,13.432,0,0,0,.074-2.476c-.017-.1-.015-.2-.032-.292a5.744,5.744,0,0,1-.172-1.024q.054-3.37.159-6.739a9.433,9.433,0,0,1,.174-1.627,1.287,1.287,0,0,1,.719-.953c.03-1.226.053-2.462.095-3.7.011-.319.113-.635.116-.954.007-.631-.027-1.262-.041-1.893a5.043,5.043,0,0,0,0-.6.735.735,0,0,0-.182-.443c-.065-.053-.278.051-.411.113-.259.121-.506.269-.764.395s-.386.031-.383-.249.013-.564.019-.8l-.789.27c.006.547-.371.725-.926.708-.849-.026-1.7-.028-2.548-.06a1.093,1.093,0,0,1-.376-.161A1.451,1.451,0,0,1,72.8,174c-.575-.008-1.152-.023-1.724-.08-1.059-.106-2.112-.306-3.172-.349a16.116,16.116,0,0,0-3.116.146c-.343.054-.638-.138-.977,0a3.649,3.649,0,0,1-.978.134c-.359.04-.666-.025-.76-.337a3.942,3.942,0,0,1-.738.129c-.417-.007-.832-.09-1.249-.1-.281,0-.562.1-.843.1-1.021,0-2.041-.043-3.061-.028-.2,0-.518.167-.553.312a.945.945,0,0,0,.2.686,3.363,3.363,0,0,0,.752.708q1.446,1.017,2.92,1.992c1.6,1.066,3.222,2.112,4.821,3.186a3.353,3.353,0,0,1,.839.778.627.627,0,0,0,.693.33,1.994,1.994,0,0,1-.513.509c-.3.187-.663.271-.8.668-.029.086-.191.147-.3.181-.163.049-.343.04-.5.093-1.117.373-2.234.744-3.343,1.14a1.513,1.513,0,0,0-.964.764,1.552,1.552,0,0,1-.878.61c-.993.326-2.011.574-3.014.871a.77.77,0,0,0-.587,1.057.423.423,0,0,0,.441.379,3.159,3.159,0,0,1,1.01.173c1.083.459,2.147.963,3.216,1.455,1.152.531,2.311,1.047,3.448,1.608,1.083.535,2.142,1.121,3.269,1.714-.179.13-.31.236-.451.325-.728.462-1.46.918-2.188,1.38-.665.422-1.336.836-1.988,1.276-.749.5-1.483,1.032-2.221,1.553-.82.579-1.63,1.174-2.461,1.737C56.488,199.466,55.926,199.779,55.378,200.113Z" transform="translate(-54.904 -173.083)" fill="#fff"/>
<path id="Path_43" data-name="Path 43" d="M55.378,200.113c.548-.334,1.11-.647,1.64-1.006.831-.563,1.64-1.158,2.461-1.737.738-.521,1.472-1.048,2.221-1.553.653-.44,1.323-.854,1.988-1.276.728-.462,1.46-.918,2.188-1.38.141-.089.272-.2.451-.325-1.127-.593-2.185-1.179-3.269-1.714-1.137-.561-2.3-1.077-3.448-1.608-1.069-.492-2.133-1-3.216-1.455a3.159,3.159,0,0,0-1.01-.173.423.423,0,0,1-.441-.379.77.77,0,0,1,.587-1.057c1-.3,2.021-.546,3.014-.871a1.552,1.552,0,0,0,.878-.61,1.513,1.513,0,0,1,.964-.764c1.109-.4,2.226-.767,3.343-1.14.161-.054.341-.045.5-.093.112-.034.274-.095.3-.181.134-.4.5-.48.8-.668a1.994,1.994,0,0,0,.513-.509.627.627,0,0,1-.693-.33,3.353,3.353,0,0,0-.839-.778c-1.6-1.074-3.217-2.12-4.821-3.186q-1.472-.978-2.92-1.992a3.363,3.363,0,0,1-.752-.708.945.945,0,0,1-.2-.686c.035-.145.357-.309.553-.312,1.02-.016,2.041.027,3.061.028.281,0,.562-.108.843-.1.417.006.832.089,1.249.1a3.942,3.942,0,0,0,.738-.129c.094.311.4.377.76.337a3.649,3.649,0,0,0,.978-.134c.34-.141.634.051.977,0a16.116,16.116,0,0,1,3.116-.146c1.06.043,2.113.244,3.172.349.572.057,1.149.073,1.724.08a1.451,1.451,0,0,0,.448-.157,1.093,1.093,0,0,0,.376.161c.849.032,1.7.034,2.548.06.555.017.932-.161.926-.708l.789-.27c-.006.235-.015.516-.019.8s.127.374.383.249.5-.273.764-.395c.133-.062.347-.166.411-.113a.735.735,0,0,1,.182.443,5.043,5.043,0,0,1,0,.6c.014.631.048,1.262.041,1.893,0,.318-.105.635-.116.954-.042,1.235-.065,2.471-.095,3.7a1.287,1.287,0,0,0-.719.953,9.433,9.433,0,0,0-.174,1.627q-.1,3.369-.159,6.739a5.744,5.744,0,0,0,.172,1.024c.016.1.015.2.032.292a13.432,13.432,0,0,1-.074,2.476c0,.748.032,1.5.053,2.243,0,.056.006.132.041.162a4.853,4.853,0,0,1,.8,1.135c.252.387.779.593,1.183.882a2.352,2.352,0,0,1,1.054,1.222,3.2,3.2,0,0,0,.682.908c.238.295.486.582.729.873l-.108.106a2.8,2.8,0,0,1-.312-.239c-.663-.668-1.308-1.355-1.992-2a.776.776,0,0,0-.611-.085c-.07.015-.08.33-.1.51a2.6,2.6,0,0,0,.008.359c-.149,0-.286,0-.421-.011s-.292-.079-.382-.025c-.515.309-1.037.021-1.55.028a12.626,12.626,0,0,1-1.431-.229c-.359-.037-.744-.052-.963.357a.239.239,0,0,1-.227.03c-.377-.242-.769-.1-1.165-.1-.907,0-1.814-.088-2.722-.108-.463-.011-.928.042-1.393.072a3.884,3.884,0,0,1-1.248.064,11.194,11.194,0,0,0-1.672-.154,11.472,11.472,0,0,0-1.648.087c-.539,0-1.079-.06-1.619-.09a.72.72,0,0,0-.322.013,4.322,4.322,0,0,1-2.062.305c-1.6-.069-3.2-.131-4.8-.211a8.773,8.773,0,0,1-.969-.154Zm21.763-6.594c.007-.274.007-.544.022-.814.009-.154.038-.307.061-.46a1.209,1.209,0,0,0,.045-.281c-.036-.357-.119-.711-.122-1.067s.094-.736.1-1.1q.011-2.094-.018-4.189c0-.278-.1-.553-.112-.832a5.632,5.632,0,0,1,.022-.83c.016-.183.1-.362.107-.544q.068-2.265.114-4.531c.021-.994.031-1.988.042-2.983,0-.368-.064-.711-.5-.783a2.892,2.892,0,0,0-.937-.057,8.342,8.342,0,0,1-2.322.155c-1.045-.11-2.1-.156-3.15-.179s-2.125.016-3.188.023c-.673,0-1.348-.019-2.02,0-.451.015-.9.093-1.349.137-.514.051-1.031.078-1.54.158a.48.48,0,0,0-.276.336,5.587,5.587,0,0,0-.033.832l-.135.027c-.081-.288-.147-.582-.25-.862-.041-.111-.161-.268-.251-.272-.755-.04-1.512-.049-2.268-.067l-.005.126c.391.289.776.587,1.176.862s.822.528,1.233.793c.651.42.65.421,1.025-.241.008-.014.041-.015.076-.026.046.106.1.209.136.314a3.734,3.734,0,0,0,1.608,1.945c.272.159.5.4.777.541a11.691,11.691,0,0,0,2.481,1.306.357.357,0,0,1,.2.15c.3.488.613.547,1.079.2.074-.055.192-.119.263-.1.338.107.667.242,1.015.373l.034.357-1.63,1.16-.491-.541c-.119.163-.183.347-.3.4-.327.145-.264.306-.1.529q1.21,1.622,2.407,3.254a2,2,0,0,1,.179.411,11.369,11.369,0,0,1-1.079-1.074c-.468-.5-.963-.978-1.406-1.5-.3-.348-.516-.761-.781-1.137a.832.832,0,0,0-1.286-.037c-.144.156-.473.372-.557.317-.477-.315-.523.088-.677.352l-.087-.138c.042.354-.2.322-.439.358a6.463,6.463,0,0,0-.832.223,29.519,29.519,0,0,0-4.782,2.256,4.965,4.965,0,0,0-.626.454,59.169,59.169,0,0,0,9.841,4.568l-.031.387c.111.014.2.019.279.035.206.039.486-.067.51.305.023.342-.105.484-.45.535-.2.03-.508.165-.556.318-.105.34-.357.423-.614.53-.2.083-.422.111-.613.206-.367.183-.916.173-.9.8,0,.038-.076.08-.12.116-.7.555-1.387,1.123-2.1,1.662-.743.564-1.506,1.1-2.262,1.647-.209.15-.432.282-.637.437-.187.142-.359.3-.614.523.674.063,1.2.148,1.724.148.453,0,.907-.142,1.36-.139,1.913.01,3.831-.03,5.735.105a22.493,22.493,0,0,0,4.773-.112,6.513,6.513,0,0,1,1.568-.066c.8.074,1.608.063,2.414.08.536.011.534.006.487-.547-.008-.1.008-.2.01-.3a5.305,5.305,0,0,0-.8-3.536l.663.4-.121-1.97Zm2.064,5.616.942.023.022-.107-1.3-1.031Zm-.728-22.305.176.012c.082-.431.181-.86.234-1.295.012-.095-.138-.209-.214-.315-.1.09-.284.18-.283.269C78.392,175.945,78.443,176.387,78.477,176.83Zm.311.2-.2,0a3.2,3.2,0,0,0,.153,1.76Z" transform="translate(-54.904 -173.083)"/>
<path id="Path_44" data-name="Path 44" d="M86.807,199.99l.191-.165.121,1.97-.663-.4a5.305,5.305,0,0,1,.8,3.536c0,.1-.018.2-.01.3.047.552.049.558-.487.547-.805-.017-1.613-.006-2.414-.08a6.512,6.512,0,0,0-1.568.066A22.5,22.5,0,0,1,78,205.88c-1.9-.135-3.823-.095-5.735-.1-.453,0-.907.14-1.36.139-.525,0-1.05-.086-1.724-.148.255-.219.427-.381.614-.523.2-.155.428-.286.637-.437.757-.546,1.519-1.083,2.262-1.647.71-.539,1.4-1.106,2.1-1.662.044-.036.121-.078.12-.116-.011-.623.538-.613.9-.8.191-.1.414-.123.613-.206.256-.107.508-.19.614-.53.048-.153.352-.289.556-.318.345-.051.473-.193.45-.535-.025-.372-.3-.266-.51-.305-.083-.016-.168-.021-.279-.035l.031-.387a59.167,59.167,0,0,1-9.841-4.568,4.967,4.967,0,0,1,.626-.454,29.522,29.522,0,0,1,4.782-2.256,6.468,6.468,0,0,1,.832-.223c.235-.036.481,0,.439-.358l.087.138c.154-.264.2-.667.677-.352.084.055.413-.16.557-.317a.832.832,0,0,1,1.286.037c.265.376.485.788.781,1.137.443.522.938,1,1.406,1.5a11.37,11.37,0,0,0,1.079,1.074,1.994,1.994,0,0,0-.179-.411q-1.2-1.633-2.407-3.254c-.166-.222-.229-.384.1-.529.122-.054.186-.237.3-.4l.491.541,1.63-1.16-.034-.357c-.348-.131-.677-.266-1.015-.373-.071-.023-.189.041-.263.1-.465.345-.779.286-1.079-.2a.357.357,0,0,0-.2-.15,11.693,11.693,0,0,1-2.481-1.306c-.279-.14-.506-.383-.777-.541a3.733,3.733,0,0,1-1.608-1.945c-.041-.105-.09-.208-.136-.314-.035.011-.068.012-.076.026-.375.661-.374.661-1.025.241-.411-.265-.83-.516-1.233-.793s-.785-.574-1.176-.862l.005-.126c.756.018,1.513.027,2.268.067.091,0,.211.162.251.272.1.28.169.573.25.862l.135-.027a5.587,5.587,0,0,1,.033-.832.48.48,0,0,1,.276-.336c.509-.08,1.027-.107,1.54-.158.45-.045.9-.122,1.349-.137.673-.023,1.347,0,2.02,0,1.063-.007,2.126-.047,3.188-.023s2.105.069,3.15.179a8.342,8.342,0,0,0,2.322-.155,2.893,2.893,0,0,1,.937.057c.435.072.5.415.5.783-.011.994-.021,1.989-.042,2.983q-.048,2.266-.114,4.531c-.005.182-.091.361-.107.544a5.633,5.633,0,0,0-.022.83c.017.279.109.554.112.832q.027,2.094.018,4.189c0,.368-.1.737-.1,1.1s.087.711.122,1.067a1.209,1.209,0,0,1-.045.281c-.023.153-.052.306-.061.46C86.814,199.446,86.813,199.716,86.807,199.99Z" transform="translate(-64.57 -179.554)" fill="#f5613c"/>
<path id="Path_45" data-name="Path 45" d="M159.549,282.74l-.335-1.115,1.3,1.031-.022.107Z" transform="translate(-135.249 -256.688)" fill="#fff"/>
<path id="Path_46" data-name="Path 46" d="M157.214,184.038c-.034-.443-.085-.885-.087-1.328,0-.089.184-.179.283-.269.076.105.225.22.214.315-.053.435-.152.864-.234,1.295Z" transform="translate(-133.641 -180.291)" fill="#fff"/>
<path id="Path_47" data-name="Path 47" d="M157.954,190.264l-.043,1.763a3.2,3.2,0,0,1-.153-1.76Z" transform="translate(-134.071 -186.317)" fill="#fff"/>
</g>
<g id="Group_64" data-name="Group 64" transform="translate(77.162 121.619)">
<path id="Path_48" data-name="Path 48" d="M178.813,293.443l.162.025a8.767,8.767,0,0,0,.154-.969c.08-1.6.142-3.2.211-4.8a4.325,4.325,0,0,0-.306-2.062.719.719,0,0,1-.013-.322c.03-.54.094-1.08.09-1.619a11.509,11.509,0,0,1-.087-1.648,11.2,11.2,0,0,1,.154-1.672,3.885,3.885,0,0,0-.064-1.248c-.029-.464-.082-.929-.072-1.393.021-.908.107-1.814.108-2.722,0-.4-.143-.788.1-1.165a.238.238,0,0,0-.03-.227c-.409-.218-.394-.6-.356-.963a12.642,12.642,0,0,0,.229-1.431c-.007-.513.281-1.035-.028-1.55-.054-.09.017-.253.025-.382s.008-.272.011-.421a2.6,2.6,0,0,1-.359.008c-.18-.024-.5-.034-.51-.1a.776.776,0,0,1,.085-.611c.645-.684,1.332-1.329,2-1.992a2.848,2.848,0,0,0,.239-.312l-.106-.109c-.291.244-.578.491-.873.729a3.2,3.2,0,0,1-.908.682,2.353,2.353,0,0,0-1.222,1.054,4.313,4.313,0,0,1-.881,1.183,4.862,4.862,0,0,0-1.135.8c-.03.035-.107.04-.162.041-.748.021-1.5.056-2.244.053a13.453,13.453,0,0,0-2.476-.074c-.1.017-.2.016-.292.032a5.747,5.747,0,0,1-1.024.172q-3.37-.055-6.739-.16a9.409,9.409,0,0,1-1.627-.174,1.287,1.287,0,0,1-.953-.719c-1.226-.03-2.462-.053-3.7-.095-.319-.011-.635-.113-.954-.116-.631-.007-1.262.027-1.893.041a5.1,5.1,0,0,0-.6,0,.735.735,0,0,0-.443.182c-.053.065.051.278.113.411.121.259.269.506.395.763s.031.386-.249.383-.564-.013-.8-.019l.27.789c.547-.006.725.371.708.926-.026.849-.028,1.7-.059,2.549a1.1,1.1,0,0,1-.161.376,1.448,1.448,0,0,1,.157.448c-.008.575-.023,1.152-.08,1.723-.106,1.059-.307,2.112-.35,3.172a16.121,16.121,0,0,0,.146,3.117c.053.343-.138.638,0,.977a3.651,3.651,0,0,1,.134.978c.04.359-.025.666-.337.76a3.948,3.948,0,0,1,.129.738c-.007.417-.09.833-.1,1.249,0,.28.1.562.1.843,0,1.021-.043,2.041-.028,3.061,0,.2.167.518.312.553a.947.947,0,0,0,.686-.2,3.362,3.362,0,0,0,.708-.752q1.017-1.446,1.992-2.92c1.066-1.6,2.112-3.222,3.186-4.821a3.348,3.348,0,0,1,.778-.839.627.627,0,0,0,.33-.693,1.986,1.986,0,0,1,.509.513c.187.3.271.663.668.8.086.029.147.191.181.3.049.163.04.343.093.5.373,1.117.744,2.234,1.14,3.343a1.514,1.514,0,0,0,.764.964,1.553,1.553,0,0,1,.61.878c.326.993.574,2.011.871,3.013a.77.77,0,0,0,1.057.588.423.423,0,0,0,.379-.441,3.167,3.167,0,0,1,.173-1.009c.46-1.083.963-2.147,1.455-3.216.531-1.152,1.047-2.311,1.608-3.448.535-1.083,1.12-2.142,1.714-3.269.13.179.235.31.325.451.462.728.918,1.46,1.38,2.188.422.665.836,1.335,1.276,1.988.5.749,1.032,1.483,1.553,2.221.579.821,1.174,1.63,1.737,2.461C178.166,292.333,178.479,292.895,178.813,293.443Z" transform="translate(-151.783 -265.761)" fill="#fff"/>
<path id="Path_49" data-name="Path 49" d="M178.813,293.443c-.334-.548-.647-1.11-1.006-1.64-.563-.831-1.158-1.64-1.737-2.461-.521-.738-1.048-1.472-1.553-2.221-.44-.653-.854-1.324-1.276-1.988-.462-.728-.918-1.46-1.38-2.188-.089-.141-.2-.272-.325-.451-.593,1.127-1.179,2.185-1.714,3.269-.562,1.137-1.078,2.3-1.608,3.448-.492,1.069-1,2.133-1.455,3.216a3.167,3.167,0,0,0-.173,1.009.423.423,0,0,1-.379.441.77.77,0,0,1-1.057-.588c-.3-1-.546-2.021-.871-3.013a1.553,1.553,0,0,0-.61-.878,1.514,1.514,0,0,1-.764-.964c-.4-1.109-.767-2.226-1.14-3.343-.054-.16-.045-.341-.093-.5-.034-.112-.095-.274-.181-.3-.4-.134-.48-.5-.668-.8a1.986,1.986,0,0,0-.509-.513.627.627,0,0,1-.33.693,3.348,3.348,0,0,0-.778.839c-1.074,1.6-2.12,3.217-3.186,4.821q-.978,1.472-1.992,2.92a3.362,3.362,0,0,1-.708.752.947.947,0,0,1-.686.2c-.145-.035-.309-.357-.312-.553-.016-1.02.027-2.041.028-3.061,0-.281-.108-.563-.1-.843.006-.417.089-.832.1-1.249a3.948,3.948,0,0,0-.129-.738c.311-.094.377-.4.337-.76a3.651,3.651,0,0,0-.134-.978c-.141-.339.051-.634,0-.977a16.121,16.121,0,0,1-.146-3.117c.043-1.06.244-2.113.35-3.172.057-.572.072-1.148.08-1.723a1.448,1.448,0,0,0-.157-.448,1.1,1.1,0,0,0,.161-.376c.032-.849.033-1.7.059-2.549.017-.555-.161-.932-.708-.926l-.27-.789c.235.006.516.015.8.019s.374-.127.249-.383-.273-.5-.395-.763c-.062-.133-.166-.347-.113-.411a.735.735,0,0,1,.443-.182,5.1,5.1,0,0,1,.6,0c.631-.014,1.262-.048,1.893-.041.318,0,.635.105.954.116,1.235.042,2.471.065,3.7.095a1.287,1.287,0,0,0,.953.719,9.409,9.409,0,0,0,1.627.174q3.369.1,6.739.16a5.747,5.747,0,0,0,1.024-.172c.1-.016.2-.015.292-.032a13.453,13.453,0,0,1,2.476.074c.748,0,1.5-.032,2.244-.053.056,0,.132-.006.162-.041a4.862,4.862,0,0,1,1.135-.8,4.313,4.313,0,0,0,.881-1.183,2.353,2.353,0,0,1,1.222-1.054,3.2,3.2,0,0,0,.908-.682c.295-.238.582-.486.873-.729l.106.109a2.848,2.848,0,0,1-.239.312c-.668.663-1.355,1.308-2,1.992a.776.776,0,0,0-.085.611c.015.07.33.08.51.1a2.6,2.6,0,0,0,.359-.008c0,.15,0,.286-.011.421s-.079.292-.025.382c.309.515.021,1.037.028,1.55a12.642,12.642,0,0,1-.229,1.431c-.037.359-.052.744.356.963a.238.238,0,0,1,.03.227c-.242.377-.1.769-.1,1.165,0,.907-.087,1.814-.108,2.722-.011.463.042.929.072,1.393a3.885,3.885,0,0,1,.064,1.248,11.2,11.2,0,0,0-.154,1.672,11.509,11.509,0,0,0,.087,1.648c0,.539-.06,1.079-.09,1.619a.719.719,0,0,0,.013.322,4.325,4.325,0,0,1,.306,2.062c-.07,1.6-.131,3.2-.211,4.8a8.767,8.767,0,0,1-.154.969Zm-6.594-21.763c-.274-.007-.544-.007-.813-.022-.154-.008-.307-.038-.46-.061a1.226,1.226,0,0,0-.282-.045c-.357.036-.711.119-1.067.122s-.736-.094-1.1-.1q-2.094-.011-4.189.018c-.278,0-.553.1-.832.112a5.667,5.667,0,0,1-.83-.022c-.183-.016-.361-.1-.543-.107q-2.266-.067-4.531-.114c-.994-.021-1.988-.031-2.983-.042-.368,0-.711.064-.783.5a2.9,2.9,0,0,0-.057.937,8.353,8.353,0,0,1,.156,2.322c-.11,1.045-.156,2.1-.179,3.15s.016,2.125.023,3.188c0,.674-.019,1.348,0,2.021.015.451.093.9.137,1.349.051.514.078,1.031.158,1.54a.479.479,0,0,0,.336.276,5.566,5.566,0,0,0,.832.033l.026.135c-.288.081-.582.147-.862.25-.111.041-.267.161-.272.251-.04.755-.049,1.512-.067,2.268l.126.005c.289-.392.587-.776.863-1.177s.528-.822.793-1.233c.42-.651.421-.65-.241-1.025-.014-.008-.015-.041-.026-.076.106-.046.209-.1.314-.136a3.734,3.734,0,0,0,1.945-1.608c.159-.272.4-.5.541-.777a11.693,11.693,0,0,0,1.306-2.481.358.358,0,0,1,.151-.2c.488-.3.547-.613.2-1.079-.055-.074-.119-.192-.1-.263.107-.338.242-.667.373-1.015l.357-.034,1.16,1.63-.541.491c.163.119.347.183.4.3.145.327.306.264.529.1q1.622-1.21,3.254-2.408a2,2,0,0,1,.411-.178,11.4,11.4,0,0,1-1.074,1.079c-.5.468-.978.963-1.5,1.406-.348.3-.761.516-1.137.781a.832.832,0,0,0-.037,1.286c.156.144.372.473.317.557-.315.477.088.523.352.677l-.138.087c.354-.042.322.2.358.439a6.479,6.479,0,0,0,.223.832,29.508,29.508,0,0,0,2.256,4.782,4.964,4.964,0,0,0,.454.627,59.179,59.179,0,0,0,4.568-9.841l.387.031c.014-.111.019-.2.035-.279.039-.206-.067-.485.305-.51.341-.023.484.105.535.45.03.2.165.508.318.556.34.106.423.358.53.614.083.2.111.422.206.612.183.367.173.916.8.9.038,0,.081.076.116.12.556.7,1.123,1.386,1.662,2.1.564.743,1.1,1.506,1.647,2.263.15.209.282.432.437.637.142.187.3.359.523.614.063-.674.148-1.2.148-1.724,0-.454-.142-.907-.139-1.361.01-1.912-.031-3.83.1-5.735a22.493,22.493,0,0,0-.112-4.773,6.509,6.509,0,0,1-.066-1.568c.074-.8.063-1.609.08-2.414.011-.535.006-.534-.547-.487-.1.008-.2-.008-.3-.01a5.307,5.307,0,0,0-3.536.8l.4-.663-1.97.121Zm5.616-2.064.023-.942-.107-.023-1.031,1.3Zm-22.305.727.012-.176c-.431-.082-.86-.181-1.295-.234-.095-.012-.209.138-.314.213.09.1.18.284.269.283C154.645,270.429,155.087,270.378,155.53,270.344Zm.2-.311,0,.2a3.194,3.194,0,0,0,1.76-.153Z" transform="translate(-151.783 -265.761)"/>
<path id="Path_50" data-name="Path 50" d="M178.69,289.984l-.165-.191,1.97-.121-.4.663a5.308,5.308,0,0,1,3.536-.8c.1,0,.2.018.3.01.552-.047.558-.049.547.487-.017.805-.006,1.614-.08,2.414a6.511,6.511,0,0,0,.066,1.568,22.494,22.494,0,0,1,.112,4.773c-.135,1.9-.095,3.823-.1,5.735,0,.453.14.907.139,1.361,0,.525-.086,1.05-.148,1.724-.219-.255-.381-.427-.523-.614-.155-.205-.287-.428-.437-.637-.546-.757-1.083-1.52-1.647-2.263-.539-.71-1.106-1.4-1.662-2.1-.035-.044-.078-.121-.116-.12-.623.011-.613-.538-.8-.9-.095-.191-.123-.414-.206-.612-.107-.256-.19-.508-.53-.614-.153-.048-.289-.352-.318-.556-.051-.345-.193-.473-.535-.45-.372.025-.267.3-.305.51-.016.083-.021.168-.035.279l-.387-.031a59.178,59.178,0,0,1-4.568,9.841,4.964,4.964,0,0,1-.454-.627,29.508,29.508,0,0,1-2.256-4.782,6.477,6.477,0,0,1-.223-.832c-.036-.235,0-.481-.358-.439l.138-.087c-.264-.154-.667-.2-.352-.677.055-.084-.16-.413-.317-.557a.832.832,0,0,1,.037-1.286c.376-.265.788-.485,1.137-.781.522-.443,1-.938,1.5-1.406a11.4,11.4,0,0,0,1.074-1.079,2,2,0,0,0-.411.179q-1.633,1.2-3.254,2.408c-.222.166-.384.229-.529-.1-.054-.122-.237-.186-.4-.3l.541-.491-1.16-1.63-.357.034c-.131.348-.266.677-.373,1.015-.023.071.041.189.1.263.345.465.286.778-.2,1.079a.358.358,0,0,0-.151.2,11.691,11.691,0,0,1-1.306,2.481c-.14.279-.383.505-.541.777a3.734,3.734,0,0,1-1.945,1.608c-.105.041-.208.09-.314.136.012.035.012.068.026.076.661.375.661.374.241,1.025-.265.411-.516.831-.793,1.233s-.574.785-.863,1.177l-.126-.005c.018-.756.028-1.513.067-2.268,0-.091.162-.211.272-.251.28-.1.573-.169.862-.25l-.026-.135a5.558,5.558,0,0,1-.832-.033.479.479,0,0,1-.336-.276c-.08-.509-.107-1.026-.158-1.54-.045-.45-.122-.9-.137-1.349-.023-.673,0-1.347,0-2.021-.007-1.063-.047-2.126-.023-3.188s.069-2.105.179-3.15a8.353,8.353,0,0,0-.156-2.322,2.9,2.9,0,0,1,.057-.937c.072-.435.415-.5.783-.5.994.011,1.989.021,2.983.042q2.266.048,4.531.114c.182.005.36.091.543.107a5.659,5.659,0,0,0,.83.022c.279-.017.554-.109.832-.112q2.094-.027,4.189-.018c.369,0,.737.1,1.1.1s.711-.087,1.067-.122a1.222,1.222,0,0,1,.282.045c.153.023.306.053.46.061C178.146,289.977,178.416,289.978,178.69,289.984Z" transform="translate(-158.254 -284.065)" fill="#f3d124"/>
<path id="Path_51" data-name="Path 51" d="M261.441,279.306l-1.115.335,1.031-1.3.107.023Z" transform="translate(-235.388 -275.451)" fill="#fff"/>
<path id="Path_52" data-name="Path 52" d="M162.739,284.332c-.442.034-.885.085-1.328.087-.089,0-.179-.184-.269-.283.105-.075.22-.225.314-.213.435.053.864.152,1.295.234Z" transform="translate(-158.992 -279.75)" fill="#fff"/>
<path id="Path_53" data-name="Path 53" d="M168.964,284.355l1.763.043a3.193,3.193,0,0,1-1.76.153Z" transform="translate(-165.017 -280.083)" fill="#fff"/>
</g>
<g id="Group_65" data-name="Group 65" transform="translate(83.56 105.109)">
<path id="Path_54" data-name="Path 54" d="M195.737,214.673c-.415.394-.831,1.028-1.443.722-3.083,3.5-8.244,2.865-12.159,1.531a11.271,11.271,0,0,1-1.225-12.356c.416-.612,1.75-1.749,1.946-.306-.722,3.914-1.859,7,0,10.716,2.887,1.837,7.741.722,10.606-1.028a8.105,8.105,0,0,0,1.247-6.714c-2.581-3.193-6.692-.088-9.885-.722-.722-1.444,1.225-2.056,2.252-2.471,3.3-.2,6.911-1.028,9.185,1.749C197.705,208.79,196.984,211.874,195.737,214.673Z" transform="translate(-179.63 -201.343)"/>
<path id="Path_55" data-name="Path 55" d="M279.441,215.783c-2.734-.722-5.751-2.909-6.123-5.926-.437-3.652-1.465-8.048-.459-11.24.547,0,1.312-.022,1.378.722.481,4.658-.634,10.781,3.477,13.974,2.821.656,3.018-2.909,4.2-4.57,0-3.106.831-6.123.175-9.229.656-.634,1.64-.809,2.012.2C284.186,205.176,283.552,215.761,279.441,215.783Z" transform="translate(-251.11 -197.532)"/>
<path id="Path_56" data-name="Path 56" d="M354.66,200.075c-.525-1.465-1.181-.35-2.843-.853l-.722.328c-.525,5.445.109,9.753-.415,14.87-.525.416-1.356.219-2.1.219.109-5.445.831-10.584,1.05-15.833-1.465-2.187-7.654.437-4.811-3.236,2.712,1.771,6.386-1.465,8.376,1.465,1.684.109,2.93,1.99,4.724,1.246.94,1.378,3.871.328,3.236,2.843C358.859,202.7,357.284,199.244,354.66,200.075Zm.219-.94v.416s.678-.219.525-.416C355.272,198.982,354.879,199.135,354.879,199.135Z" transform="translate(-306.299 -195.186)"/>
<path id="Path_57" data-name="Path 57" d="M437.463,205.217a5.635,5.635,0,0,1,1.05,3.805,16.041,16.041,0,0,0-2.493,2.362c-1.115-.765-2.3,1.312-3.543.131-.2.131-.35.481-.612.35-2.143-2.428.481-5.6-.35-8.507.131-2.078-1.05-6.342.35-6.889,1.181-.459-.568,5.8,2.493,6.889a3.291,3.291,0,0,0,3.587-.831c.219-1.881.722-4.658-1.312-6.1-1.29-.919-2.909-1.859-4.505-1.028-.2-.35-.9-.569-.612-1.181,1.247-.7,2.69-.066,3.937.2a15.463,15.463,0,0,1,3.543,2.843c.612,1.946,1.159,4.221,0,6.1C438.491,204.168,437.945,204.452,437.463,205.217Zm-2.012,0c-.766-.2-1.378-.7-2.275-.481-.219,1.662-.35,3.587-.481,5.117,1.443.481,2.624-.7,3.805-1.531C437.113,207.076,436.086,206.18,435.452,205.217Z" transform="translate(-373.249 -193.899)"/>
<path id="Path_58" data-name="Path 58" d="M496.215,214.673c-.415.394-.831,1.028-1.443.722-3.083,3.5-8.244,2.865-12.159,1.531a11.271,11.271,0,0,1-1.225-12.356c.416-.612,1.75-1.749,1.946-.306-.722,3.914-1.859,7,0,10.716,2.887,1.837,7.741.722,10.606-1.028a8.105,8.105,0,0,0,1.247-6.714c-2.581-3.193-6.692-.088-9.885-.722-.722-1.444,1.225-2.056,2.252-2.471,3.3-.2,6.911-1.028,9.185,1.749C498.183,208.79,497.462,211.874,496.215,214.673Z" transform="translate(-411.073 -201.343)"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 32 KiB

+1
View File
@@ -0,0 +1 @@
<svg id="Calque_1" data-name="Calque 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 376.2 376.19"><title>outbo_rocket</title><path d="M444,72.21a1.48,1.48,0,0,0,0-.19,2.57,2.57,0,0,0-.06-.4,2.38,2.38,0,0,0,0-.26,1.29,1.29,0,0,0-.07-.21,1.61,1.61,0,0,0-.12-.33,1.84,1.84,0,0,0-.09-.25l-.13-.24-.15-.25-.14-.22-.2-.25-.17-.19c-.06-.06-.11-.12-.18-.18l-.25-.22-.19-.15-.21-.14a1.92,1.92,0,0,0-.31-.18,1.18,1.18,0,0,0-.19-.11l-.26-.1a2,2,0,0,0-.34-.12l-.2-.07a1.37,1.37,0,0,0-.28-.05A2.38,2.38,0,0,0,440,68a1.52,1.52,0,0,0-.21,0l-.42,0c-3.56-.17-28.39-.93-62.82,7.72l-1.38.35-1.59.41-2.24.6-1.08.3c-1.17.32-2.35.65-3.54,1h0C326.79,90,269.65,116.71,222.87,178.2a124.2,124.2,0,0,0-69.46,11c-37.12,17.7-65.79,53.42-85.23,106.16a4.48,4.48,0,0,0,7,5c.59-.48,57.29-46.29,99.17-33.91-1.77,4.57-3.49,9.25-5.15,14l-2.05-2a4.47,4.47,0,0,0-6.33,0l-12.3,12.3a4.47,4.47,0,0,0,0,6.33l9.81,9.81-15.9,15.9a4.47,4.47,0,0,0,0,6.33l40.42,40.42a4.48,4.48,0,0,0,6.33,0l15.9-15.9,9.81,9.8a4.48,4.48,0,0,0,6.33,0l12.3-12.3a4.47,4.47,0,0,0,0-6.33l-2-2q7.14-2.49,14-5.16c12.38,41.89-33.43,98.59-33.91,99.18a4.48,4.48,0,0,0,3.47,7.31,4.36,4.36,0,0,0,1.54-.28c52.75-19.43,88.47-48.1,106.17-85.23a124.16,124.16,0,0,0,11-69.45c61.5-46.78,88.23-103.94,99.83-143.85C445.7,103.75,444.07,73.48,444,72.21ZM82.29,284.13c18.28-42.63,43.45-71.8,75-86.84a115.07,115.07,0,0,1,59.48-10.75,362,362,0,0,0-39,71.6C143.56,247.48,102.65,270.37,82.29,284.13ZM186.06,360,152,325.94l12.73-12.73,34.09,34.09Zm32-6.09-9.8-9.8h0l-40.42-40.42h0l-9.81-9.81,6-6,56.12,56.12h0l3.91,3.91Zm96.62.79c-15,31.52-44.21,56.7-86.84,75,13.76-20.36,36.65-61.27,26-95.43a362.14,362.14,0,0,0,71.61-39A115.2,115.2,0,0,1,314.71,354.73Zm11.59-71.15c-23.17,17.31-50,32.12-79.74,44h0c-7.09,2.84-14.51,5.56-22.08,8.12l-48.18-48.18c2.56-7.56,5.28-15,8.12-22.09,11.89-29.74,26.7-56.57,44-79.74,43.78-58.6,97.26-85.21,136-97.29a76.77,76.77,0,0,0,59.14,59.14C411.51,186.32,384.9,239.8,326.3,283.58ZM435.14,77c0,.34,0,.7,0,1.08v.28c0,.33,0,.67,0,1,0,.09,0,.17,0,.26,0,.44,0,.91,0,1.39v.19c0,.41,0,.84,0,1.29,0,.15,0,.3,0,.45,0,.4,0,.81,0,1.23,0,.14,0,.28,0,.43,0,.55-.05,1.13-.08,1.72,0,.1,0,.2,0,.3q0,.75-.09,1.53c0,.19,0,.39,0,.58,0,.48-.06,1-.1,1.47,0,.18,0,.35,0,.53-.05.67-.1,1.36-.16,2.06l0,.32c-.05.6-.11,1.22-.17,1.84l-.06.67c-.05.56-.11,1.13-.17,1.71,0,.2,0,.41-.07.61-.08.78-.16,1.57-.26,2.37,0,.11,0,.22,0,.33-.08.7-.17,1.41-.26,2.13l-.09.74c-.09.64-.17,1.3-.27,1.95l-.09.67c-.12.88-.25,1.76-.39,2.67a2.51,2.51,0,0,1,0,.27q-.18,1.22-.39,2.46c0,.26-.08.52-.13.79-.12.71-.24,1.44-.37,2.17l-.12.72c-.18,1-.35,1.94-.54,2.93,0,.07,0,.14,0,.21-.17.92-.36,1.84-.54,2.77-.06.27-.11.55-.17.82-.16.79-.33,1.59-.5,2.39-.06.25-.11.5-.16.75-.23,1-.47,2.11-.71,3.18,0,.05,0,.09,0,.14-.23,1-.47,2-.72,3l-.21.85c-.21.85-.42,1.7-.65,2.56-.06.27-.13.54-.2.8-.28,1.09-.57,2.18-.88,3.28a67.87,67.87,0,0,1-53.06-53.06l3.36-.9.48-.12,3.06-.77.36-.09a238.46,238.46,0,0,1,54.82-7.13V77A0,0,0,0,1,435.14,77Z" transform="translate(-67.91 -67.9)"/><path d="M318.37,155.72a37.91,37.91,0,1,0,37.91,37.91A38,38,0,0,0,318.37,155.72Zm0,66.87a29,29,0,1,1,29-29A29,29,0,0,1,318.37,222.59Z" transform="translate(-67.91 -67.9)"/><path d="M153,359a4.47,4.47,0,0,0-6.33,0L94,411.72a4.48,4.48,0,0,0,6.33,6.33L153,365.33A4.47,4.47,0,0,0,153,359Z" transform="translate(-67.91 -67.9)"/><path d="M173.23,379.23a4.47,4.47,0,0,0-6.33,0l-34.43,34.43A4.47,4.47,0,1,0,138.8,420l34.43-34.42A4.47,4.47,0,0,0,173.23,379.23Z" transform="translate(-67.91 -67.9)"/><path d="M210.35,382.56a4.47,4.47,0,0,0-6.33,0l-36.08,36.08a4.48,4.48,0,0,0,3.17,7.64,4.43,4.43,0,0,0,3.16-1.31l36.08-36.08A4.47,4.47,0,0,0,210.35,382.56Z" transform="translate(-67.91 -67.9)"/><path d="M129.44,308a4.48,4.48,0,1,0-6.33-6.33L87,337.73a4.48,4.48,0,0,0,6.33,6.33Z" transform="translate(-67.91 -67.9)"/><path d="M98.34,379.53l34.43-34.43a4.48,4.48,0,1,0-6.33-6.33L92,373.2a4.48,4.48,0,0,0,6.33,6.33Z" transform="translate(-67.91 -67.9)"/></svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18">
<g id="photo_icon" data-name="photo icon" transform="translate(0)">
<path id="photo_icon-2" data-name="photo icon" d="M11221.5,1637.5l2.5,3.01,3.5-4.51,4.5,6h-14Zm12.5,5.5v-14a2.006,2.006,0,0,0-2-2h-14a2.005,2.005,0,0,0-2,2v14a2.005,2.005,0,0,0,2,2h14A2.006,2.006,0,0,0,11234,1643Z" transform="translate(-11216 -1627)" fill="#fff"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 437 B

+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 36 36">
<g id="arrow_small_button" data-name="arrow small button" transform="translate(-11662 -1616)">
<path id="placeholder" d="M18,0A18,18,0,1,1,0,18,18,18,0,0,1,18,0Z" transform="translate(11662 1616)" fill="rgba(245,140,57,0.75)"/>
<text id="_" data-name="+" transform="translate(11686 1641)" fill="#fff" font-size="20" font-family="Roboto-Medium, Roboto" font-weight="500"><tspan x="-11.143" y="0">+</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 520 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="12" viewBox="0 0 18 12">
<path id="poll_icon" data-name="poll icon" d="M12558,228h12v-2h-12Zm0-7v2h18v-2Zm0,12h6v-2h-6Z" transform="translate(-12558 -221)" fill="#d1d1d1"/>
</svg>

After

Width:  |  Height:  |  Size: 241 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="21" height="21" viewBox="0 0 21 21">
<path id="widgets_icon" data-name="widgets icon" d="M3525.5,226H3524v-4a2,2,0,0,0-2-2h-4v-1.5a2.5,2.5,0,1,0-5,0V220h-4a2,2,0,0,0-1.99,2v3.8h1.489a2.7,2.7,0,1,1,0,5.4H3507V235a2.006,2.006,0,0,0,2,2h3.8v-1.5a2.7,2.7,0,1,1,5.4,0V237h3.8a2,2,0,0,0,2-2v-4h1.5a2.5,2.5,0,1,0,0-5Z" transform="translate(-3507 -216)" fill="#e4943a"/>
</svg>

After

Width:  |  Height:  |  Size: 419 B

+3
View File
@@ -0,0 +1,3 @@
<svg id="search_icon" data-name="search icon" xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 13 13">
<path id="search_icon-2" data-name="search icon" d="M20097,1411a3,3,0,1,0-3-3A3,3,0,0,0,20097,1411Zm2.773,1.16a4.987,4.987,0,1,1,1.387-1.385l3.84,3.848-1.379,1.377Z" transform="translate(-20092 -1403)" fill="#ccc"/>
</svg>

After

Width:  |  Height:  |  Size: 350 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 15 15">
<g id="user_icon" data-name="user icon" transform="translate(-17610 -198)">
<path id="user_icon-2" data-name="user icon" d="M17617.5,208c-2.5,0-7.5,1.117-7.5,3.333V213h15v-1.667C17625,209.117,17620,208,17617.5,208Zm.5-2a4,4,0,1,0-4-4A4,4,0,0,0,17618,206Z" fill="#e6e6e6"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 377 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="2" height="20" viewBox="0 0 2 41">
<path id="Op_component_1" data-name="Op component 1" d="M0,0H2V41H0V0Z" fill="#f5f5f5"/>
</svg>

After

Width:  |  Height:  |  Size: 180 B

+8
View File
@@ -0,0 +1,8 @@
import React, { HTMLAttributes } from 'react';
import cx from 'classnames';
const BriefHeader: React.FC<HTMLAttributes<HTMLSpanElement>> = ({ className, ...props }) => {
return <span className={cx('brief__title', className)} {...props} />;
};
export default BriefHeader;
+21
View File
@@ -0,0 +1,21 @@
import React, { InputHTMLAttributes } from 'react';
import cx from 'classnames';
const ButtonCheckbox: React.FC<InputHTMLAttributes<HTMLInputElement>> = ({ children, className, ...props }) => (
<div className="button-checkbox">
<input type="checkbox" id={props.name} {...props} />
<label
htmlFor={props.id || props.name}
className={cx([
'button is-light-grey',
'button-checkbox',
{ 'button-checkbox--is-active': props.checked },
className,
])}
>
<span>{children}</span>
</label>
</div>
);
export default ButtonCheckbox;
+11
View File
@@ -0,0 +1,11 @@
import React, { InputHTMLAttributes } from 'react';
import cx from 'classnames';
const Checkbox: React.FC<InputHTMLAttributes<HTMLInputElement>> = ({ className, children, ...props }) => (
<>
<input type="checkbox" className={cx('form-checkbox', className)} id={props.name} {...props} />
<label htmlFor={props.id || props.name}>{children}</label>
</>
);
export default Checkbox;
+14
View File
@@ -0,0 +1,14 @@
import React from 'react';
import { ClimbingBoxLoader } from 'react-spinners';
export const ClimbingLoader = (): JSX.Element => {
return (
<div className="hero is-fullheight">
<div className="container columns has-items-centered">
<div className="column">
<ClimbingBoxLoader color="#f58c39" size={1.5} sizeUnit="rem" />
</div>
</div>
</div>
);
};
+36
View File
@@ -0,0 +1,36 @@
import React from 'react';
import DatePicker from 'react-datepicker';
import cx from 'classnames';
import 'react-datepicker/dist/react-datepicker.css';
import { CalendarIcon } from 'assets';
import { parse } from 'date-fns';
export type DatePickerChangeHandler = (
date: Date | null,
event: React.SyntheticEvent<Element, Event> | undefined
) => void;
interface DatePickerProps {
onChange: DatePickerChangeHandler;
selected: string;
disabled: boolean;
dataTestid?: string;
title?: string;
}
const Datepicker: React.FC<DatePickerProps> = ({ onChange, selected, disabled, dataTestid, title }) => {
return (
<div className="datepicker" data-testid={dataTestid}>
<DatePicker
onChange={onChange}
selected={parse(selected, 'yyyy-MM-dd', new Date())}
dateFormat="yyyy-MM-dd"
className={cx(['input', 'is-input-border'])}
disabled={disabled}
title={title}
/>
<CalendarIcon />
</div>
);
};
export default Datepicker;
+160
View File
@@ -0,0 +1,160 @@
import React, { useState, useCallback, useRef, useContext, useEffect } from 'react';
import { Caret } from '../assets/index';
import cx from 'classnames';
import useOnClickOutside from 'use-onclickoutside';
export type CurrentValue = {
value: number;
label: string;
};
type ContextValues = {
open: boolean;
disabled: boolean;
fullwidth: boolean;
current: CurrentValue | undefined;
onChange: (value: CurrentValue) => void;
toggleOpen: () => void;
};
type ItemProps = {
label: string;
value: number;
};
const DropdownContext = React.createContext<ContextValues>({
open: false,
fullwidth: false,
disabled: false,
current: undefined,
onChange: () => {},
toggleOpen: () => {},
});
const useDropdownContext = (): ContextValues => {
const context = useContext(DropdownContext);
if (!context) {
throw new Error(`NOPE`);
}
return context;
};
const Item: React.FC<ItemProps> = ({ label, value }) => {
const { onChange } = useDropdownContext();
return (
<a className="dropdown-item" onClick={(): void => onChange({ label, value })}>
{label}
</a>
);
};
const Label: React.FC<{ valueDataTestid?: string }> = ({ children, valueDataTestid }) => {
const { current, fullwidth, disabled } = useDropdownContext();
return (
<div className="dropdown-trigger">
<button
className={cx(['button', 'is-outlined', { 'is-fullwidth': fullwidth }])}
aria-haspopup="true"
aria-controls="dropdown-menu"
type="button"
disabled={disabled}
>
<span className={cx({ 'is-dimmed': !current })} data-testid={valueDataTestid}>
{current ? current.label : children}
</span>
<span className={cx('icon is-small', { 'is-dimmed': !current })}>
<Caret className={cx('caret--opened')} />
</span>
</button>
</div>
);
};
const Menu: React.FC = ({ children }) => {
return (
<div className="dropdown-menu" id="dropdown-menu" role="menu">
<div className="dropdown-content">{children}</div>
</div>
);
};
type CompoundComponents = {
Item: React.FC<ItemProps>;
Menu: React.FC;
Label: React.FC<{ valueDataTestid?: string }>;
};
type DropdownProps = {
fullwidth: boolean;
disabled: boolean;
onChange: (value: number) => void;
initial?: CurrentValue;
dataTestid?: string;
title?: string;
};
const Dropdown: React.FC<DropdownProps> & CompoundComponents = ({
onChange,
title,
disabled,
fullwidth,
children,
initial,
dataTestid,
}) => {
const [current, setCurrent] = useState<CurrentValue | undefined>(initial);
const [open, setOpen] = useState(false);
useEffect(() => {
setCurrent(initial);
}, [initial]);
const handleChange = useCallback(
({ value, label }) => {
if (!disabled) {
setCurrent({ value, label });
onChange && onChange(value);
}
},
[disabled, onChange]
);
const toggleOpen = useCallback(() => {
if (!disabled) {
setOpen((open) => !open);
}
}, [disabled]);
const node = useRef<HTMLHeadingElement>(null);
useOnClickOutside(node, () => setOpen(false));
return (
<DropdownContext.Provider
value={{
open,
fullwidth,
disabled,
current,
onChange: handleChange,
toggleOpen,
}}
>
<div
className={cx(['dropdown', open && 'is-active', { 'is-fullwidth': fullwidth }])}
onClick={toggleOpen}
ref={node}
data-testid={dataTestid}
title={title}
>
{children}
</div>
</DropdownContext.Provider>
);
};
Dropdown.Item = Item;
Dropdown.Menu = Menu;
Dropdown.Label = Label;
export default Dropdown;
+8
View File
@@ -0,0 +1,8 @@
import React, { HTMLAttributes } from 'react';
import cx from 'classnames';
const FieldInputError: React.FC<HTMLAttributes<HTMLDivElement>> = ({ className, ...props }) => {
return <div className={cx('error', className)} {...props} data-testid="inputError" />;
};
export default FieldInputError;
+7
View File
@@ -0,0 +1,7 @@
import React from 'react';
function HorizontalLine(): JSX.Element {
return <div className="horizontal-line" />;
}
export default HorizontalLine;
+27
View File
@@ -0,0 +1,27 @@
import React from 'react';
import cx from 'classnames';
type ModalFooterProps = {
action: any;
text: string;
cancelAction: () => void;
isSubmitting: boolean;
};
export const ModalFooter: React.FC<ModalFooterProps> = ({ action, text, cancelAction, isSubmitting }) => {
return (
<>
<button
className={cx({ 'is-loading': isSubmitting }, 'button is-orange-lighten mr-1')}
type="button"
onClick={async (): Promise<void> => {
await action();
}}
>
<span>{text}</span>
</button>
<button type="button" className="button is-light-grey" onClick={cancelAction}>
<span>Anuluj</span>
</button>
</>
);
};
+31
View File
@@ -0,0 +1,31 @@
import React, { InputHTMLAttributes } from 'react';
import cx from 'classnames';
import { FieldTextInputInterface } from 'interfaces/InputInterfaces';
import debounce from 'lodash.debounce';
interface SearchInput {
searchFunc: (searchValue: string) => void;
}
const SearchInput: React.FC<SearchInput & FieldTextInputInterface & InputHTMLAttributes<HTMLInputElement>> = ({
field,
form,
searchFunc,
className,
...props
}) => {
return (
<input
className={cx('input', className)}
{...field}
{...props}
title={props.value ? props.placeholder : undefined}
onChange={(e) => {
form.handleChange(e);
searchFunc(e.target.value);
}}
/>
);
};
export default SearchInput;
+35
View File
@@ -0,0 +1,35 @@
import React from 'react';
import { FieldProps } from 'formik';
type SelectProps = {
disabled: boolean;
dataTestid?: string;
title?: string;
};
const Select: React.FC<FieldProps & SelectProps> = ({ field, form, children, dataTestid, title, ...props }) => {
return (
<div className="row columns">
<div className="column has-items-right">
<span>Ilość produktów: </span>
</div>
<div className="column has-items-right is-narrow ml-2">
<div className="select is-input-border brief__input--select">
<select
value={field.value}
disabled={props.disabled}
onChange={(event: React.FormEvent<HTMLSelectElement>): void =>
form.setFieldValue(field.name, parseInt((event.target as HTMLInputElement).value, 10))
}
data-testid={dataTestid}
title={title}
>
{children}
</select>
</div>
</div>
</div>
);
};
export default Select;
+35
View File
@@ -0,0 +1,35 @@
import React, { useMemo } from 'react';
import cx from 'classnames';
import { useHistory } from 'react-router';
import useLocationParams from '../hooks/useLocationParams';
type StageBarLinkProps = {
target: string;
currentStage: number;
group: number;
index: number;
};
export const StageBarLink: React.FC<StageBarLinkProps> = ({ target, index, currentStage, group }) => {
const disabled = useMemo(() => group < index, [group, index]);
const active = useMemo(() => currentStage === index, [currentStage, index]);
const history = useHistory();
const [params, location] = useLocationParams();
return (
<div
onClick={!disabled ? (): void => history.push({ pathname: target, search: params.toString() }) : (): void => {}}
className={cx('stage-bar__box', {
'stage-bar__box--active': active,
})}
>
<span
className={cx('stage-bar__box__item', {
'stage-bar__box__item--active': active,
'stage-bar__box__item--disabled': disabled,
})}
>
{index}
</span>
</div>
);
};
+14
View File
@@ -0,0 +1,14 @@
import React, { TextareaHTMLAttributes } from 'react';
import cx from 'classnames';
const TextArea: React.FC<TextareaHTMLAttributes<HTMLTextAreaElement>> = ({ className, ...props }) => {
return (
<textarea
className={cx('textarea is-input-border is-mobile', className)}
{...props}
title={props.value ? props.placeholder : undefined}
/>
);
};
export default TextArea;
+15
View File
@@ -0,0 +1,15 @@
import React, { InputHTMLAttributes } from 'react';
import cx from 'classnames';
import { FieldTextInputInterface } from '../interfaces/InputInterfaces';
const TextInput: React.FC<InputHTMLAttributes<HTMLInputElement>> = ({ className, ...props }) => {
return (
<input
className={cx('input', 'is-input-border', className)}
{...props}
title={props.value ? props.placeholder : undefined}
/>
);
};
export default TextInput;
+7
View File
@@ -0,0 +1,7 @@
import React from 'react';
const PDFTemplate: React.FC = () => {
return <div></div>;
};
export default PDFTemplate;
Vendored Executable
+44
View File
@@ -0,0 +1,44 @@
// Set your brand colors
$orange: #f58c39;
$pink: #fa7c91;
$beige-light: #d0d1cd;
$beige-lighter: #eff0eb;
$bg-light:#fff;
// Update Bulma's global variables
$family-sans-serif: "Nunito", sans-serif;
$grey-dark: #b7b7b7 !default;
$grey-light: $beige-light;
$primary: $orange;
$link: $beige-light;
$link-hover-border: $beige-light;
$link-active-border: $beige-light;
$link-focus-border: $beige-light;
// Update some of Bulma's component variables
$body-background-color: $beige-lighter;
$control-border-width: 2px;
$input-border-color: transparent;
$input-shadow: none;
$avatar-size: 30px;
$options-dots-size: 20px;
$dropdown-height: 3rem;
// Import only what you need from Bulma
// @import "../node_modules/bulma/sass/utilities/_all.sass";
@import "./scss/CustomUtilities.scss";
@import "../node_modules/bulma/sass/base/_all.sass";
@import "../node_modules/bulma/sass/grid/_all.sass";
@import "../node_modules/bulma/sass/components/_all.sass";
@import "../node_modules/bulma/sass/form/_all.sass";
@import "../node_modules/bulma/sass/elements/_all.sass";
@import "../node_modules/bulma/sass/layout/_all.sass";
@import "../node_modules/bulma-steps-component/bulma-steps.sass";
@import "./scss/Datepicker.scss";
@import "../node_modules/bulma-tooltip/src/sass/index.sass";
+18
View File
@@ -0,0 +1,18 @@
import React from 'react';
interface DropdownItemProps {
text: string;
icon: JSX.Element;
logout?: any;
}
const DropdownItem: React.FC<DropdownItemProps> = ({ text, icon, logout = () => {} }) => {
return (
<a className="navbar-item" onClick={() => logout()}>
{icon}
<span className="ml-1">{text}</span>
</a>
);
};
export default DropdownItem;
+54
View File
@@ -0,0 +1,54 @@
import React from 'react';
import { OptionDots, OutboLogo, DocIcon } from 'assets';
import DropdownItem from './DropdownItem';
import { useSelector, useDispatch } from 'react-redux';
import { AppState } from 'modules/rootReducer';
import { UserActionTypes, logoutUser } from 'modules/users/actions';
import { Dispatch } from 'redux';
const Navbar: React.FC = () => {
const dispatch = useDispatch<Dispatch<UserActionTypes>>();
const user = useSelector((state: AppState) => state.users.currentUser);
return (
<nav className="navbar has-shadow" role="navigation" aria-label="main navigation">
<div className="navbar-brand">
<a className="navbar-item navbar-item__logo">
<OutboLogo style={{ maxHeight: '2.75rem' }} className="navbar-item__logo" />
</a>
</div>
<div className="navbar-menu">
<div className="navbar-end">
{user && (
<div className="navbar-item">
<div className="avatar">{user.photo && <img src={user.photo} alt={'avatar'} />}</div>
<div className="navbar-item rows" style={{ paddingTop: 0, paddingBottom: 0 }}>
<div>{user.fullName}</div>
<div className="font-small">{user.organization.name}</div>
</div>
</div>
)}
</div>
</div>
<div className="navbar-item has-dropdown is-hoverable">
<div className="navbar-item">
<div className="navbar-item__dots">
<OptionDots />
</div>
<div className="navbar-dropdown is-right">
<DropdownItem
text="Logout"
icon={<DocIcon />}
logout={(): void => {
logoutUser(dispatch);
}}
/>
</div>
</div>
</div>
</nav>
);
};
export default Navbar;
+1
View File
@@ -0,0 +1 @@
export { default } from './Navbar';
+19
View File
@@ -0,0 +1,19 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'modules/rootReducer';
interface StoreProviderProps {
selectors: any;
component: React.ComponentType;
}
const StoreProvider: React.FC<StoreProviderProps> = ({ component: Component, selectors, ...props }) => {
// const result = useSelector((state:AppState)=> selectors.map((selector:any)=>selector(state)));
const result = useSelector((state: AppState) =>
Object.keys(selectors).reduce((previous, current) => ({ ...previous, [current]: selectors[current](state) }), {})
);
return <Component {...props} {...result} />;
};
export default StoreProvider;
+2
View File
@@ -0,0 +1,2 @@
export { default as Login } from '../pages/Login';
export { default as Main } from '../pages/Main';
+1
View File
@@ -0,0 +1 @@
import '@testing-library/jest-dom/extend-expect';
+24
View File
@@ -0,0 +1,24 @@
import React from 'react';
import { Route, Redirect, useLocation, RouteProps } from 'react-router-dom';
import { TOKEN } from 'API/axiosConfig';
import { parse } from 'query-string';
export const PrivateRoute: React.FC<RouteProps> = ({ children, ...rest }) => {
const location = useLocation();
return (
<Route
{...rest}
render={(props) =>
localStorage.getItem(TOKEN) ? children : <Redirect to={`/login?next=${location.pathname}`} />
}
/>
);
};
export const NotAuthorizedRoute: React.FC<RouteProps> = ({ children, ...rest }) => {
const location = useLocation();
const query = parse(location.search);
const redirectUrl = query.next ? query : '/';
return (
<Route {...rest} render={(props) => (!localStorage.getItem(TOKEN) ? children : <Redirect to={redirectUrl} />)} />
);
};
+25
View File
@@ -0,0 +1,25 @@
import { PhotoFormModel, BlankingFormModel, BlankingRequest, PhotoRequest } from 'interfaces/API/products';
const isProperPhoto = (maybeProperPhoto: PhotoFormModel): maybeProperPhoto is PhotoFormModel & { file: File } => {
return typeof maybeProperPhoto['file'] !== 'undefined' && maybeProperPhoto['file'] instanceof File;
};
const castToPhotoRequest = ({ file }: PhotoFormModel & { file: File }): PhotoRequest => {
return { file };
};
const isProperBlanking = (
maybeProperBlanking: BlankingFormModel
): maybeProperBlanking is BlankingFormModel & { file: File } => {
return typeof maybeProperBlanking['file'] !== 'undefined' && maybeProperBlanking['file'] instanceof File;
};
const castToBlankingRequest = ({ file }: BlankingFormModel & { file: File }): BlankingRequest => {
return { file };
};
export const filterPhotos = (array: PhotoFormModel[]): PhotoRequest[] =>
array.filter(isProperPhoto).map(castToPhotoRequest);
export const filterBlankings = (array: BlankingFormModel[]): BlankingRequest[] =>
array.filter(isProperBlanking).map(castToBlankingRequest);
+44
View File
@@ -0,0 +1,44 @@
export const handleFileSelect = (e: any, push: any): void => {
const { files } = e.target;
Array.from<File>(files).forEach((file: File): void => {
push({
file,
filename: file.name,
size: file.size,
url: URL.createObjectURL(file),
});
});
};
export const handleImageSelect = (e: any, push: any): void => {
const { files } = e.target;
Array.from<File>(files).forEach((file: File): void => {
push({
file,
url: URL.createObjectURL(file),
filename: file.name,
});
});
};
export const downloadItem = (file: any): void => {
const downloadUrl = file.url;
const link = document.createElement('a');
link.href = downloadUrl;
link.setAttribute('download', file.file ? file.file.name : 'file.pdf');
link.setAttribute('target', '_blank');
document.body.appendChild(link);
link.click();
link.remove();
};
export const downloadByURL = (url: string, filename: string): void => {
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', filename ? filename : 'file.pdf');
link.setAttribute('target', '_blank');
document.body.appendChild(link);
link.click();
link.remove();
};
+10
View File
@@ -0,0 +1,10 @@
import { AnyProductStore, SimpleProductResponse, ProductStore } from 'interfaces/API/products';
// const isSimpleProduct = (x: AnyProductStore): x is SimpleProductResponse => {
// return typeof (x as ProductStore).briefConfig === 'undefined';
// };
// const isFullProduct = (x: AnyProductStore): x is ProductStore => {
// return typeof (x as ProductStore).briefConfig !== 'undefined';
// };
// export { isSimpleProduct, isFullProduct };
+11
View File
@@ -0,0 +1,11 @@
import { SimpleProject, Project, AnyProject } from 'interfaces/API/projects';
const isSimpleProject = (x: AnyProject): x is SimpleProject => {
return typeof (x as Project).productsAmount === 'undefined';
};
const isFullProject = (x: AnyProject): x is Project => {
return typeof (x as Project).productsAmount !== 'undefined';
};
export { isSimpleProject, isFullProject };
+60
View File
@@ -0,0 +1,60 @@
interface FlattenOptions {
delimiter?: string;
safe?: boolean;
maxDepth?: number;
transformKey?: (x: string) => string;
}
interface Flatten {
<TTarget, TResult>(target: TTarget, options?: FlattenOptions): TResult;
}
function isBuffer(val: any): boolean {
// eslint-disable-next-line no-sparse-arrays
return ![, null].includes(val) && val.constructor === Buffer;
}
function keyIdentity(key: string): string {
return key;
}
const flatten: Flatten = (target, opts = {}) => {
const objDelimiter = opts.delimiter || '.';
const arrDelimiter = opts.delimiter || '-';
const transformKey = opts.transformKey || keyIdentity;
const output: any = {};
function step(object: any, prev?: any, currentDepth = 1): any {
Object.keys(object).forEach(function(key) {
const value = object[key];
const isarray = opts.safe && Array.isArray(value);
const type = Object.prototype.toString.call(value);
const isbuffer = isBuffer(value);
const isobject = type === '[object Object]' || type === '[object Array]';
const newKey = prev
? prev + (Array.isArray(object) ? arrDelimiter : objDelimiter) + transformKey(key)
: transformKey(key);
if (!isarray && !isbuffer && isobject && (!opts.maxDepth || currentDepth < opts.maxDepth)) {
return step(value, newKey, currentDepth + 1);
}
output[newKey] = value;
});
}
step(target);
return output;
};
const objectToFormDataConverter = (object: any) => {
const flattenedObject: any = flatten(object);
const formData = new FormData();
Object.keys(flattenedObject).forEach((item) => {
formData.append(item, flattenedObject[item]);
});
return formData;
};
export default objectToFormDataConverter;
+11
View File
@@ -0,0 +1,11 @@
import { PhotoBriefPhotoFormModel, PhotoBriefPhotoRequest } from 'interfaces/API/photoBrief';
export const isProperPhoto = (
maybeProperPhoto: PhotoBriefPhotoFormModel
): maybeProperPhoto is PhotoBriefPhotoFormModel & { file: File } => {
return typeof maybeProperPhoto['file'] !== 'undefined' && maybeProperPhoto['file'] instanceof File;
};
export const castToPhotoRequest = ({ file }: PhotoBriefPhotoFormModel & { file: File }): PhotoBriefPhotoRequest => {
return { file };
};
+4
View File
@@ -0,0 +1,4 @@
enum Routes {
NEW_PROJECT = '/projects/new',
}
export default Routes;
+3
View File
@@ -0,0 +1,3 @@
export type WithId<T> = T & { id: number };
export type WithoutId<T> = Omit<Text, 'id'>;
Loaded 100 of 454 files, more files were not shown because too many files have changed in this diff. Show more