From 6e2d8c25f96543d2f146ab02322646c54d62bf32 Mon Sep 17 00:00:00 2001 From: Pavel Gnedov Date: Fri, 16 Jun 2023 20:14:53 +0700 Subject: [PATCH] =?UTF-8?q?Kanban=20=D0=B4=D0=BE=D1=81=D0=BA=D0=B0=20?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B5=D0=BD=D0=B5=D1=81=D0=B5=D0=BD=D0=B0=20?= =?UTF-8?q?=D0=B2=20react=20=D0=B8=20mobx?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/package-lock.json | 84 +++++++++++ frontend/package.json | 2 + frontend/src/kanban-board/column.module.css | 11 ++ frontend/src/kanban-board/column.tsx | 27 ++++ .../src/kanban-board/kanban-board.module.css | 5 + frontend/src/kanban-board/kanban-board.tsx | 33 +++++ .../src/kanban-board/kanban-boards-page.tsx | 22 +++ frontend/src/kanban-board/kanban-boards.tsx | 29 ++++ .../src/kanban-board/kanban-card.module.css | 56 +++++++ frontend/src/kanban-board/kanban-card.tsx | 67 +++++++++ frontend/src/kanban-board/store.ts | 101 +++++++++++++ frontend/src/redmine-types.ts | 140 ++++++++++++++++++ frontend/src/router.tsx | 5 + frontend/src/start-page/cover.module.css | 8 +- frontend/src/utils/style.ts | 24 +++ 15 files changed, 610 insertions(+), 4 deletions(-) create mode 100644 frontend/src/kanban-board/column.module.css create mode 100644 frontend/src/kanban-board/column.tsx create mode 100644 frontend/src/kanban-board/kanban-board.module.css create mode 100644 frontend/src/kanban-board/kanban-board.tsx create mode 100644 frontend/src/kanban-board/kanban-boards-page.tsx create mode 100644 frontend/src/kanban-board/kanban-boards.tsx create mode 100644 frontend/src/kanban-board/kanban-card.module.css create mode 100644 frontend/src/kanban-board/kanban-card.tsx create mode 100644 frontend/src/kanban-board/store.ts create mode 100644 frontend/src/redmine-types.ts create mode 100644 frontend/src/utils/style.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 435a829..e3655d0 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,7 +15,9 @@ "@types/node": "^16.18.14", "@types/react": "^18.0.28", "@types/react-dom": "^18.0.11", + "axios": "^1.4.0", "mobx": "^6.9.0", + "mobx-react-lite": "^3.4.3", "mobx-state-tree": "^5.1.8", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -4826,6 +4828,29 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", + "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/axobject-query": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz", @@ -11869,6 +11894,27 @@ "url": "https://opencollective.com/mobx" } }, + "node_modules/mobx-react-lite": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/mobx-react-lite/-/mobx-react-lite-3.4.3.tgz", + "integrity": "sha512-NkJREyFTSUXR772Qaai51BnE1voWx56LOL80xG7qkZr6vo8vEaLF3sz1JNUVh+rxmUzxYaqOhfuxTfqUh0FXUg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mobx" + }, + "peerDependencies": { + "mobx": "^6.1.0", + "react": "^16.8.0 || ^17 || ^18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/mobx-state-tree": { "version": "5.1.8", "resolved": "https://registry.npmjs.org/mobx-state-tree/-/mobx-state-tree-5.1.8.tgz", @@ -13820,6 +13866,11 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -20369,6 +20420,28 @@ "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.6.3.tgz", "integrity": "sha512-/BQzOX780JhsxDnPpH4ZiyrJAzcd8AfzFPkv+89veFSr1rcMjuq2JDCwypKaPeB6ljHp9KjXhPpjgCvQlWYuqg==" }, + "axios": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", + "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", + "requires": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + }, + "dependencies": { + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + } + } + }, "axobject-query": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz", @@ -25482,6 +25555,12 @@ "resolved": "https://registry.npmjs.org/mobx/-/mobx-6.9.0.tgz", "integrity": "sha512-HdKewQEREEJgsWnErClfbFoVebze6rGazxFLU/XUyrII8dORfVszN1V0BMRnQSzcgsNNtkX8DHj3nC6cdWE9YQ==" }, + "mobx-react-lite": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/mobx-react-lite/-/mobx-react-lite-3.4.3.tgz", + "integrity": "sha512-NkJREyFTSUXR772Qaai51BnE1voWx56LOL80xG7qkZr6vo8vEaLF3sz1JNUVh+rxmUzxYaqOhfuxTfqUh0FXUg==", + "requires": {} + }, "mobx-state-tree": { "version": "5.1.8", "resolved": "https://registry.npmjs.org/mobx-state-tree/-/mobx-state-tree-5.1.8.tgz", @@ -26702,6 +26781,11 @@ } } }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 7b164bd..908c135 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,7 +10,9 @@ "@types/node": "^16.18.14", "@types/react": "^18.0.28", "@types/react-dom": "^18.0.11", + "axios": "^1.4.0", "mobx": "^6.9.0", + "mobx-react-lite": "^3.4.3", "mobx-state-tree": "^5.1.8", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/frontend/src/kanban-board/column.module.css b/frontend/src/kanban-board/column.module.css new file mode 100644 index 0000000..25ae530 --- /dev/null +++ b/frontend/src/kanban-board/column.module.css @@ -0,0 +1,11 @@ +.kanbanColumn { + width: 200px; + background-color: rgb(225, 225, 225); + display: flex; + flex-direction: column; +} + +.kanbanHeader { + text-align: center; + width: 200px; +} \ No newline at end of file diff --git a/frontend/src/kanban-board/column.tsx b/frontend/src/kanban-board/column.tsx new file mode 100644 index 0000000..0694ca8 --- /dev/null +++ b/frontend/src/kanban-board/column.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import ColumnCss from './column.module.css'; +import * as Stores from './store'; +import { observer } from 'mobx-react-lite'; +import * as KanbanCard from './kanban-card'; + +export type Props = { + store: Stores.IColumnStore +} + +export const Column = observer((props: Props) => { + const cards = props.store.cards.map((card) => { + return ( + + ); + }); + return ( +
+
+ {props.store.status} ({props.store.count}) +
+ {cards} +
+ ); +}); + +export default Column; diff --git a/frontend/src/kanban-board/kanban-board.module.css b/frontend/src/kanban-board/kanban-board.module.css new file mode 100644 index 0000000..967ffae --- /dev/null +++ b/frontend/src/kanban-board/kanban-board.module.css @@ -0,0 +1,5 @@ +.kanbanContainer { + display: flex; + flex-direction: row; + width: 100%; +} \ No newline at end of file diff --git a/frontend/src/kanban-board/kanban-board.tsx b/frontend/src/kanban-board/kanban-board.tsx new file mode 100644 index 0000000..970438b --- /dev/null +++ b/frontend/src/kanban-board/kanban-board.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import KanbanBoardCss from './kanban-board.module.css'; +import { IBoardStore } from './store'; +import { observer } from 'mobx-react-lite'; +import Column from './column'; + +export type Props = { + store: IBoardStore +}; + +export const KanbanBoard = observer((props: Props) => { + let title: any; + if (props.store.metainfo.url) { + title = {props.store.metainfo.title}; + } else { + title = <>{props.store.metainfo.title}; + } + const columns = []; + for (let i = 0; i < props.store.data.length; i++) { + const column = props.store.data[i]; + columns.push() + } + return ( + <> +

{title}

+
+ {columns} +
+ + ); +}); + +export default KanbanBoard; \ No newline at end of file diff --git a/frontend/src/kanban-board/kanban-boards-page.tsx b/frontend/src/kanban-board/kanban-boards-page.tsx new file mode 100644 index 0000000..f9ba580 --- /dev/null +++ b/frontend/src/kanban-board/kanban-boards-page.tsx @@ -0,0 +1,22 @@ +import React, { useEffect } from 'react'; +import { useParams } from 'react-router-dom'; +import * as Stores from './store'; +import * as KBS from './kanban-boards'; + +export const KanbanBoardsPage = (): JSX.Element => { + const params = useParams(); + const name = params.name as string; + const type = params.type as string; + + // DEBUG: begin + console.debug(`KanbanBoardsPage: type=${type}; name=${name}`); + useEffect(() => { + console.debug(`KanbanBoardsPage: type=${type}; name=${name}`); + }); + // DEBUG: end + + const store = Stores.PageStore.create({loaded: false, type: type, name: name, data: null}); + Stores.PageStoreLoadData(store); + + return ; +} \ No newline at end of file diff --git a/frontend/src/kanban-board/kanban-boards.tsx b/frontend/src/kanban-board/kanban-boards.tsx new file mode 100644 index 0000000..19661ad --- /dev/null +++ b/frontend/src/kanban-board/kanban-boards.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import * as KB from './kanban-board'; +import { IPageStore } from './store'; +import { observer } from 'mobx-react-lite'; + +export type Props = { + store: IPageStore +} + +export const KanbanBoards = observer((props: Props) => { + const data = props.store.data; + if (!props.store.loaded || !data) { + return
Loading...
+ } + const list: any[] = []; + for (let i = 0; i < data.length; i++) { + const boardData = data[i]; + const key = boardData.metainfo.title; + const board = ; + list.push(board); + } + return ( + <> + {list} + + ); +}); + +export default KanbanBoards; \ No newline at end of file diff --git a/frontend/src/kanban-board/kanban-card.module.css b/frontend/src/kanban-board/kanban-card.module.css new file mode 100644 index 0000000..17fde41 --- /dev/null +++ b/frontend/src/kanban-board/kanban-card.module.css @@ -0,0 +1,56 @@ +.kanbanCard { + background-color: rgb(196, 196, 196); + border-width: 1px; + border-color: rgba(255, 255, 255, 0.2) rgba(96, 96, 96, 0.2) rgba(96, 96, 96, 0.2) rgba(255, 255, 255, 0.2); + /*border-color: black;*/ + border-style: solid; + margin: 2px; + padding: 3px; + width: 190px; + /*display: flex;*/ + box-shadow: 0 0 3px rgba(0, 0, 0, 0.5); + border-radius: 3px; + z-index: 100; +} + +.kanbanCard div { + font-size: small; +} + +.kanbanCard .kanbanCardTitle { + font-weight: bold; +} + +.timepassedDot { + height: 10px; + width: 10px; + background-color: #bbb; + border-radius: 50%; + display: inline-block; +} + +.timepassedDot.hot { + background-color: red; +} + +.timepassedDot.warm { + background-color: orange; +} + +.timepassedDot.comfort { + background-color: rgba(255, 255, 0, 0.4); +} + +.timepassedDot.breezy { + background-color: rgba(0, 255, 0, 0.4); +} + +.timepassedDot.cold { + background-color: rgba(0, 0, 255, 0.1); +} + +.kanbanCardTag { + font-size: 8pt; + border-radius: 4px; + padding: 2px; +} \ No newline at end of file diff --git a/frontend/src/kanban-board/kanban-card.tsx b/frontend/src/kanban-board/kanban-card.tsx new file mode 100644 index 0000000..4402739 --- /dev/null +++ b/frontend/src/kanban-board/kanban-card.tsx @@ -0,0 +1,67 @@ +import { observer } from 'mobx-react-lite'; +import KanbanCardCss from './kanban-card.module.css'; +import React from 'react'; +import { ICardStore } from './store'; +import { getStyleObjectFromString } from '../utils/style'; + +export type Props = { + store: ICardStore +}; + +export type TagProps = { + style?: string; + tag: string; +}; + +export const KanbanCardTag = (props: TagProps): JSX.Element => { + const inlineStyle = getStyleObjectFromString(props.style || ''); + return ( + + {props.tag} + + ); +} + +/** + * Какие дальше требования к карточкам? + * + * 1. Отобразить как было в статичной доске + * 2. Переделать отображение с учётом store.params + */ + +export const KanbanCard = observer((props: Props) => { + let timePassedColorClassName = ''; + const timePassedIssueValue: string = props.store.issue.timePassedClass; + if (timePassedIssueValue && KanbanCardCss[timePassedIssueValue]) { + timePassedColorClassName = KanbanCardCss[timePassedIssueValue]; + } + const timePassedClassName = `${KanbanCardCss.timepassedDot} ${timePassedColorClassName}`; + let tagsSection = <>; + const tagsParams = props.store.params?.fields.find((field) => { + return field.component === 'tags'; + }); + if (tagsParams && props.store.issue[tagsParams.path]) { + const tags = props.store.issue[tagsParams.path] as TagProps[]; + tagsSection = ( +
+ {tagsParams.label || 'Tags'}: {tags.map(tag => )} +
+ ); + } + return ( +
+
+ + {props.store.issue.tracker.name} #{props.store.issue.id} - {props.store.issue.subject} +
Исп.: {props.store.issue.current_user.name}
+
Прио.: {props.store.issue.priority.name}
+
Версия: {props.store.issue.fixed_version?.name || ''}
+
Прогресс: {props.store.issue.done_ratio}
+
Трудозатраты: {props.store.issue.total_spent_hours} / {props.store.issue.total_estimated_hours}
+ {tagsSection} +
+
+ ); +}); + +export default KanbanCard; \ No newline at end of file diff --git a/frontend/src/kanban-board/store.ts b/frontend/src/kanban-board/store.ts new file mode 100644 index 0000000..3cc5977 --- /dev/null +++ b/frontend/src/kanban-board/store.ts @@ -0,0 +1,101 @@ +import { Instance, types } from 'mobx-state-tree'; +import { RedmineTypes } from '../redmine-types'; +import axios from 'axios'; + +export const IssueStore = types.frozen() + +export interface IIssueStore extends Instance {} + +export const ColumnStore = types.model({ + status: '', + count: 0, + issues: types.array(IssueStore) +}).views((self) => { + return { + get cards(): ICardStore[] { + return self.issues.map(issue => { + return { + issue: issue + } as ICardStore; + }); + } + } +}); + +export interface IColumnStore extends Instance {} + +export const MetaInfoStore = types.model({ + title: '', + url: types.maybe(types.string), + rootIssue: types.model({ + id: 0, + tracker: types.model({ + id: 0, + name: '' + }), + subject: '' + }) +}); + +export interface IMetaInfoStore extends Instance {} + +export const BoardStore = types.model({ + data: types.array(ColumnStore), + metainfo: MetaInfoStore +}); + +export interface IBoardStore extends Instance {} + +export const PageStore = types.model({ + loaded: false, + type: '', + name: '', + data: types.maybeNull( + types.array(BoardStore) + ) +}).actions(self => { + return { + setData: (data: any) => { + self.data = data; + self.loaded = true; + } + }; +}); + +export async function PageStoreLoadData(store: IPageStore): Promise { + const url = `/simple-kanban-board/${store.type}/${store.name}/raw`; + const resp = await axios.get(url); + if (!(resp?.data)) return; + store.setData(resp.data); +} + +export interface IPageStore extends Instance { } + +export type CardField = { + component: string; +} & Record; + +export const CardSettings = types.model({}); + +export const CardStore = types.model({ + issue: IssueStore, + params: types.maybe(types.model({ + fields: types.optional( + types.array( + types.frozen() + ), + [ + { component: 'text', label: 'Исп.', path: 'current_user.name' }, + { component: 'text', label: 'Прио.', path: 'priority.name' }, + { component: 'text', label: 'Версия', path: 'fixed_version.name' }, + { component: 'text', label: 'Прогресс', path: 'done_ratio' }, + { component: 'labor_costs' }, + { component: 'tags', label: 'Tags', path: 'styledTags' } + ] + ), + autoCollapse: types.boolean + })), +}); + +export interface ICardStore extends Instance {} + diff --git a/frontend/src/redmine-types.ts b/frontend/src/redmine-types.ts new file mode 100644 index 0000000..00471c3 --- /dev/null +++ b/frontend/src/redmine-types.ts @@ -0,0 +1,140 @@ +// eslint-disable-next-line @typescript-eslint/prefer-namespace-keyword, @typescript-eslint/no-namespace +export module RedmineTypes { + export type IdAndName = { + id: number; + name: string; + }; + + export type CustomField = { + id: number; + name: string; + value: string; + }; + + export type JournalDetail = { + property: string; + name: string; + old_value?: string; + new_value?: string; + }; + + export type Journal = { + id: number; + user: IdAndName; + notes?: string; + created_on: string; + details?: JournalDetail[]; + }; + + export type ChildIssue = { + id: number; + tracker: IdAndName; + subject: string; + children?: Children; + }; + + export type Children = ChildIssue[]; + + export type Issue = { + id: number; + project: IdAndName; + tracker: IdAndName; + status: IdAndName; + priority: IdAndName; + author: IdAndName; + assigned_to?: IdAndName; + category: IdAndName; + fixed_version?: IdAndName; + subject: string; + description: string; + start_date: string; + done_ratio: number; + spent_hours: number; + total_spent_hours: number; + custom_fields: CustomField[]; + created_on: string; + updated_on?: string; + closed_on?: string; + relations?: Record[]; + journals?: Journal[]; + children?: Children; + parent?: { id: number }; + }; + + export type ExtendedIssue = Issue & Record; + + // eslint-disable-next-line @typescript-eslint/prefer-namespace-keyword, @typescript-eslint/no-namespace + export module Unknown { + export const num = -1; + export const str = ''; + export const idAndName: IdAndName = { + id: -1, + name: str, + }; + export const unknownName = 'Unknown'; + export const subject = 'Unknown'; + export const date = '1970-01-01T00:00:00Z'; + export const issue: Issue = { + id: num, + project: idAndName, + tracker: idAndName, + status: idAndName, + priority: idAndName, + author: idAndName, + category: idAndName, + fixed_version: idAndName, + subject: subject, + description: str, + start_date: date, + done_ratio: num, + spent_hours: num, + total_spent_hours: num, + custom_fields: [], + created_on: date, + }; + + export const user: User = { + id: num, + login: str, + firstname: unknownName, + lastname: unknownName, + mail: str, + }; + } + + export type User = { + id: number; + login: string; + firstname: string; + lastname: string; + mail: string; + }; + + export type PublicUser = { + id: number; + firstname: string; + lastname: string; + login: string; + name: string; + }; + + export function CreatePublicUserFromUser(obj: User): PublicUser { + return { + id: obj.id, + login: obj.login, + firstname: obj.firstname, + lastname: obj.lastname, + name: `${obj.firstname} ${obj.lastname}`, + }; + } + + export function CreateUser(obj: User): User { + return { + id: obj.id, + login: obj.login, + firstname: obj.firstname, + lastname: obj.lastname, + mail: obj.mail, + }; + } +} diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index 396cf9d..fb6336f 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -2,12 +2,17 @@ import React from "react"; import { createBrowserRouter } from "react-router-dom"; import StartPage from "./start-page/start-page"; import UnknownPage from "./unknown-page"; +import { KanbanBoardsPage } from "./kanban-board/kanban-boards-page"; export const router = createBrowserRouter([ { path: "/", element: (), }, + { + path: "/kanban-board/:type/:name", + element: () + }, { path: "*", element: () diff --git a/frontend/src/start-page/cover.module.css b/frontend/src/start-page/cover.module.css index 6f56673..b4acfee 100644 --- a/frontend/src/start-page/cover.module.css +++ b/frontend/src/start-page/cover.module.css @@ -11,7 +11,7 @@ border: 0; } -h1 { +.cover h1 { left: 0%; right: 0%; top: 18.22%; @@ -23,7 +23,7 @@ h1 { text-transform: uppercase; } -h3 { +.cover h3 { color: #F5B14E; font-family: 'Source Code Pro'; letter-spacing: 0.12em; @@ -33,7 +33,7 @@ h3 { line-height: 34px; } -h4 { +.cover h4 { font-style: normal; font-weight: 600; font-size: 40px; @@ -43,7 +43,7 @@ h4 { font-feature-settings: 'pnum' on, 'lnum' on; } -h4:hover { +.cover h4:hover { text-decoration: underline; color: #F5B14E; } diff --git a/frontend/src/utils/style.ts b/frontend/src/utils/style.ts new file mode 100644 index 0000000..0b675d2 --- /dev/null +++ b/frontend/src/utils/style.ts @@ -0,0 +1,24 @@ +const formatStringToCamelCase = (str: string): string => { + const splitted = str.split("-"); + if (splitted.length === 1) return splitted[0]; + return ( + splitted[0] + + splitted + .slice(1) + .map(word => word[0].toUpperCase() + word.slice(1)) + .join("") + ); +}; + +export const getStyleObjectFromString = (str: string): Record => { + const style = {} as Record; + str.split(";").forEach(el => { + const [property, value] = el.split(":"); + if (!property) return; + + const formattedProperty = formatStringToCamelCase(property.trim()); + style[formattedProperty] = value.trim(); + }); + + return style; +}; \ No newline at end of file