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 (
+ <>
+
+
+ {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 (
+
+ );
+});
+
+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