pulled react

This commit is contained in:
Ruidy Nemausat 2020-04-18 15:00:47 +02:00
commit f5edc62f00
75 changed files with 2721 additions and 943 deletions

View file

@ -13,7 +13,7 @@ using TicketManager.Resources;
namespace TicketManager.Controllers namespace TicketManager.Controllers
{ {
[Authorize] // [Authorize]
[Produces("application/json")] [Produces("application/json")]
[Route("api/v1/users")] [Route("api/v1/users")]
[ApiController] [ApiController]

View file

@ -13,7 +13,7 @@ using System;
namespace TicketManager.Controllers namespace TicketManager.Controllers
{ {
// [Authorize(Roles = "Admin")] // [Authorize(Roles = "Admin")]
[Authorize] // [Authorize]
[Produces("application/json")] [Produces("application/json")]
[Route("api/v1/[controller]")] [Route("api/v1/[controller]")]
[ApiController] [ApiController]

View file

@ -10,7 +10,7 @@ using TicketManager.Models;
namespace TicketManager.Controllers namespace TicketManager.Controllers
{ {
[Authorize] // [Authorize]
[Route("api/v1/[controller]")] [Route("api/v1/[controller]")]
[ApiController] [ApiController]
public class TicketsController : ControllerBase public class TicketsController : ControllerBase

View file

@ -53,3 +53,10 @@
- [x] Form validators - [x] Form validators
- [x] API deployed to Azure - [x] API deployed to Azure
- [x] Front-end deployment: build first then use ssh … - [x] Front-end deployment: build first then use ssh …
- [x] Azure
- [ ] Refactor TabPanels code
- [ ] Refactor Lists code
- [ ] Query project members in UserPage
- [ ] Query progression info in UserPage
- [x] Add info fields in New Ticket Form
- [ ] Filter users in Users Modal

View file

@ -63,7 +63,7 @@ namespace TicketManager
{ {
Name = "Ruidy Nemausat", Name = "Ruidy Nemausat",
Email = "ruidy.nemausat@gmail.com", Email = "ruidy.nemausat@gmail.com",
Url = new Uri("https://ruidywebsite.herokuapp.com/"), Url = new Uri("https://ruidyportfolio.herokuapp.com/"),
} }
}); });
// Set the comments path for the Swagger JSON and UI. // Set the comments path for the Swagger JSON and UI.

View file

@ -27,6 +27,8 @@
</CustomCommands> </CustomCommands>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
</ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.0.2" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.0.2" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.0.2" />
@ -39,7 +41,6 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.0.1" /> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.0.1" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="3.0.1" /> <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="3.0.1" />
<!-- <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.0.1" /> -->
<PackageReference Include="Microsoft.OpenApi" Version="1.1.4" /> <PackageReference Include="Microsoft.OpenApi" Version="1.1.4" />
<PackageReference Include="Moq" Version="4.13.0" /> <PackageReference Include="Moq" Version="4.13.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0" />

BIN
app.db Normal file

Binary file not shown.

356
client/package-lock.json generated
View file

@ -1057,6 +1057,11 @@
"resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-10.1.0.tgz", "resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-10.1.0.tgz",
"integrity": "sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg==" "integrity": "sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg=="
}, },
"@emotion/hash": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz",
"integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow=="
},
"@hapi/address": { "@hapi/address": {
"version": "2.1.4", "version": "2.1.4",
"resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz", "resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz",
@ -1282,6 +1287,93 @@
"@types/yargs": "^13.0.0" "@types/yargs": "^13.0.0"
} }
}, },
"@material-ui/core": {
"version": "4.9.8",
"resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.9.8.tgz",
"integrity": "sha512-4cslpG6oLoPWUfwPkX+hvbak4hAGiOfgXOu/UIYeeMrtsTEebC0Mirjoby7zhS4ny86YI3rXEFW6EZDmlj5n5w==",
"requires": {
"@babel/runtime": "^7.4.4",
"@material-ui/styles": "^4.9.6",
"@material-ui/system": "^4.9.6",
"@material-ui/types": "^5.0.0",
"@material-ui/utils": "^4.9.6",
"@types/react-transition-group": "^4.2.0",
"clsx": "^1.0.2",
"hoist-non-react-statics": "^3.3.2",
"popper.js": "^1.14.1",
"prop-types": "^15.7.2",
"react-is": "^16.8.0",
"react-transition-group": "^4.3.0"
}
},
"@material-ui/icons": {
"version": "4.9.1",
"resolved": "https://registry.npmjs.org/@material-ui/icons/-/icons-4.9.1.tgz",
"integrity": "sha512-GBitL3oBWO0hzBhvA9KxqcowRUsA0qzwKkURyC8nppnC3fw54KPKZ+d4V1Eeg/UnDRSzDaI9nGCdel/eh9AQMg==",
"requires": {
"@babel/runtime": "^7.4.4"
}
},
"@material-ui/lab": {
"version": "4.0.0-alpha.47",
"resolved": "https://registry.npmjs.org/@material-ui/lab/-/lab-4.0.0-alpha.47.tgz",
"integrity": "sha512-+WC3O0M/769D3nO9Rqupusc+lob7tQMe5/DnOjAhZ0bpXlJbhZb7N84WkEk4JgQLj6ydP8e9Jhqd1lG+mGj+xw==",
"requires": {
"@babel/runtime": "^7.4.4",
"@material-ui/utils": "^4.9.6",
"clsx": "^1.0.4",
"prop-types": "^15.7.2",
"react-is": "^16.8.0"
}
},
"@material-ui/styles": {
"version": "4.9.6",
"resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.9.6.tgz",
"integrity": "sha512-ijgwStEkw1OZ6gCz18hkjycpr/3lKs1hYPi88O/AUn4vMuuGEGAIrqKVFq/lADmZUNF3DOFIk8LDkp7zmjPxtA==",
"requires": {
"@babel/runtime": "^7.4.4",
"@emotion/hash": "^0.8.0",
"@material-ui/types": "^5.0.0",
"@material-ui/utils": "^4.9.6",
"clsx": "^1.0.2",
"csstype": "^2.5.2",
"hoist-non-react-statics": "^3.3.2",
"jss": "^10.0.3",
"jss-plugin-camel-case": "^10.0.3",
"jss-plugin-default-unit": "^10.0.3",
"jss-plugin-global": "^10.0.3",
"jss-plugin-nested": "^10.0.3",
"jss-plugin-props-sort": "^10.0.3",
"jss-plugin-rule-value-function": "^10.0.3",
"jss-plugin-vendor-prefixer": "^10.0.3",
"prop-types": "^15.7.2"
}
},
"@material-ui/system": {
"version": "4.9.6",
"resolved": "https://registry.npmjs.org/@material-ui/system/-/system-4.9.6.tgz",
"integrity": "sha512-QtfoAePyqXoZ2HUVSwGb1Ro0kucMCvVjbI0CdYIR21t0Opgfm1Oer6ni9P5lfeXA39xSt0wCierw37j+YES48Q==",
"requires": {
"@babel/runtime": "^7.4.4",
"@material-ui/utils": "^4.9.6",
"prop-types": "^15.7.2"
}
},
"@material-ui/types": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@material-ui/types/-/types-5.0.0.tgz",
"integrity": "sha512-UeH2BuKkwDndtMSS0qgx1kCzSMw+ydtj0xx/XbFtxNSTlXydKwzs5gVW5ZKsFlAkwoOOQ9TIsyoCC8hq18tOwg=="
},
"@material-ui/utils": {
"version": "4.9.6",
"resolved": "https://registry.npmjs.org/@material-ui/utils/-/utils-4.9.6.tgz",
"integrity": "sha512-gqlBn0JPPTUZeAktn1rgMcy9Iczrr74ecx31tyZLVGdBGGzsxzM6PP6zeS7FuoLS6vG4hoZP7hWnOoHtkR0Kvw==",
"requires": {
"@babel/runtime": "^7.4.4",
"prop-types": "^15.7.2",
"react-is": "^16.8.0"
}
},
"@mrmlnc/readdir-enhanced": { "@mrmlnc/readdir-enhanced": {
"version": "2.2.1", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz",
@ -1622,6 +1714,22 @@
"@types/react-router": "*" "@types/react-router": "*"
} }
}, },
"@types/react-swipeable-views": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@types/react-swipeable-views/-/react-swipeable-views-0.13.0.tgz",
"integrity": "sha512-orrreCcXev6IUXDuHf07RDDCAoIZRMSr95eyWmYNRfjic7w/O+68iPu0NCysVls+UygRNvoqZMuXI72N/58E1w==",
"requires": {
"@types/react": "*"
}
},
"@types/react-transition-group": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.2.4.tgz",
"integrity": "sha512-8DMUaDqh0S70TjkqU0DxOu80tFUiiaS9rxkWip/nb7gtvAsbqOXm02UCmR8zdcjWujgeYPiPNTVpVpKzUDotwA==",
"requires": {
"@types/react": "*"
}
},
"@types/stack-utils": { "@types/stack-utils": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz",
@ -3168,6 +3276,11 @@
"shallow-clone": "^0.1.2" "shallow-clone": "^0.1.2"
} }
}, },
"clsx": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.0.tgz",
"integrity": "sha512-3avwM37fSK5oP6M5rQ9CNe99lwxhXDOeSWVPAOYF6OazUTgZCMb0yWlJpmdD74REy1gkEaFiub2ULv4fq9GUhA=="
},
"co": { "co": {
"version": "4.6.0", "version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@ -3665,6 +3778,15 @@
"resolved": "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.1.tgz", "resolved": "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.1.tgz",
"integrity": "sha1-2bkoGtz9jO2TW9urqDeGiX9k6ZY=" "integrity": "sha1-2bkoGtz9jO2TW9urqDeGiX9k6ZY="
}, },
"css-vendor": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/css-vendor/-/css-vendor-2.0.7.tgz",
"integrity": "sha512-VS9Rjt79+p7M0WkPqcAza4Yq1ZHrsHrwf7hPL/bjQB+c1lwmAI+1FXxYTYt818D/50fFVflw0XKleiBN5RITkg==",
"requires": {
"@babel/runtime": "^7.6.2",
"is-in-browser": "^1.0.2"
}
},
"css-what": { "css-what": {
"version": "3.2.1", "version": "3.2.1",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-3.2.1.tgz", "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.2.1.tgz",
@ -4091,6 +4213,30 @@
"utila": "~0.4" "utila": "~0.4"
} }
}, },
"dom-helpers": {
"version": "5.1.4",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.1.4.tgz",
"integrity": "sha512-TjMyeVUvNEnOnhzs6uAn9Ya47GmMo3qq7m+Lr/3ON0Rs5kHvb8I+SQYjLUSYn7qhEm0QjW0yrBkvz9yOrwwz1A==",
"requires": {
"@babel/runtime": "^7.8.7",
"csstype": "^2.6.7"
},
"dependencies": {
"@babel/runtime": {
"version": "7.9.2",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.9.2.tgz",
"integrity": "sha512-NE2DtOdufG7R5vnfQUTehdTfNycfUANEtCa9PssN9O/xmTzP4E08UI797ixaei6hBEVL9BI/PsdJS5x7mWoB9Q==",
"requires": {
"regenerator-runtime": "^0.13.4"
}
},
"regenerator-runtime": {
"version": "0.13.5",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz",
"integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA=="
}
}
},
"dom-serializer": { "dom-serializer": {
"version": "0.2.2", "version": "0.2.2",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz",
@ -6072,6 +6218,11 @@
"resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz",
"integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=" "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM="
}, },
"hyphenate-style-name": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.3.tgz",
"integrity": "sha512-EcuixamT82oplpoJ2XU4pDtKGWQ7b00CD9f1ug9IaQ3p1bkHMiKCZ9ut9QDI6qsa6cpUuB+A/I+zLtdNK4n2DQ=="
},
"iconv-lite": { "iconv-lite": {
"version": "0.4.24", "version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@ -6371,6 +6522,11 @@
"is-extglob": "^2.1.1" "is-extglob": "^2.1.1"
} }
}, },
"is-in-browser": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz",
"integrity": "sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU="
},
"is-number": { "is-number": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
@ -7780,6 +7936,83 @@
"verror": "1.10.0" "verror": "1.10.0"
} }
}, },
"jss": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/jss/-/jss-10.1.1.tgz",
"integrity": "sha512-Xz3qgRUFlxbWk1czCZibUJqhVPObrZHxY3FPsjCXhDld4NOj1BgM14Ir5hVm+Qr6OLqVljjGvoMcCdXNOAbdkQ==",
"requires": {
"@babel/runtime": "^7.3.1",
"csstype": "^2.6.5",
"is-in-browser": "^1.1.3",
"tiny-warning": "^1.0.2"
}
},
"jss-plugin-camel-case": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/jss-plugin-camel-case/-/jss-plugin-camel-case-10.1.1.tgz",
"integrity": "sha512-MDIaw8FeD5uFz1seQBKz4pnvDLnj5vIKV5hXSVdMaAVq13xR6SVTVWkIV/keyTs5txxTvzGJ9hXoxgd1WTUlBw==",
"requires": {
"@babel/runtime": "^7.3.1",
"hyphenate-style-name": "^1.0.3",
"jss": "10.1.1"
}
},
"jss-plugin-default-unit": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/jss-plugin-default-unit/-/jss-plugin-default-unit-10.1.1.tgz",
"integrity": "sha512-UkeVCA/b3QEA4k0nIKS4uWXDCNmV73WLHdh2oDGZZc3GsQtlOCuiH3EkB/qI60v2MiCq356/SYWsDXt21yjwdg==",
"requires": {
"@babel/runtime": "^7.3.1",
"jss": "10.1.1"
}
},
"jss-plugin-global": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/jss-plugin-global/-/jss-plugin-global-10.1.1.tgz",
"integrity": "sha512-VBG3wRyi3Z8S4kMhm8rZV6caYBegsk+QnQZSVmrWw6GVOT/Z4FA7eyMu5SdkorDlG/HVpHh91oFN56O4R9m2VA==",
"requires": {
"@babel/runtime": "^7.3.1",
"jss": "10.1.1"
}
},
"jss-plugin-nested": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/jss-plugin-nested/-/jss-plugin-nested-10.1.1.tgz",
"integrity": "sha512-ozEu7ZBSVrMYxSDplPX3H82XHNQk2DQEJ9TEyo7OVTPJ1hEieqjDFiOQOxXEj9z3PMqkylnUbvWIZRDKCFYw5Q==",
"requires": {
"@babel/runtime": "^7.3.1",
"jss": "10.1.1",
"tiny-warning": "^1.0.2"
}
},
"jss-plugin-props-sort": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/jss-plugin-props-sort/-/jss-plugin-props-sort-10.1.1.tgz",
"integrity": "sha512-g/joK3eTDZB4pkqpZB38257yD4LXB0X15jxtZAGbUzcKAVUHPl9Jb47Y7lYmiGsShiV4YmQRqG1p2DHMYoK91g==",
"requires": {
"@babel/runtime": "^7.3.1",
"jss": "10.1.1"
}
},
"jss-plugin-rule-value-function": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.1.1.tgz",
"integrity": "sha512-ClV1lvJ3laU9la1CUzaDugEcwnpjPTuJ0yGy2YtcU+gG/w9HMInD5vEv7xKAz53Bk4WiJm5uLOElSEshHyhKNw==",
"requires": {
"@babel/runtime": "^7.3.1",
"jss": "10.1.1"
}
},
"jss-plugin-vendor-prefixer": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.1.1.tgz",
"integrity": "sha512-09MZpQ6onQrhaVSF6GHC4iYifQ7+4YC/tAP6D4ZWeZotvCMq1mHLqNKRIaqQ2lkgANjlEot2JnVi1ktu4+L4pw==",
"requires": {
"@babel/runtime": "^7.3.1",
"css-vendor": "^2.0.7",
"jss": "10.1.1"
}
},
"jsx-ast-utils": { "jsx-ast-utils": {
"version": "2.2.3", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.2.3.tgz", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.2.3.tgz",
@ -7789,6 +8022,11 @@
"object.assign": "^4.1.0" "object.assign": "^4.1.0"
} }
}, },
"keycode": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/keycode/-/keycode-2.2.0.tgz",
"integrity": "sha1-PQr1bce4uOXLqNCpfxByBO7CKwQ="
},
"killable": { "killable": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
@ -9204,6 +9442,11 @@
"ts-pnp": "^1.1.2" "ts-pnp": "^1.1.2"
} }
}, },
"popper.js": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz",
"integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ=="
},
"portfinder": { "portfinder": {
"version": "1.0.25", "version": "1.0.25",
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.25.tgz", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.25.tgz",
@ -10620,6 +10863,16 @@
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.5.tgz", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.5.tgz",
"integrity": "sha512-+DMR2k5c6BqMDSMF8hLH0vYKtKTeikiFW+fj0LClN+XZg4N9b8QUAdHC62CGWNLTi/gnuuemNcNcTFrCvK1f+A==" "integrity": "sha512-+DMR2k5c6BqMDSMF8hLH0vYKtKTeikiFW+fj0LClN+XZg4N9b8QUAdHC62CGWNLTi/gnuuemNcNcTFrCvK1f+A=="
}, },
"react-event-listener": {
"version": "0.6.6",
"resolved": "https://registry.npmjs.org/react-event-listener/-/react-event-listener-0.6.6.tgz",
"integrity": "sha512-+hCNqfy7o9wvO6UgjqFmBzARJS7qrNoda0VqzvOuioEpoEXKutiKuv92dSz6kP7rYLmyHPyYNLesi5t/aH1gfw==",
"requires": {
"@babel/runtime": "^7.2.0",
"prop-types": "^15.6.0",
"warning": "^4.0.1"
}
},
"react-is": { "react-is": {
"version": "16.12.0", "version": "16.12.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz",
@ -10731,6 +10984,96 @@
"workbox-webpack-plugin": "4.3.1" "workbox-webpack-plugin": "4.3.1"
} }
}, },
"react-swipeable-views": {
"version": "0.13.9",
"resolved": "https://registry.npmjs.org/react-swipeable-views/-/react-swipeable-views-0.13.9.tgz",
"integrity": "sha512-WXC2FKYvZ9QdJ31v9LjEJEl1bA7E4AcaloTkbW0uU0dYf5uvv4aOpiyxubvOkVl1a5L2UAHmKSif4TmJ9usrSg==",
"requires": {
"@babel/runtime": "7.0.0",
"prop-types": "^15.5.4",
"react-swipeable-views-core": "^0.13.7",
"react-swipeable-views-utils": "^0.13.9",
"warning": "^4.0.1"
},
"dependencies": {
"@babel/runtime": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.0.0.tgz",
"integrity": "sha512-7hGhzlcmg01CvH1EHdSPVXYX1aJ8KCEyz6I9xYIi/asDtzBPMyMhVibhM/K6g/5qnKBwjZtp10bNZIEFTRW1MA==",
"requires": {
"regenerator-runtime": "^0.12.0"
}
},
"regenerator-runtime": {
"version": "0.12.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz",
"integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg=="
}
}
},
"react-swipeable-views-core": {
"version": "0.13.7",
"resolved": "https://registry.npmjs.org/react-swipeable-views-core/-/react-swipeable-views-core-0.13.7.tgz",
"integrity": "sha512-ekn9oDYfBt0oqJSGGwLEhKvn+QaqMGTy//9dURTLf+vp7W5j6GvmKryYdnwJCDITaPFI2hujXV4CH9krhvaE5w==",
"requires": {
"@babel/runtime": "7.0.0",
"warning": "^4.0.1"
},
"dependencies": {
"@babel/runtime": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.0.0.tgz",
"integrity": "sha512-7hGhzlcmg01CvH1EHdSPVXYX1aJ8KCEyz6I9xYIi/asDtzBPMyMhVibhM/K6g/5qnKBwjZtp10bNZIEFTRW1MA==",
"requires": {
"regenerator-runtime": "^0.12.0"
}
},
"regenerator-runtime": {
"version": "0.12.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz",
"integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg=="
}
}
},
"react-swipeable-views-utils": {
"version": "0.13.9",
"resolved": "https://registry.npmjs.org/react-swipeable-views-utils/-/react-swipeable-views-utils-0.13.9.tgz",
"integrity": "sha512-QLGxRKrbJCbWz94vkWLzb1Daaa2Y/TZKmsNKQ6WSNrS+chrlfZ3z9tqZ7YUJlW6pRWp3QZdLSY3UE3cN0TXXmw==",
"requires": {
"@babel/runtime": "7.0.0",
"keycode": "^2.1.7",
"prop-types": "^15.6.0",
"react-event-listener": "^0.6.0",
"react-swipeable-views-core": "^0.13.7",
"shallow-equal": "^1.2.1"
},
"dependencies": {
"@babel/runtime": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.0.0.tgz",
"integrity": "sha512-7hGhzlcmg01CvH1EHdSPVXYX1aJ8KCEyz6I9xYIi/asDtzBPMyMhVibhM/K6g/5qnKBwjZtp10bNZIEFTRW1MA==",
"requires": {
"regenerator-runtime": "^0.12.0"
}
},
"regenerator-runtime": {
"version": "0.12.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz",
"integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg=="
}
}
},
"react-transition-group": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.3.0.tgz",
"integrity": "sha512-1qRV1ZuVSdxPlPf4O8t7inxUGpdyO5zG9IoNfJxSO0ImU2A1YWkEQvFPuIPZmMLkg5hYs7vv5mMOyfgSkvAwvw==",
"requires": {
"@babel/runtime": "^7.5.5",
"dom-helpers": "^5.0.1",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2"
}
},
"read-pkg": { "read-pkg": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz",
@ -11506,6 +11849,11 @@
} }
} }
}, },
"shallow-equal": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.1.tgz",
"integrity": "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA=="
},
"shebang-command": { "shebang-command": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
@ -12888,6 +13236,14 @@
"makeerror": "1.0.x" "makeerror": "1.0.x"
} }
}, },
"warning": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
"requires": {
"loose-envify": "^1.0.0"
}
},
"watchpack": { "watchpack": {
"version": "1.6.0", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz",

View file

@ -4,6 +4,9 @@
"private": true, "private": true,
"dependencies": { "dependencies": {
"@auth0/auth0-spa-js": "^1.6.4", "@auth0/auth0-spa-js": "^1.6.4",
"@material-ui/core": "^4.9.8",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "^4.0.0-alpha.47",
"@testing-library/jest-dom": "^4.2.4", "@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.4.0", "@testing-library/react": "^9.4.0",
"@testing-library/user-event": "^7.2.1", "@testing-library/user-event": "^7.2.1",
@ -13,12 +16,14 @@
"@types/react": "^16.9.19", "@types/react": "^16.9.19",
"@types/react-dom": "^16.9.5", "@types/react-dom": "^16.9.5",
"@types/react-router-dom": "^5.1.3", "@types/react-router-dom": "^5.1.3",
"@types/react-swipeable-views": "^0.13.0",
"@types/underscore": "^1.9.4", "@types/underscore": "^1.9.4",
"history": "^4.10.1", "history": "^4.10.1",
"react": "^16.12.0", "react": "^16.12.0",
"react-dom": "^16.12.0", "react-dom": "^16.12.0",
"react-router-dom": "^5.1.2", "react-router-dom": "^5.1.2",
"react-scripts": "3.3.1", "react-scripts": "3.3.1",
"react-swipeable-views": "^0.13.9",
"typescript": "^3.7.5", "typescript": "^3.7.5",
"underscore": "^1.9.2" "underscore": "^1.9.2"
}, },

View file

@ -12,11 +12,11 @@
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link <!-- <link
rel="stylesheet" rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css"
/> />
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script> -->
<link <link
rel="stylesheet" rel="stylesheet"
href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"

View file

@ -1,8 +1,14 @@
import React, { FC } from "react"; import React from "react";
import Layout from "./pages/Layout"; import { Router } from "react-router-dom";
import { useAuth0 } from "./authentication/auth0"; import { useAuth0 } from "./authentication/auth0";
import * as createHistory from "history";
// import history from "./utils/history";
import MainLayout from "./layouts/MainLayout";
import { AppRouter } from "./utils/router";
const App: FC = () => { export const history = createHistory.createBrowserHistory();
export default function App() {
const { loading } = useAuth0(); const { loading } = useAuth0();
if (loading) { if (loading) {
@ -10,10 +16,10 @@ const App: FC = () => {
} }
return ( return (
<div className="App"> <Router history={history}>
<Layout /> <MainLayout>
</div> <AppRouter />
</MainLayout>
</Router>
); );
}; }
export default App;

View file

@ -16,8 +16,9 @@ export class UserVM {
public projects: Project[]; public projects: Project[];
public tickets: Ticket[]; public tickets: Ticket[];
public activities: Activity[]; public activities: Activity[];
public allUsers: User[];
public constructor(user: User) { public constructor(user: User, allUsers: User[]) {
this.id = user.id; this.id = user.id;
this.firstName = user.firstName; this.firstName = user.firstName;
this.lastName = user.lastName; this.lastName = user.lastName;
@ -30,5 +31,6 @@ export class UserVM {
this.projects = user.projects; this.projects = user.projects;
this.tickets = user.tickets; this.tickets = user.tickets;
this.activities = user.activities; this.activities = user.activities;
this.allUsers = allUsers;
} }
} }

View file

@ -0,0 +1,90 @@
// src/react-auth0-spa.js
import React, { useState, useEffect, useContext } from "react";
import createAuth0Client from "@auth0/auth0-spa-js";
const DEFAULT_REDIRECT_CALLBACK = () =>
window.history.replaceState({}, document.title, window.location.pathname);
export const Auth0Context = React.createContext();
export const useAuth0 = () => useContext(Auth0Context);
export const Auth0Provider = ({
children,
onRedirectCallback = DEFAULT_REDIRECT_CALLBACK,
...initOptions
}) => {
const [isAuthenticated, setIsAuthenticated] = useState();
const [user, setUser] = useState();
const [auth0Client, setAuth0] = useState();
const [loading, setLoading] = useState(true);
const [popupOpen, setPopupOpen] = useState(false);
useEffect(() => {
const initAuth0 = async () => {
const auth0FromHook = await createAuth0Client(initOptions);
setAuth0(auth0FromHook);
if (
window.location.search.includes("code=") &&
window.location.search.includes("state=")
) {
const { appState } = await auth0FromHook.handleRedirectCallback();
onRedirectCallback(appState);
}
const isAuthenticated = await auth0FromHook.isAuthenticated();
setIsAuthenticated(isAuthenticated);
if (isAuthenticated) {
const user = await auth0FromHook.getUser();
setUser(user);
}
setLoading(false);
};
initAuth0();
// eslint-disable-next-line
}, []);
const loginWithPopup = async (params = {}) => {
setPopupOpen(true);
try {
await auth0Client.loginWithPopup(params);
} catch (error) {
console.error(error);
} finally {
setPopupOpen(false);
}
const user = await auth0Client.getUser();
setUser(user);
setIsAuthenticated(true);
};
const handleRedirectCallback = async () => {
setLoading(true);
await auth0Client.handleRedirectCallback();
const user = await auth0Client.getUser();
setLoading(false);
setIsAuthenticated(true);
setUser(user);
};
return (
<Auth0Context.Provider
value={{
isAuthenticated,
user,
loading,
popupOpen,
loginWithPopup,
handleRedirectCallback,
getIdTokenClaims: (...p) => auth0Client.getIdTokenClaims(...p),
loginWithRedirect: (...p) => auth0Client.loginWithRedirect(...p),
getTokenSilently: (...p) => auth0Client.getTokenSilently(...p),
getTokenWithPopup: (...p) => auth0Client.getTokenWithPopup(...p),
logout: (...p) => auth0Client.logout(...p)
}}
>
{children}
</Auth0Context.Provider>
);
};

View file

@ -1,112 +0,0 @@
import React, { useState, useEffect, useContext, FC } from "react";
import createAuth0Client from "@auth0/auth0-spa-js";
import Auth0Client from "@auth0/auth0-spa-js/dist/typings/Auth0Client";
interface IAuth0Context {
isAuthenticated: boolean;
user: any;
loading: boolean;
popupOpen: boolean;
loginWithPopup(options: PopupLoginOptions): Promise<void>;
handleRedirectCallback(): Promise<RedirectLoginResult>;
getIdTokenClaims(o?: getIdTokenClaimsOptions): Promise<IdToken>;
loginWithRedirect(o: RedirectLoginOptions): Promise<void>;
getTokenSilently(o?: GetTokenSilentlyOptions): Promise<string | undefined>;
getTokenWithPopup(o?: GetTokenWithPopupOptions): Promise<string | undefined>;
logout(o?: LogoutOptions): void;
}
export interface IAuth0ProviderOptions {
children: React.ReactElement;
onRedirectCallback?(result: RedirectLoginResult): void;
}
const DEFAULT_REDIRECT_CALLBACK: () => void = () =>
window.history.replaceState({}, document.title, window.location.pathname);
export const Auth0Context: React.Context<IAuth0Context | null> = React.createContext<IAuth0Context | null>(
null
);
export const useAuth0: () => IAuth0Context = () => useContext(Auth0Context)!;
export const Auth0Provider = ({
children,
onRedirectCallback = DEFAULT_REDIRECT_CALLBACK,
...initOptions
}: IAuth0ProviderOptions & Auth0ClientOptions) => {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [user, setUser] = useState();
const [auth0Client, setAuth0] = useState<Auth0Client>();
const [loading, setLoading] = useState(true);
const [popupOpen, setPopupOpen] = useState(false);
useEffect(() => {
const initAuth0 = async () => {
const auth0FromHook = await createAuth0Client(initOptions);
setAuth0(auth0FromHook);
if (window.location.search.includes("code=")) {
const { appState } = await auth0FromHook.handleRedirectCallback();
onRedirectCallback(appState);
}
const isAuthenticated = await auth0FromHook.isAuthenticated();
setIsAuthenticated(isAuthenticated);
if (isAuthenticated) {
const user = await auth0FromHook.getUser();
setUser(user);
}
setLoading(false);
};
initAuth0();
// eslint-disable-next-line
}, []);
const loginWithPopup = async (o: PopupLoginOptions) => {
setPopupOpen(true);
try {
await auth0Client!.loginWithPopup(o);
} catch (error) {
console.error(error);
} finally {
setPopupOpen(false);
}
const user = await auth0Client!.getUser();
setUser(user);
setIsAuthenticated(true);
};
const handleRedirectCallback = async () => {
setLoading(true);
const result = await auth0Client!.handleRedirectCallback();
const user = await auth0Client!.getUser();
setLoading(false);
setIsAuthenticated(true);
setUser(user);
return result;
};
return (
<Auth0Context.Provider
value={{
isAuthenticated,
user,
loading,
popupOpen,
loginWithPopup,
handleRedirectCallback,
getIdTokenClaims: (o: getIdTokenClaimsOptions | undefined) =>
auth0Client!.getIdTokenClaims(o),
loginWithRedirect: (o: RedirectLoginOptions) =>
auth0Client!.loginWithRedirect(o),
getTokenSilently: (o: GetTokenSilentlyOptions | undefined) =>
auth0Client!.getTokenSilently(o),
getTokenWithPopup: (o: GetTokenWithPopupOptions | undefined) =>
auth0Client!.getTokenWithPopup(o),
logout: (o: LogoutOptions | undefined) => auth0Client!.logout(o)
}}
>
{children}
</Auth0Context.Provider>
);
};

View file

@ -1,35 +0,0 @@
import React, { FC, useState, ChangeEvent, MouseEvent } from "react";
import { AppFile } from "../types/AppFile";
import { FileCollection } from "./FileCollection";
import { InputFile } from "./InputFile";
import { FilterBar } from "./FilterBar";
type IProps = {
files: AppFile[];
};
export const FileList: FC<IProps> = ({ files }) => {
const [filterText, setFilterText] = useState<string>("");
const clearFilterText: (e: MouseEvent) => void = (e: MouseEvent) => {
setFilterText("");
};
const handleChange: (e: ChangeEvent<HTMLInputElement>) => void = (
e: ChangeEvent<HTMLInputElement>
) => {
setFilterText(e.target.value);
};
return (
<>
<div className="row valign-wrapper">
<h3>Files</h3>
<FilterBar
filterText={filterText}
handleChange={handleChange}
clearFilterText={clearFilterText}
/>
</div>
<InputFile />
<FileCollection files={files} filterText={filterText} />
</>
);
};

View file

@ -1,18 +0,0 @@
import React, { FC } from "react";
interface IProps {
picture: string;
}
export const Avatar: FC<IProps> = ({ picture }) => {
return (
<>
<img
className="circle"
src={picture}
height="100vh"
width="100vh"
alt="user avatar"
/>
</>
);
};

View file

@ -1,27 +0,0 @@
import React, { FC } from "react";
import { User } from "../types/User";
import { Link } from "react-router-dom";
interface AvatarListProps {
users: User[];
}
export const AvatarList: FC<AvatarListProps> = ({ users }) => {
return users === undefined ? (
<></>
) : (
<>
{users.map((user: User, i: number) => (
<Link to={`/users/${user.id}`} key={i}>
<img
className="circle"
src={user.picture}
width="32vh"
height="32vh"
alt={user.fullName}
/>
</Link>
))}
</>
);
};

View file

@ -0,0 +1,36 @@
import React, { FC } from "react";
import { Link } from "react-router-dom";
import Avatar from "@material-ui/core/Avatar";
import AvatarGroup from "@material-ui/lab/AvatarGroup";
import { User } from "../../types/User";
import { makeStyles, Theme, createStyles } from "@material-ui/core";
interface AvatarListProps {
users: User[];
}
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
marginLeft: theme.spacing(2),
marginRight: theme.spacing(2),
},
})
);
export const AvatarList: FC<AvatarListProps> = ({ users }) => {
const classes = useStyles();
return users === undefined ? (
<></>
) : (
<div className={classes.root}>
<AvatarGroup max={5}>
{users.map((user: User, i: number) => (
<Link to={`/users/${user.id}`} key={i}>
<Avatar src={user.picture} alt={user.fullName} />
</Link>
))}
</AvatarGroup>
</div>
);
};

View file

@ -0,0 +1,37 @@
import React, { FC } from "react";
import { makeStyles, createStyles, Theme } from "@material-ui/core/styles";
import Avatar from "@material-ui/core/Avatar";
interface IProps {
picture: string;
alt: string;
}
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
display: "flex",
"& > *": {
margin: theme.spacing(1)
}
},
small: {
width: theme.spacing(3),
height: theme.spacing(3)
},
large: {
width: theme.spacing(10),
height: theme.spacing(10)
}
})
);
export const UserAvatar: FC<IProps> = ({ picture, alt }) => {
const classes = useStyles();
return (
<div className={classes.root}>
<Avatar alt={alt} src={picture} className={classes.large} />
</div>
);
};

View file

@ -0,0 +1,59 @@
import React from "react";
import { createStyles, makeStyles, Theme } from "@material-ui/core/styles";
import AppBar from "@material-ui/core/AppBar";
import Toolbar from "@material-ui/core/Toolbar";
import Typography from "@material-ui/core/Typography";
import Button from "@material-ui/core/Button";
import IconButton from "@material-ui/core/IconButton";
import MenuIcon from "@material-ui/icons/Menu";
import { useAuth0 } from "../authentication/auth0";
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
flexGrow: 1
},
menuButton: {
marginRight: theme.spacing(2)
},
title: {
flexGrow: 1
}
})
);
export default function ButtonAppBar() {
const classes = useStyles();
const { isAuthenticated, loginWithRedirect, logout } = useAuth0();
return (
<div className={classes.root}>
<AppBar position="static">
<Toolbar>
<IconButton
edge="start"
className={classes.menuButton}
color="inherit"
aria-label="menu"
>
<MenuIcon />
</IconButton>
<Typography variant="h6" className={classes.title}>
<Button color="inherit" href="/">
BugBuster
</Button>
</Typography>
{!isAuthenticated ? (
<Button color="inherit" onClick={() => loginWithRedirect({})}>
Log in
</Button>
) : (
<Button color="inherit" onClick={() => logout()}>
Log out
</Button>
)}
</Toolbar>
</AppBar>
</div>
);
}

View file

@ -0,0 +1,26 @@
import React, { FC, MouseEvent } from "react";
import { Fab } from "@material-ui/core";
import AddIcon from "@material-ui/icons/Add";
interface IProps {
icon?: string;
color?: "inherit" | "primary" | "secondary" | "default" | undefined;
onClick?: (e: MouseEvent) => void;
size?: "small" | "medium" | "large" | undefined;
text?: string;
}
export const FloatingButton: FC<IProps> = ({
color,
icon,
size,
text,
onClick
}) => {
return (
<Fab color={color} aria-label={icon} size={size} onClick={onClick}>
<AddIcon />
{text}
</Fab>
);
};

View file

@ -0,0 +1,48 @@
import React, { FC, ReactNode } from "react";
import { Link } from "react-router-dom";
import { makeStyles } from "@material-ui/core/styles";
import Card from "@material-ui/core/Card";
import CardActions from "@material-ui/core/CardActions";
import CardContent from "@material-ui/core/CardContent";
import Typography from "@material-ui/core/Typography";
import { ProgressBar } from "../Progress/ProgressBar";
interface IProps {
title?: string;
link?: string;
progress?: number;
content: ReactNode;
actions?: ReactNode;
}
const useStyles = makeStyles({
root: {
minWidth: 275,
marginBottom: 20,
},
});
export const HorizontalCard: FC<IProps> = ({
title,
link = "#",
content,
actions,
progress = 0,
}) => {
const classes = useStyles();
return (
<Card className={classes.root}>
<ProgressBar value={progress} />
<CardContent>
<Typography variant="h5" component="h2">
<Link to={link}>
<b>{title ?? "Nothing to do"}</b>
</Link>
</Typography>
{content}
</CardContent>
<CardActions>{actions}</CardActions>
</Card>
);
};

View file

@ -0,0 +1,63 @@
import React, { FC } from "react";
import { HorizontalCard } from "./HorizontalCard";
import { makeStyles, Theme, createStyles } from "@material-ui/core";
import { AvatarList } from "../Avatars/AvatarList";
import { ProgressInfo } from "../Progress/ProgressInfo";
import { User } from "../../types/User";
import { getRemainingdays } from "../../utils/methods";
interface IProps {
title?: string;
remainingDays?: string;
link?: string;
members?: User[];
progress?: number;
ticketsNumber?: number;
ticketsDone?: number;
}
const useStyles = makeStyles((theme: Theme) =>
createStyles({
progress: {
paddingTop: theme.spacing(2),
},
})
);
const ProjectCard: FC<IProps> = ({
title,
remainingDays = "",
link = "#",
members,
progress = 0,
ticketsNumber,
ticketsDone,
}) => {
const classes = useStyles();
const Content: FC = () => {
return (
<>
{members && <AvatarList users={members} />}
<div className={classes.progress}>
<ProgressInfo
remainingDays={getRemainingdays(remainingDays)}
tasksDone={ticketsDone}
tasksTotalCount={ticketsNumber}
/>
</div>
</>
);
};
return (
<HorizontalCard
title={title}
link={link}
content={<Content />}
progress={progress}
/>
);
};
export default ProjectCard;

View file

@ -0,0 +1,65 @@
import React, { FC, MouseEvent } from "react";
import { Button, Typography, Grid } from "@material-ui/core";
import { HorizontalCard } from "./HorizontalCard";
import TicketChipsArray from "./TicketChipsArray";
import { Ticket } from "../../types/Ticket";
import { getRemainingdays } from "../../utils/methods";
interface IProps {
ticket?: Ticket;
validateTicket?: (event: MouseEvent) => void;
link?: string;
}
const TicketCard: FC<IProps> = ({
link = "#",
validateTicket,
ticket = {} as Ticket,
}) => {
const Content: FC = () => {
return (
<Grid container justify="space-between" alignItems="center">
<TicketChipsArray
status={ticket.status}
category={ticket.category}
impact={ticket.impact}
difficulty={ticket.difficulty}
/>
<Typography variant="body2" component="p">
<span>
Due in{" "}
{ticket?.endingDate ? (
getRemainingdays(ticket?.endingDate)
) : (
<span>
<del>Too much</del> 0
</span>
)}{" "}
days
</span>
</Typography>
</Grid>
);
};
const Action = () => {
return (
<>
<Button size="small" onClick={validateTicket}>
Mark as done
</Button>
</>
);
};
return (
<HorizontalCard
title={ticket?.title}
link={link}
content={<Content />}
actions={<Action />}
/>
);
};
export default TicketCard;

View file

@ -0,0 +1,76 @@
import React, { FC } from "react";
import { Grid, Chip, makeStyles, Theme, createStyles } from "@material-ui/core";
import CategoryIcon from "@material-ui/icons/Category";
import PriorityHighIcon from "@material-ui/icons/PriorityHigh";
import SpeedIcon from "@material-ui/icons/Speed";
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
display: "flex",
justifyContent: "space-between",
flexWrap: "wrap",
listStyle: "none",
padding: theme.spacing(0.5),
margin: 0,
marginTop: 20,
marginBottom: 20,
},
chip: {
margin: theme.spacing(0.5),
},
})
);
interface IProps {
status: string;
category: string;
impact: string;
difficulty: string;
}
const TicketChipsArray: FC<IProps> = ({
status,
category,
impact,
difficulty,
}) => {
const classes = useStyles();
const chipData = [
// { label: "status", value: status },
{ label: "category", value: category },
{ label: "impact", value: impact },
{ label: "difficulty", value: difficulty },
];
return (
// <Paper component="ul" className={classes.root}>
<Grid component="ul" className={classes.root}>
{chipData.map((data, i: number) => {
let icon = <CategoryIcon />;
let color: "inherit" | "default" | "primary" | "secondary" | undefined;
if (data.label === "impact") {
color = "primary";
icon = <PriorityHighIcon />;
}
if (data.label === "difficulty") {
color = "secondary";
icon = <SpeedIcon />;
}
return (
<li key={i}>
<Chip
icon={icon}
color={color}
label={data.value}
className={classes.chip}
/>
</li>
);
})}
</Grid>
);
};
export default TicketChipsArray;

View file

@ -1,4 +1,11 @@
import React, { FC } from "react"; import React, { FC } from "react";
import { createStyles, Theme, makeStyles } from "@material-ui/core/styles";
import List from "@material-ui/core/List";
import ListItem from "@material-ui/core/ListItem";
import ListItemText from "@material-ui/core/ListItemText";
import ListItemAvatar from "@material-ui/core/ListItemAvatar";
import Avatar from "@material-ui/core/Avatar";
import WorkIcon from "@material-ui/icons/Work";
import { AppFile } from "../types/AppFile"; import { AppFile } from "../types/AppFile";
type IProps = { type IProps = {
@ -6,24 +13,32 @@ type IProps = {
filterText: string; filterText: string;
}; };
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
width: "100%",
maxWidth: 360,
backgroundColor: theme.palette.background.paper
}
})
);
export const FileCollection: FC<IProps> = ({ files, filterText }) => { export const FileCollection: FC<IProps> = ({ files, filterText }) => {
console.log(); const classes = useStyles();
return ( return (
<> <List className={classes.root}>
<ul className="collection"> {files.length === 0 ? (
{files.length === 0 ? ( <FileEntry />
<FileEntry /> ) : (
) : ( files
files .filter(
.filter( f =>
f => f.name.toLowerCase().includes(filterText.toLowerCase()) ||
f.name.toLowerCase().includes(filterText.toLowerCase()) || f.format.toLowerCase().includes(filterText.toLowerCase())
f.format.toLowerCase().includes(filterText.toLowerCase()) )
) .map((file: AppFile) => <FileEntry file={file} key={file.id} />)
.map((file: AppFile) => <FileEntry file={file} key={file.id} />) )}
)} </List>
</ul>
</>
); );
}; };
@ -33,16 +48,16 @@ type IFProps = {
export const FileEntry: FC<IFProps> = ({ file }) => { export const FileEntry: FC<IFProps> = ({ file }) => {
return ( return (
<li className="collection-item avatar"> <ListItem>
{/* <img src={require("../images/user_1.jpg")} alt="" className="circle" /> */} <ListItemAvatar>
<i className="material-icons circle indigo lighten-1">folder</i> <Avatar>
<span className="title">{file ? file.name : "Add your first file"}</span> <WorkIcon />
<p> </Avatar>
{file ? file.size : 0}kb {file ? file.format : "pdf"} </ListItemAvatar>
</p> <ListItemText
<a href="#!" className="secondary-content"> primary={file ? file.name : "Add your first file"}
<i className="material-icons">more_vert</i> secondary={`${file ? file.size : 0}kb ${file ? file.format : "pdf"}`}
</a> />
</li> </ListItem>
); );
}; };

View file

@ -1,5 +1,9 @@
import React, { FC, ChangeEvent, MouseEvent } from "react"; import React, { FC, ChangeEvent, MouseEvent } from "react";
import { useRouteMatch } from "react-router-dom"; import { useRouteMatch } from "react-router-dom";
import { createStyles, makeStyles, Theme } from "@material-ui/core/styles";
import TextField from "@material-ui/core/TextField";
import { Grid } from "@material-ui/core";
// import { AccountCircle, FilterList, FilterListSharp } from "@material-ui/icons";
type IProps = { type IProps = {
filterText: string; filterText: string;
@ -7,35 +11,47 @@ type IProps = {
clearFilterText: (e: MouseEvent<HTMLInputElement>) => void; clearFilterText: (e: MouseEvent<HTMLInputElement>) => void;
}; };
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
"& > *": {
margin: theme.spacing(1),
width: "25ch",
},
},
margin: {
margin: theme.spacing(1),
},
filter: {
backgroundColor: "#fff",
},
})
);
export const FilterBar: FC<IProps> = ({ export const FilterBar: FC<IProps> = ({
filterText, filterText,
handleChange, handleChange,
clearFilterText // clearFilterText
}) => { }) => {
const { url } = useRouteMatch(); const { url } = useRouteMatch();
const placeholder: string = url.split("/")[3] || "users"; const placeholder: string = url.split("/")[3] || "elements";
const classes = useStyles();
return ( return (
<> <div className={classes.margin}>
<div className="nav-wrapper"> <Grid container spacing={1} alignItems="flex-end">
<div className="input-field"> <Grid item>
<input <TextField
// className="validate" id="input-with-icon-grid"
id="filter" variant="outlined"
type="search" label={`Filter ${placeholder}`}
required size="small"
name="filter"
value={filterText} value={filterText}
placeholder={`Filter ${placeholder}`}
onChange={handleChange} onChange={handleChange}
color="primary"
className={classes.filter}
/> />
<label className="label-icon" htmlFor="search"> </Grid>
<i className="material-icons">filter_list</i> </Grid>
</label> </div>
<i className="material-icons" onClick={clearFilterText}>
close
</i>
</div>
</div>
</>
); );
}; };

View file

@ -1,23 +0,0 @@
import React, { FC, MouseEvent } from "react";
import { Button } from "./Button";
interface IProps {
icon?: string;
size?: string;
color?: string;
onClick?: (e: MouseEvent) => void;
}
export const FloatingButton: FC<IProps> = ({
icon = "add",
size = "small",
color = "red",
onClick
}) => {
const iconComponent = <i className="material-icons left">{icon}</i>;
return (
<Button color={color} size={size} shape="btn-floating" onClick={onClick}>
{iconComponent}
</Button>
);
};

View file

@ -0,0 +1,61 @@
import React, { FC } from "react";
import Typography from "@material-ui/core/Typography";
import { makeStyles } from "@material-ui/core/styles";
import Container from "@material-ui/core/Container";
import Link from "@material-ui/core/Link";
interface IProps {
brand: string;
text: string;
}
const copyParams: IProps = {
brand: "BugBuster",
text: "Made with 🔥"
};
const Copyright: FC<IProps> = ({ brand, text }) => {
return (
<Typography variant="body2" color="textSecondary">
{"© "}
<Link color="inherit" href="/">
{brand}
</Link>{" "}
{new Date().getFullYear()}
{`. All Rights Reserved. ${text}`}
</Typography>
);
};
const useStyles = makeStyles(theme => ({
footer: {
padding: theme.spacing(3, 2),
marginTop: "auto",
backgroundColor:
theme.palette.type === "light"
? theme.palette.grey[200]
: theme.palette.grey[800]
}
}));
export default function Footer() {
const classes = useStyles();
return (
<footer className={classes.footer}>
<Container maxWidth="sm">
<Typography variant="body1">
<Link
color="inherit"
href="https://github.com/rjNemo"
target="_blank"
rel="noopener"
>
Ruidy Nemausat
</Link>{" "}
</Typography>
<Copyright brand={copyParams.brand} text={copyParams.text} />
</Container>
</footer>
);
}

View file

@ -1,4 +1,5 @@
import React, { FC } from "react"; import React, { FC } from "react";
import { Typography, Box } from "@material-ui/core";
type HeaderProps = { type HeaderProps = {
title: string; title: string;
@ -7,9 +8,13 @@ type HeaderProps = {
export const Header: FC<HeaderProps> = ({ title, description }) => { export const Header: FC<HeaderProps> = ({ title, description }) => {
return ( return (
<> <Box>
<h1>{title}</h1> <Typography variant="h2" component="h2">
<p className="lead">{description}</p> {title}
</> </Typography>
<Typography variant="subtitle2" component="h3">
{description}
</Typography>
</Box>
); );
}; };

View file

@ -1,60 +0,0 @@
import React, { FC, MouseEvent } from "react";
import { Link } from "react-router-dom";
import { getRemainingdays } from "../utils/methods";
interface IProps {
title?: string;
remainingDays?: string;
validateTicket?: (event: MouseEvent) => void;
link?: string;
}
export const HorizontalCard: FC<IProps> = ({
title,
remainingDays,
link = "#",
validateTicket
}) => {
return (
<li>
<div className="card horizontal">
<div className="card-stacked">
<div className="card-content">
<div className="row">
<div className="card-title">
<h6>
<Link to={link}>
<b>{title ?? "Nothing to do"}</b>
</Link>
</h6>
</div>
<span>
Due{" "}
{remainingDays ? (
getRemainingdays(remainingDays)
) : (
<span>
<del>Too much</del> 0
</span>
)}{" "}
days
</span>
<div className="right">
<Link to="#">
<i className="material-icons" onClick={validateTicket}>
check
</i>
</Link>
{/* <Link to="#">
<i className="material-icons" onClick={archiveTicket}>
archive
</i>
</Link> */}
</div>
</div>
</div>
</div>
</div>
</li>
);
};

View file

@ -1,29 +1,62 @@
import React, { FC } from "react"; import React, { FC } from "react";
import { CloudUpload } from "@material-ui/icons";
import { makeStyles, createStyles, Theme } from "@material-ui/core/styles";
import Button from "@material-ui/core/Button";
type IProps = {}; export const InputFile: FC = () => {
export const InputFile: FC<IProps> = () => {
return ( return (
<> <>
<form action="/upload"> <form action="/upload">
<div className="file-field input-field"> <div className="file-field input-field">
<div className="btn indigo lighten-1"> <UploadButton>
<i className="material-icons ">cloud_upload</i> <CloudUpload />
<input <input
type="file" type="file"
multiple multiple
accept=".doc,.docx,.pdf,.md,.gdoc,.zip,image/*" accept=".doc,.docx,.pdf,.md,.gdoc,.zip,image/*"
/> />
</div> </UploadButton>
<div className="file-path-wrapper">
<input
className="file-path validate"
type="text"
placeholder="Upload one or more files"
/>
</div>
</div> </div>
</form> </form>
</> </>
); );
}; };
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
"& > *": {
margin: theme.spacing(1)
}
},
input: {
display: "none"
}
})
);
const UploadButton: FC = () => {
const classes = useStyles();
return (
<div className={classes.root}>
<input
accept=".doc,.docx,.pdf,.md,.gdoc,.zip,image/*"
className={classes.input}
id="contained-button-file"
multiple
type="file"
/>
<label htmlFor="contained-button-file">
<Button
variant="contained"
color="primary"
component="span"
startIcon={<CloudUpload />}
>
Upload files
</Button>
</label>
</div>
);
};

View file

@ -1,7 +1,7 @@
import React, { FC, useState, ChangeEvent, MouseEvent } from "react"; import React, { FC, useState, ChangeEvent, MouseEvent } from "react";
import { ActivityCollection } from "./ActivityCollection"; import { ActivityCollection } from "../ActivityCollection";
import { Activity } from "../types/Activity"; import { Activity } from "../../types/Activity";
import { FilterBar } from "./FilterBar"; import { FilterBar } from "../FilterBar";
type IProps = { type IProps = {
activities: Activity[]; activities: Activity[];
@ -12,9 +12,7 @@ export const ActivityList: FC<IProps> = ({ activities }) => {
const clearFilterText: (e: MouseEvent) => void = (e: MouseEvent) => { const clearFilterText: (e: MouseEvent) => void = (e: MouseEvent) => {
setFilterText(""); setFilterText("");
}; };
const handleChange: (e: ChangeEvent<HTMLInputElement>) => void = ( const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
e: ChangeEvent<HTMLInputElement>
) => {
setFilterText(e.target.value); setFilterText(e.target.value);
}; };

View file

@ -0,0 +1,40 @@
import React, { FC, useState, ChangeEvent, MouseEvent } from "react";
import { AppFile } from "../../types/AppFile";
import { FileCollection } from "../FileCollection";
import { InputFile } from "../InputFile";
import { FilterBar } from "../FilterBar";
import { Grid, Typography } from "@material-ui/core";
type IProps = {
files: AppFile[];
};
export const FileList: FC<IProps> = ({ files }) => {
const [filterText, setFilterText] = useState<string>("");
const clearFilterText = (e: MouseEvent): void => {
setFilterText("");
};
const handleChange = (e: ChangeEvent<HTMLInputElement>): void => {
setFilterText(e.target.value);
};
return (
<>
<Grid container>
<Grid item xs>
<Typography variant="h4" component="h4">
Files
</Typography>
</Grid>
<Grid item xs={4}>
<FilterBar
filterText={filterText}
handleChange={handleChange}
clearFilterText={clearFilterText}
/>
</Grid>
</Grid>
<InputFile />
<FileCollection files={files} filterText={filterText} />
</>
);
};

View file

@ -1,7 +1,7 @@
import React, { FC, useState, ChangeEvent, MouseEvent } from "react"; import React, { FC, useState, ChangeEvent, MouseEvent } from "react";
import { UsersModalEntry } from "./UsersModalEntry"; import { UsersModalEntry } from "../Modals/UsersModalEntry";
import { FilterBar } from "./FilterBar"; import { FilterBar } from "../FilterBar";
import { User } from "../types/User"; import { User } from "../../types/User";
interface IProps { interface IProps {
users: User[]; users: User[];
@ -10,12 +10,10 @@ interface IProps {
export const MemberList: FC<IProps> = ({ users }) => { export const MemberList: FC<IProps> = ({ users }) => {
const [members, setMembers] = useState<User[]>([]); const [members, setMembers] = useState<User[]>([]);
const [filterText, setFilterText] = useState<string>(""); const [filterText, setFilterText] = useState<string>("");
const clearFilterText: (e: MouseEvent) => void = (e: MouseEvent) => { const clearFilterText = (e: MouseEvent): void => {
setFilterText(""); setFilterText("");
}; };
const handleChange: (e: ChangeEvent<HTMLInputElement>) => void = ( const handleChange = (e: ChangeEvent<HTMLInputElement>): void => {
e: ChangeEvent<HTMLInputElement>
) => {
setFilterText(e.target.value); setFilterText(e.target.value);
}; };
return ( return (

View file

@ -0,0 +1,111 @@
import React, { FC, useState, ChangeEvent, MouseEvent } from "react";
import {
Typography,
Grid,
makeStyles,
createStyles,
Theme,
} from "@material-ui/core";
import { FilterBar } from "../FilterBar";
import ProjectCard from "../Cards/ProjectCard";
import { FloatingButton } from "../Buttons/FloatingButton";
import { NewProjectModal } from "../Modals/NewProjectModal";
import { Project } from "../../types/Project";
import { User } from "../../types/User";
const useStyles = makeStyles((theme: Theme) =>
createStyles({
header: {
paddingBottom: theme.spacing(2),
},
addButton: {
position: "relative",
marginLeft: "20px",
},
})
);
type IProps = {
projects: Project[];
allUsers: User[];
};
export const ProjectList: FC<IProps> = ({ projects, allUsers }) => {
const [filterText, setFilterText] = useState<string>("");
const clearFilterText: (e: MouseEvent) => void = (e: MouseEvent) => {
setFilterText("");
};
const [showNew, setShowNew] = useState(false);
const onClick = (e: MouseEvent): void => {
e.preventDefault();
setShowNew(true);
};
const handleChange = (e: ChangeEvent<HTMLInputElement>): void => {
setFilterText(e.target.value);
};
let filteredProjects = projects.filter(
(t) =>
t.status !== "Done" &&
t.title.toLowerCase().includes(filterText.toLowerCase())
);
const classes = useStyles();
return (
<>
<NewProjectModal
handleClose={() => {
setShowNew(false);
}}
show={showNew}
allUsers={allUsers}
/>
<Grid container>
<Grid
container
direction="row"
justify="space-between"
alignItems="center"
className={classes.header}
>
<Typography variant="h4" component="h4">
Projects
<span className={classes.addButton}>
<FloatingButton color="primary" size="small" onClick={onClick} />
</span>
</Typography>
<FilterBar
filterText={filterText}
handleChange={handleChange}
clearFilterText={clearFilterText}
/>
</Grid>
<Grid item xs={12}>
<div className="col s12 grey lighten-1">
{filteredProjects.length === 0 ? (
<ProjectCard />
) : (
filteredProjects.map((t: Project) => (
<ProjectCard
key={t.id}
title={t.title}
remainingDays={t.endingDate}
link={`/projects/${t.id}`}
members={t.users}
progress={t.progression}
ticketsNumber={t.tickets === undefined ? 0 : t.tickets.length}
ticketsDone={
t.tickets === undefined
? 0
: t.tickets.filter((t) => t.status === "Done").length
}
/>
))
)}
</div>
</Grid>
</Grid>
</>
);
};

View file

@ -0,0 +1,126 @@
import React, { FC, useState, ChangeEvent, MouseEvent } from "react";
import {
Grid,
Typography,
makeStyles,
Theme,
createStyles,
} from "@material-ui/core";
import { FloatingButton } from "../Buttons/FloatingButton";
import { FilterBar } from "../FilterBar";
import { HttpResponse } from "../../types/HttpResponse";
import { Ticket } from "../../types/Ticket";
import { NewTicketModal } from "../Modals/NewTicketModal";
import { Project } from "../../types/Project";
import { put } from "../../utils/http";
import { Constants } from "../../utils/Constants";
import TicketCard from "../Cards/TicketCard";
const useStyles = makeStyles((theme: Theme) =>
createStyles({
header: {
paddingBottom: theme.spacing(2),
},
addButton: {
position: "relative",
marginLeft: "20px",
},
})
);
type TicketListProps = {
tickets: Ticket[];
allProjects: Project[];
addButton?: Boolean;
};
export const TicketList: FC<TicketListProps> = ({
tickets,
allProjects,
addButton = true,
}) => {
const [filterText, setFilterText] = useState<string>("");
const clearFilterText = (e: MouseEvent): void => {
setFilterText("");
};
const onClick = (e: MouseEvent): void => {
e.preventDefault();
setShowNew(true);
};
const handleChange = (e: ChangeEvent<HTMLInputElement>): void => {
setFilterText(e.target.value);
};
const [showNew, setShowNew] = useState(false);
let filteredTickets = tickets.filter(
(t) =>
t.status !== "Done" &&
t.title.toLowerCase().includes(filterText.toLowerCase())
);
const classes = useStyles();
return (
<>
<NewTicketModal
handleClose={() => {
setShowNew(false);
}}
show={showNew}
allProjects={allProjects}
/>
<Grid container>
<Grid
container
direction="row"
justify="space-between"
alignItems="center"
className={classes.header}
>
<Typography variant="h4" component="h4">
Tickets
{addButton && (
<span className={classes.addButton}>
<FloatingButton
color="primary"
size="small"
onClick={onClick}
/>
</span>
)}
</Typography>
<FilterBar
filterText={filterText}
handleChange={handleChange}
clearFilterText={clearFilterText}
/>
</Grid>
<Grid item xs={12}>
<div className="col s12 grey lighten-1">
{filteredTickets.length === 0 ? (
<TicketCard />
) : (
filteredTickets.map((t: Ticket) => (
<TicketCard
key={t.id}
ticket={t}
link={`/tickets/${t.id}`}
validateTicket={async (e: MouseEvent) => {
e.preventDefault();
await put<HttpResponse<Ticket>>(
`${Constants.ticketsURI}/${t.id}/closed`,
{}
);
}}
/>
))
)}
</div>
</Grid>
</Grid>
</>
);
};

View file

@ -1,7 +1,7 @@
import React, { FC } from "react"; import React, { FC } from "react";
import { InputField } from "./InputField"; import { InputField } from "./InputField";
import { PasswordField } from "./PasswordField"; import { PasswordField } from "./PasswordField";
import { Button } from "./Button"; import { Button } from "./Buttons/Button";
export const LogInForm: FC = () => { export const LogInForm: FC = () => {
return ( return (

View file

@ -1,16 +0,0 @@
import React, { FC, CSSProperties } from "react";
interface IProps {
handleClose: () => void;
show: boolean;
}
export const Modal: FC<IProps> = ({ handleClose, show, children }) => {
const showHideStyle: CSSProperties = show
? { display: "block", zIndex: 10 }
: { display: "none", zIndex: 10 };
return (
<div className="modal" style={showHideStyle}>
<div className="modal-content">{children}</div>
</div>
);
};

View file

@ -0,0 +1,79 @@
import React, { FC } from "react";
import Dialog from "@material-ui/core/Dialog";
import {
DialogTitle,
Typography,
IconButton,
DialogContent,
makeStyles,
Theme,
createStyles,
DialogActions,
Button,
} from "@material-ui/core";
import CloseIcon from "@material-ui/icons/Close";
interface IProps {
handleClose: () => void;
show: boolean;
action: string;
handleAction: (e: React.FormEvent) => Promise<void>;
name: string;
}
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
margin: 0,
padding: theme.spacing(2),
},
closeButton: {
position: "absolute",
right: theme.spacing(1),
top: theme.spacing(1),
color: theme.palette.grey[500],
},
})
);
export const Modal: FC<IProps> = ({
handleClose,
show,
action,
handleAction,
children,
name,
}) => {
const classes = useStyles();
return (
<Dialog
open={show}
onClose={handleClose}
maxWidth="md"
// fullWidth
>
<DialogTitle disableTypography className={classes.root}>
<Typography variant="h6">{name}</Typography>
{handleClose ? (
<IconButton
aria-label="close"
className={classes.closeButton}
onClick={handleClose}
>
<CloseIcon />
</IconButton>
) : null}
</DialogTitle>
<DialogContent dividers>{children}</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary">
Cancel
</Button>
<Button onClick={handleAction} color="primary">
{action}
</Button>
</DialogActions>
</Dialog>
);
};

View file

@ -0,0 +1,90 @@
import React, { FC, useState, FormEvent } from "react";
import { TextField } from "@material-ui/core";
import { Modal } from "./Modal";
import { Project } from "../../types/Project";
import { User } from "../../types/User";
import { post } from "../../utils/http";
import { Constants } from "../../utils/Constants";
interface IProps {
show: boolean;
handleClose: () => void;
allUsers: User[];
}
export const NewProjectModal: FC<IProps> = ({ show, handleClose }) => {
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
const [endingDate, setEndingDate] = useState("");
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
let newProject = {
title: title,
description: description,
endingDate: new Date(endingDate).toISOString(),
managerId: "cd179eb7-3a54-4060-b22c-3e947bdffcbc", // get current User id
};
await post<Project>(`${Constants.projectsURI}`, newProject);
handleClose();
};
return (
<Modal
name="New Project"
show={show}
handleClose={handleClose}
action="New Project"
handleAction={handleSubmit}
>
<TextField
variant="outlined"
margin="normal"
required
fullWidth
id="title"
value={title}
label="Title"
name="text"
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setTitle(e.target.value)
}
autoFocus
/>
<TextField
variant="outlined"
margin="normal"
required
fullWidth
id="description"
value={description}
label="Description"
name="text"
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
setDescription(e.target.value)
}
multiline
/>
<TextField
id="date"
name="date"
label="Due Date"
type="date"
margin="normal"
fullWidth
InputLabelProps={{
shrink: true,
}}
variant="outlined"
required
value={endingDate}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setEndingDate(e.target.value)
}
/>
</Modal>
);
};

View file

@ -0,0 +1,213 @@
import React, { FC, useState, FormEvent } from "react";
import { useRouteMatch } from "react-router-dom";
import {
TextField,
MenuItem,
Grid,
makeStyles,
Theme,
} from "@material-ui/core";
import { Modal } from "./Modal";
import { Ticket } from "../../types/Ticket";
import { Project } from "../../types/Project";
import { post } from "../../utils/http";
import { Constants } from "../../utils/Constants";
import Category from "../../types/enums/category";
import Impact from "../../types/enums/impact";
import Difficulty from "../../types/enums/difficulty";
interface IProps {
show: boolean;
handleClose: () => void;
allProjects: Project[];
}
const useStyles = makeStyles((theme: Theme) => ({
select: {
width: 120,
},
}));
export const NewTicketModal: FC<IProps> = ({
show,
handleClose,
allProjects,
}) => {
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
const [endingDate, setEndingDate] = useState("");
const { url } = useRouteMatch();
const id = url.split("/")[2];
const [projectId, setProjectId] = useState(id);
const [categoryID, setCategoryID] = useState(0);
const [impactID, setImpactID] = useState(0);
const [difficultyID, setDifficultyID] = useState(0);
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
let newTicket = {
title: title,
description: description,
endingDate: new Date(endingDate).toISOString(),
creatorId: "20bf4b2a-7209-4826-96cd-29c2bc937a94", // get current User id
projectId: parseInt(projectId),
impact: impactID,
difficulty: difficultyID,
category: categoryID,
};
// const response: HttpResponse<Ticket> =
await post<Ticket>(`${Constants.ticketsURI}`, newTicket);
handleClose();
};
const classes = useStyles();
return (
<Modal
name="New Ticket"
show={show}
handleClose={handleClose}
action="New Ticket"
handleAction={handleSubmit}
>
<TextField
variant="outlined"
margin="normal"
required
fullWidth
id="title"
value={title}
label="Title"
name="text"
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setTitle(e.target.value)
}
autoFocus
/>
<TextField
variant="outlined"
margin="normal"
required
fullWidth
id="description"
value={description}
label="Description"
name="text"
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
setDescription(e.target.value)
}
multiline
/>
<TextField
id="project"
name="project"
select
fullWidth
required
label="Project"
value={projectId}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
e.preventDefault();
setProjectId(e.target.value);
}}
// helperText="Please select your currency"
variant="outlined"
margin="normal"
>
{allProjects.map((p) => (
<MenuItem key={p.id} value={p.id}>
{p.title}
</MenuItem>
))}
</TextField>
<TextField
id="date"
name="date"
label="Due Date"
type="date"
margin="normal"
fullWidth
// defaultValue={new Date().toISOString()}
// className={classes.textField}
InputLabelProps={{
shrink: true,
}}
variant="outlined"
required
value={endingDate}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setEndingDate(e.target.value)
}
/>
<Grid container justify="space-between">
<TextField
id="category"
name="category"
select
label="Category"
value={categoryID}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
e.preventDefault();
setCategoryID(parseInt(e.target.value));
}}
variant="outlined"
margin="normal"
className={classes.select}
>
{Category.map((c: string, i: number) => (
<MenuItem key={i} value={i}>
{c}
</MenuItem>
))}
</TextField>
<TextField
className={classes.select}
id="impact"
name="impact"
select
label="Impact"
value={impactID}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
e.preventDefault();
setImpactID(parseInt(e.target.value));
}}
variant="outlined"
margin="normal"
>
{Impact.map((c: string, i: number) => (
<MenuItem key={i} value={i}>
{c}
</MenuItem>
))}
</TextField>
<TextField
className={classes.select}
id="difficulty"
name="difficulty"
select
label="Difficulty"
value={difficultyID}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
e.preventDefault();
setDifficultyID(parseInt(e.target.value));
}}
variant="outlined"
margin="normal"
>
{Difficulty.map((c: string, i: number) => (
<MenuItem key={i} value={i}>
{c}
</MenuItem>
))}
</TextField>
</Grid>
</Modal>
);
};

View file

@ -0,0 +1,114 @@
import React, { FC, useState, ChangeEvent, FormEvent } from "react";
import { useParams } from "react-router-dom";
import { Grid } from "@material-ui/core";
import { createStyles, makeStyles, Theme } from "@material-ui/core/styles";
import List from "@material-ui/core/List";
import ListItem from "@material-ui/core/ListItem";
import ListItemSecondaryAction from "@material-ui/core/ListItemSecondaryAction";
import ListItemText from "@material-ui/core/ListItemText";
import ListItemAvatar from "@material-ui/core/ListItemAvatar";
import Checkbox from "@material-ui/core/Checkbox";
import Avatar from "@material-ui/core/Avatar";
import { Modal } from "./Modal";
import { AvatarList } from "../Avatars/AvatarList";
import { FilterBar } from "../FilterBar";
import { User } from "../../types/User";
import { patch } from "../../utils/http";
import { Constants } from "../../utils/Constants";
interface IProps {
show: boolean;
users: User[];
allUsers: User[];
handleClose(): void;
}
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
width: "100%",
maxWidth: 360,
backgroundColor: theme.palette.background.paper,
},
})
);
export const UsersModal: FC<IProps> = ({
show,
handleClose,
users,
allUsers,
}) => {
const { id } = useParams();
const [filterText, setFilterText] = useState<string>("");
const handleChange = (e: ChangeEvent<HTMLInputElement>): void => {
setFilterText(e.target.value);
};
const memberIDs = users.map((u) => u.id);
const [members, setMembers] = useState<string[]>(memberIDs);
const handleToggle = (value: string) => () => {
const currentIndex = members.indexOf(value);
const newChecked = [...members];
if (currentIndex === -1) {
newChecked.push(value);
} else {
newChecked.splice(currentIndex, 1);
}
setMembers(newChecked);
};
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
await patch<User[]>(
`${Constants.projectsURI}/${id}/members`,
members //.map((m) => m.id)
);
handleClose();
};
const classes = useStyles();
return (
<Modal
name="Manage Users"
show={show}
handleClose={handleClose}
action="Submit"
handleAction={handleSubmit}
>
<Grid container justify="center">
<AvatarList users={users} />
<FilterBar
filterText={filterText}
clearFilterText={() => setFilterText("")}
handleChange={handleChange}
/>
</Grid>
<Grid container justify="center">
<List dense className={classes.root}>
{allUsers.map((u: User) => (
<ListItem key={u.id}>
<ListItemAvatar>
<Avatar alt={u.fullName} src={u.picture} />
</ListItemAvatar>
<ListItemText id={u.id} primary={u.fullName} />
<ListItemSecondaryAction>
<Checkbox
edge="end"
onChange={handleToggle(u.id)}
checked={members.indexOf(u.id) !== -1}
inputProps={{ "aria-labelledby": `checkbox-${u.id}` }}
/>
</ListItemSecondaryAction>
</ListItem>
))}
</List>
</Grid>
</Modal>
);
};

View file

@ -1,5 +1,5 @@
import React, { FC } from "react"; import React, { FC } from "react";
import { User } from "../types/User"; import { User } from "../../types/User";
interface IProps { interface IProps {
setMembers: React.Dispatch<React.SetStateAction<User[]>>; setMembers: React.Dispatch<React.SetStateAction<User[]>>;
@ -9,7 +9,7 @@ interface IProps {
export const UsersModalEntry: FC<IProps> = ({ user, setMembers, members }) => { export const UsersModalEntry: FC<IProps> = ({ user, setMembers, members }) => {
const match: (id: string) => boolean = (id: string) => { const match: (id: string) => boolean = (id: string) => {
return Boolean(members.find(m => m.id === id)); return Boolean(members.find((m) => m.id === id));
}; };
return ( return (
<div className="valign-wrapper"> <div className="valign-wrapper">
@ -22,7 +22,7 @@ export const UsersModalEntry: FC<IProps> = ({ user, setMembers, members }) => {
onChange={() => { onChange={() => {
!match(user.id) !match(user.id)
? setMembers([...members, user]) ? setMembers([...members, user])
: setMembers(members.filter(p => p.id !== user.id)); : setMembers(members.filter((p) => p.id !== user.id));
}} }}
/> />
<span> <span>

View file

@ -1,4 +1,5 @@
import React, { FC } from "react"; import React, { FC } from "react";
import { TextField, MenuItem } from "@material-ui/core";
import { Project } from "../types/Project"; import { Project } from "../types/Project";
interface IProps { interface IProps {
@ -22,45 +23,17 @@ export const NewTicketForm: FC<IProps> = ({
setEndingDate, setEndingDate,
allProjects, allProjects,
projectId, projectId,
setProjectId setProjectId,
}) => { }) => {
return ( return (
<> <>
<div className="row"> {/* <div className="row">
<div className="input-field">
<i className="material-icons prefix">note_add</i>
<input
id="title"
type="text"
className="validate"
value={title}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setTitle(e.target.value)
}
/>
<label htmlFor="title">Title</label>
</div>
<div className="input-field">
<i className="material-icons prefix">mode_edit</i>
<textarea
id="description"
className="materialize-textarea validate"
value={description}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
setDescription(e.target.value)
}
></textarea>
<label htmlFor="description">Description</label>
</div>
<div className="input-field"> <div className="input-field">
<i className="material-icons prefix">date_range</i> <i className="material-icons prefix">date_range</i>
<input <input
id="Due Date" id="Due Date"
type="text" type="text"
className="datepicker" className="datepicker"
value={endingDate}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setEndingDate(e.target.value) setEndingDate(e.target.value)
} }
@ -81,14 +54,14 @@ export const NewTicketForm: FC<IProps> = ({
<option value={0} disabled> <option value={0} disabled>
Project Project
</option> </option>
{allProjects.map(p => ( {allProjects.map((p) => (
<option key={p.id} value={p.id}> <option key={p.id} value={p.id}>
{p.title} {p.title}
</option> </option>
))} ))}
</select> </select>
</div> </div>
</div> </div> */}
</> </>
); );
}; };

View file

@ -1,89 +0,0 @@
import React, { FC, useState, FormEvent } from "react";
import { useRouteMatch } from "react-router-dom";
import { Modal } from "./Modal";
import { NewTicketForm } from "./NewTicketForm";
import { Ticket } from "../types/Ticket";
import { Project } from "../types/Project";
import { post } from "../utils/http";
import { Constants } from "../utils/Constants";
// import { HttpResponse } from "../types/HttpResponse";
interface IProps {
show: boolean;
handleClose(): void;
allProjects: Project[];
}
export const NewTicketModal: FC<IProps> = ({
show,
handleClose,
allProjects
}) => {
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
const [endingDate, setEndingDate] = useState("");
const { url } = useRouteMatch();
const id = url.split("/")[2];
const [projectId, setProjectId] = useState(id);
const handleSubmit: (event: FormEvent<HTMLFormElement>) => void = async (
e: FormEvent
) => {
e.preventDefault();
let newTicket = {
title: title,
description: description,
endingDate: new Date(endingDate).toISOString(),
creatorId: "20bf4b2a-7209-4826-96cd-29c2bc937a94",
projectId: parseInt(projectId)
};
// const response: HttpResponse<Ticket> =
await post<Ticket>(`${Constants.ticketsURI}`, newTicket);
handleClose();
};
return (
<Modal show={show} handleClose={handleClose}>
<div className="row valign-wrapper indigo">
<div className="col s10">
<h4 className="white-text">New Ticket</h4>
</div>
<div className="col s2">
<i
className="right material-icons indigo lighten-3 circle"
onClick={handleClose}
>
close
</i>
</div>
</div>
<form onSubmit={handleSubmit}>
<div className="row">
<NewTicketForm
title={title}
setTitle={setTitle}
description={description}
setDescription={setDescription}
endingDate={endingDate}
setEndingDate={setEndingDate}
allProjects={allProjects}
projectId={projectId}
setProjectId={setProjectId}
/>
</div>
<div className="modal-footer grey lighten-3">
<input
type="submit"
className="modal-close waves-effect waves-green btn indigo"
value="Create Task"
/>
</div>
</form>
</Modal>
);
};

View file

@ -1,41 +0,0 @@
import React, { FC } from "react";
import { useRouteMatch } from "react-router-dom";
import { TabRouterHeader } from "./TabRouterHeader";
interface IProps {
tabNames: string[];
description: string;
setDescription: React.Dispatch<React.SetStateAction<string>>;
title: string;
setTitle: React.Dispatch<React.SetStateAction<string>>;
endingDate: string;
setEndingDate: React.Dispatch<React.SetStateAction<string>>;
}
export const NewTicketTabRouter: FC<IProps> = ({
tabNames,
description,
setDescription,
title,
setTitle,
endingDate,
setEndingDate
}) => {
const { url } = useRouteMatch();
return (
<>
<div className="row">
<TabRouterHeader tabNames={tabNames} />
{/* <NewTicketForm
title={title}
setTitle={setTitle}
description={description}
setDescription={setDescription}
endingDate={endingDate}
setEndingDate={setEndingDate}
/> */}
</div>
</>
);
};

View file

@ -0,0 +1,118 @@
import React, { FC, useState, ReactNode } from "react";
import { makeStyles, Theme, useTheme } from "@material-ui/core/styles";
import AppBar from "@material-ui/core/AppBar";
import Tabs from "@material-ui/core/Tabs";
import Tab from "@material-ui/core/Tab";
import Typography from "@material-ui/core/Typography";
import Box from "@material-ui/core/Box";
import SwipeableViews from "react-swipeable-views";
import { Ticket } from "../../types/Ticket";
import { Project } from "../../types/Project";
import { TicketList } from "../Lists/TicketList";
// import { FileList } from "./AppFileList";
import { AppFile } from "../../types/AppFile";
interface TabProps {
children?: ReactNode;
dir?: string;
index: any;
value: any;
}
const TabPanel: FC<TabProps> = (props: TabProps) => {
const { children, value, index, ...other } = props;
return (
<Typography
component="div"
role="tabpanel"
hidden={value !== index}
id={`full-width-tabpanel-${index}`}
aria-labelledby={`full-width-tab-${index}`}
{...other}
>
{value === index && <Box p={3}>{children}</Box>}
</Typography>
);
};
const a11yProps = (index: any) => {
return {
id: `full-width-tab-${index}`,
"aria-controls": `full-width-tabpanel-${index}`,
};
};
const useStyles = makeStyles((theme: Theme) => ({
root: {
backgroundColor: "#E9ECEF",
borderRadius: "20px",
},
}));
interface IProps {
tickets: Ticket[];
remainingDays?: number;
tabNames: string[];
files: AppFile[];
// activities: Activity[];
allProjects: Project[];
}
export const ProjectTabPanel: FC<IProps> = ({
tickets,
tabNames,
files,
allProjects,
}) => {
const classes = useStyles();
const theme = useTheme();
const [value, setValue] = useState(0);
const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => {
setValue(newValue);
};
const handleChangeIndex = (index: number) => {
setValue(index);
};
return (
<div className={classes.root}>
<AppBar
position="static"
color="inherit"
style={{ borderTopLeftRadius: "10px", borderTopRightRadius: "10px" }}
>
<Tabs
value={value}
onChange={handleChange}
indicatorColor="primary"
textColor="primary"
variant="fullWidth"
aria-label="full width tabs"
>
{tabNames.map((t: string, i: number) => (
<Tab key={i} label={t} {...a11yProps({ i })} />
))}
</Tabs>
</AppBar>
<SwipeableViews
axis={theme.direction === "rtl" ? "x-reverse" : "x"}
index={value}
onChangeIndex={handleChangeIndex}
>
<TabPanel value={value} index={0} dir={theme.direction}>
<TicketList
tickets={tickets}
allProjects={allProjects}
addButton={true}
/>
</TabPanel>
{/* <TabPanel value={value} index={1} dir={theme.direction}>
<FileList files={files} />
</TabPanel> */}
</SwipeableViews>
</div>
);
};

View file

@ -1,13 +1,12 @@
import React, { FC } from "react"; import React, { FC } from "react";
import { Route, useRouteMatch, Redirect } from "react-router-dom"; import { Route, useRouteMatch, Redirect } from "react-router-dom";
import { TabRouterHeader } from "./TabRouterHeader"; import { TabRouterHeader } from "./TabRouterHeader";
import { TicketList } from "./TicketList"; import { TicketList } from "../Lists/TicketList";
import { FileList } from "./AppFileList"; import { FileList } from "../Lists/AppFileList";
// import { ActivityList } from "./ActivityList"; import { Ticket } from "../../types/Ticket";
import { Ticket } from "../types/Ticket"; import { AppFile } from "../../types/AppFile";
import { AppFile } from "../types/AppFile"; import { Activity } from "../../types/Activity";
import { Activity } from "../types/Activity"; import { Project } from "../../types/Project";
import { Project } from "../types/Project";
interface IProps { interface IProps {
tickets: Ticket[]; tickets: Ticket[];
@ -23,7 +22,7 @@ export const TabRouter: FC<IProps> = ({
tabNames, tabNames,
files, files,
activities, activities,
allProjects allProjects,
}) => { }) => {
const { url } = useRouteMatch(); const { url } = useRouteMatch();

View file

@ -0,0 +1,109 @@
import React, { FC, useState, ReactNode } from "react";
import SwipeableViews from "react-swipeable-views";
import { makeStyles, Theme, useTheme } from "@material-ui/core/styles";
import AppBar from "@material-ui/core/AppBar";
import Tabs from "@material-ui/core/Tabs";
import Tab from "@material-ui/core/Tab";
import Typography from "@material-ui/core/Typography";
import Box from "@material-ui/core/Box";
import { Ticket } from "../../types/Ticket";
import { Project } from "../../types/Project";
import { ProjectList } from "../Lists/ProjectList";
import { TicketList } from "../Lists/TicketList";
import { User } from "../../types/User";
interface TabProps {
children?: ReactNode;
dir?: string;
index: any;
value: any;
}
const TabPanel: FC<TabProps> = (props: TabProps) => {
const { children, value, index, ...other } = props;
return (
<Typography
component="div"
role="tabpanel"
hidden={value !== index}
id={`full-width-tabpanel-${index}`}
aria-labelledby={`full-width-tab-${index}`}
{...other}
>
{value === index && <Box p={3}>{children}</Box>}
</Typography>
);
};
const a11yProps = (index: any) => {
return {
id: `full-width-tab-${index}`,
"aria-controls": `full-width-tabpanel-${index}`,
};
};
const useStyles = makeStyles((theme: Theme) => ({
root: {
backgroundColor: "#E9ECEF",
borderRadius: "20px",
},
topbar: { borderTopLeftRadius: "10px", borderTopRightRadius: "10px" },
}));
interface IProps {
tabNames: string[];
tickets: Ticket[];
projects: Project[];
allUsers: User[];
}
export const UserTabPanel: FC<IProps> = ({
tickets,
tabNames,
projects,
allUsers,
}) => {
const classes = useStyles();
const theme = useTheme();
const [value, setValue] = useState(0);
const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => {
setValue(newValue);
};
const handleChangeIndex = (index: number) => {
setValue(index);
};
return (
<div className={classes.root}>
<AppBar position="static" color="inherit" className={classes.topbar}>
<Tabs
value={value}
onChange={handleChange}
indicatorColor="primary"
textColor="primary"
variant="fullWidth"
aria-label="full width tabs"
>
{tabNames.map((t: string, i: number) => (
<Tab key={i} label={t} {...a11yProps({ i })} />
))}
</Tabs>
</AppBar>
<SwipeableViews
axis={theme.direction === "rtl" ? "x-reverse" : "x"}
index={value}
onChangeIndex={handleChangeIndex}
>
<TabPanel value={value} index={0} dir={theme.direction}>
<ProjectList projects={projects} allUsers={allUsers} />
</TabPanel>
<TabPanel value={value} index={1} dir={theme.direction}>
<TicketList tickets={tickets} allProjects={[]} addButton={false} />
</TabPanel>
</SwipeableViews>
</div>
);
};

View file

@ -1,10 +1,10 @@
import React, { FC } from "react"; import React, { FC } from "react";
import { Route, useRouteMatch, Redirect } from "react-router-dom"; import { Route, useRouteMatch, Redirect } from "react-router-dom";
import { TabRouterHeader } from "./TabRouterHeader"; import { TabRouterHeader } from "./TabRouterHeader";
import { ProjectList } from "./ProjectList"; import { ProjectList } from "../Lists/ProjectList";
import { Ticket } from "../types/Ticket"; import { Ticket } from "../../types/Ticket";
import { Project } from "../types/Project"; import { Project } from "../../types/Project";
import { TicketList } from "./TicketList"; import { TicketList } from "../Lists/TicketList";
interface IProps { interface IProps {
tabNames: string[]; tabNames: string[];
@ -23,7 +23,7 @@ export const UserTabRouter: FC<IProps> = ({ tickets, tabNames, projects }) => {
<Redirect from={url} to={`${url}/projects`} /> <Redirect from={url} to={`${url}/projects`} />
<Route path={`${url}/projects`}> <Route path={`${url}/projects`}>
<ProjectList projects={projects} /> {/* <ProjectList projects={projects} /> */}
</Route> </Route>
<Route path={`${url}/tickets`}> <Route path={`${url}/tickets`}>

View file

@ -1,6 +1,5 @@
import React, { FC } from "react"; import React, { FC } from "react";
import { HorizontalCard } from "./HorizontalCard"; import { UserAvatar } from "./Avatars/UserAvatar";
import { Avatar } from "./Avatar";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
export const ProfileSelector: FC = () => { export const ProfileSelector: FC = () => {
@ -10,7 +9,10 @@ export const ProfileSelector: FC = () => {
<div className="center "> <div className="center ">
<h4>Select a profile</h4> <h4>Select a profile</h4>
<Link to="/users/cd179eb7-3a54-4060-b22c-3e947bdffcbc"> <Link to="/users/cd179eb7-3a54-4060-b22c-3e947bdffcbc">
<Avatar picture="https://vignette.wikia.nocookie.net/jamescameronsavatar/images/0/08/Neytiri_Profilbild.jpg/revision/latest/scale-to-width-down/250?cb=20100107164021&path-prefix=de" /> <UserAvatar
alt=""
picture="https://vignette.wikia.nocookie.net/jamescameronsavatar/images/0/08/Neytiri_Profilbild.jpg/revision/latest/scale-to-width-down/250?cb=20100107164021&path-prefix=de"
/>
</Link> </Link>
<h5>Demo User</h5> <h5>Demo User</h5>
</div> </div>

View file

@ -0,0 +1,45 @@
import React, { FC } from "react";
import { makeStyles, Theme, createStyles } from "@material-ui/core/styles";
import LinearProgress from "@material-ui/core/LinearProgress";
import { Box } from "@material-ui/core";
type IProps = {
value: number;
};
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
width: "100%",
"& > * + *": {
marginTop: theme.spacing(2),
},
},
})
);
export const ProgressBar: FC<IProps> = ({ value }) => {
// const styleString: CSSProperties = { width: `${value}%` };
// let barColor: string = "green";
// if (value < 100) {
// barColor = "yellow";
// }
// if (value < 200 / 3) {
// barColor = "orange";
// }
// if (value < 100 / 3) {
// barColor = "red";
// }
const classes = useStyles();
return (
<Box className="row">
<div className={classes.root}>
<LinearProgress variant="determinate" value={value} />
{/* <LinearProgress variant="determinate" value={value} color={barColor} /> */}
</div>
</Box>
);
};

View file

@ -0,0 +1,41 @@
import React, { FC } from "react";
// import { makeStyles, Theme, createStyles } from "@material-ui/core/styles";
import { Box } from "@material-ui/core";
import { PlaylistAddCheck } from "@material-ui/icons";
type IProps = {
tasksTotalCount?: number;
tasksDone?: number;
remainingDays?: number;
};
// const useStyles = makeStyles((theme: Theme) =>
// createStyles({
// root: {
// width: "100%",
// "& > * + *": {
// marginTop: theme.spacing(2),
// },
// },
// })
// );
export const ProgressInfo: FC<IProps> = ({
tasksDone,
tasksTotalCount,
remainingDays,
}) => {
// const classes = useStyles();
return (
<Box>
<PlaylistAddCheck />
<span>
{tasksDone}/{tasksTotalCount}
</span>
<Box className="right">
<span>Due in {remainingDays} days</span>
</Box>
</Box>
);
};

View file

@ -1,49 +0,0 @@
import React, { FC, CSSProperties } from "react";
type ProgressBarProps = {
value: number;
max?: number;
tasksTotalCount?: number;
tasksDone?: number;
remainingDays?: number;
};
export const ProgressBar: FC<ProgressBarProps> = ({
value,
max = 100,
tasksDone,
tasksTotalCount,
remainingDays
}) => {
const styleString: CSSProperties = { width: `${value}%` };
let barColor: string = "green";
if (value < 100) {
barColor = "yellow";
}
if (value < 200 / 3) {
barColor = "orange";
}
if (value < 100 / 3) {
barColor = "red";
}
return (
<>
<div className="row">
<div className="progress">
<div className={`determinate ${barColor}`} style={styleString}></div>
</div>
<div>
<i className="left material-icons">playlist_add_check</i>
<span>
{tasksDone}/{tasksTotalCount}
</span>
<div className="right">
<span>Due {remainingDays} days</span>
</div>
</div>
</div>
</>
);
};

View file

@ -1,66 +0,0 @@
import React, { FC, useState, ChangeEvent, MouseEvent } from "react";
import { Ticket } from "../types/Ticket";
import { HorizontalCard } from "./HorizontalCard";
import { FilterBar } from "./FilterBar";
import { put } from "../utils/http";
import { Constants } from "../utils/Constants";
import { HttpResponse } from "../types/HttpResponse";
import { Project } from "../types/Project";
type IProps = {
projects: Project[];
};
export const ProjectList: FC<IProps> = ({ projects }) => {
const [filterText, setFilterText] = useState<string>("");
const clearFilterText: (e: MouseEvent) => void = (e: MouseEvent) => {
setFilterText("");
};
const handleChange: (e: ChangeEvent<HTMLInputElement>) => void = (
e: ChangeEvent<HTMLInputElement>
) => {
setFilterText(e.target.value);
};
let filteredTickets = projects.filter(
t =>
t.status !== "Done" &&
t.title.toLowerCase().includes(filterText.toLowerCase())
);
return (
<>
<div className="row valign-wrapper">
<h3>Projects</h3>
<FilterBar
filterText={filterText}
handleChange={handleChange}
clearFilterText={clearFilterText}
/>
</div>
<div className="col s12 grey lighten-1">
<ul>
{filteredTickets.length === 0 ? (
<HorizontalCard />
) : (
filteredTickets.map((t: Project) => (
<HorizontalCard
key={t.id}
title={t.title}
remainingDays={t.endingDate}
link={`/projects/${t.id}`}
validateTicket={async (e: MouseEvent) => {
e.preventDefault();
await put<HttpResponse<Ticket>>(
`${Constants.ticketsURI}/${t.id}/closed`,
{}
);
}}
/>
))
)}
</ul>
</div>
</>
);
};

View file

@ -0,0 +1,118 @@
import React from "react";
import Avatar from "@material-ui/core/Avatar";
import Button from "@material-ui/core/Button";
import CssBaseline from "@material-ui/core/CssBaseline";
import TextField from "@material-ui/core/TextField";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import Checkbox from "@material-ui/core/Checkbox";
import Link from "@material-ui/core/Link";
import Paper from "@material-ui/core/Paper";
import Grid from "@material-ui/core/Grid";
import LockOutlinedIcon from "@material-ui/icons/LockOutlined";
import Typography from "@material-ui/core/Typography";
import { makeStyles, createStyles, Theme } from "@material-ui/core/styles";
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
height: "100vh"
},
image: {
backgroundImage: "url(https://source.unsplash.com/daily?dev)",
backgroundRepeat: "no-repeat",
backgroundColor:
theme.palette.type === "light"
? theme.palette.grey[50]
: theme.palette.grey[900],
backgroundSize: "cover",
backgroundPosition: "center"
},
paper: {
margin: theme.spacing(8, 4),
display: "flex",
flexDirection: "column",
alignItems: "center"
},
avatar: {
margin: theme.spacing(1),
backgroundColor: theme.palette.secondary.main
},
form: {
width: "100%",
marginTop: theme.spacing(1)
},
submit: {
margin: theme.spacing(3, 0, 2)
}
})
);
export default function SignInSide() {
const classes = useStyles();
return (
<Grid container component="main" className={classes.root}>
<CssBaseline />
<Grid item xs={false} sm={4} md={7} className={classes.image} />
<Grid item xs={12} sm={8} md={5} component={Paper} elevation={6} square>
<div className={classes.paper}>
<Avatar className={classes.avatar}>
<LockOutlinedIcon />
</Avatar>
<Typography component="h1" variant="h5">
Sign in
</Typography>
<form className={classes.form} noValidate>
<TextField
variant="outlined"
margin="normal"
required
fullWidth
id="email"
label="Email Address"
name="email"
autoComplete="email"
autoFocus
/>
<TextField
variant="outlined"
margin="normal"
required
fullWidth
name="password"
label="Password"
type="password"
id="password"
autoComplete="current-password"
/>
<FormControlLabel
control={<Checkbox value="remember" color="primary" />}
label="Remember me"
/>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
className={classes.submit}
>
Sign In
</Button>
<Grid container>
<Grid item xs>
<Link href="#" variant="body2">
Forgot password?
</Link>
</Grid>
<Grid item>
<Link href="#" variant="body2">
{"Don't have an account? Sign Up"}
</Link>
</Grid>
</Grid>
</form>
</div>
</Grid>
</Grid>
);
}

View file

@ -1,93 +0,0 @@
import React, { FC, useState, ChangeEvent, MouseEvent } from "react";
import { Ticket } from "../types/Ticket";
import { FloatingButton } from "./FloatingButton";
import { HorizontalCard } from "./HorizontalCard";
import { FilterBar } from "./FilterBar";
import { HttpResponse } from "../types/HttpResponse";
import { put } from "../utils/http";
import { Constants } from "../utils/Constants";
import { NewTicketModal } from "./NewTicketModal";
import { Project } from "../types/Project";
type TicketListProps = {
tickets: Ticket[];
allProjects: Project[];
addButton?: Boolean;
};
export const TicketList: FC<TicketListProps> = ({
tickets,
allProjects,
addButton = true
}) => {
const [filterText, setFilterText] = useState<string>("");
const clearFilterText: (e: MouseEvent) => void = (e: MouseEvent) => {
setFilterText("");
};
const onClick: (e: MouseEvent) => void = (e: MouseEvent) => {
e.preventDefault();
setShowNew(true);
};
const handleChange: (e: ChangeEvent<HTMLInputElement>) => void = (
e: ChangeEvent<HTMLInputElement>
) => {
setFilterText(e.target.value);
};
const [showNew, setShowNew] = useState(false);
let filteredTickets = tickets.filter(
t =>
t.status !== "Done" &&
t.title.toLowerCase().includes(filterText.toLowerCase())
);
return (
<>
<div className="row valign-wrapper">
<NewTicketModal
handleClose={() => {
setShowNew(false);
}}
show={showNew}
allProjects={allProjects}
/>
<h3>Tickets</h3>
{addButton && (
<FloatingButton
color="indigo lighten-3"
size="small"
onClick={onClick}
/>
)}
<FilterBar
filterText={filterText}
handleChange={handleChange}
clearFilterText={clearFilterText}
/>
</div>
<div className="col s12 grey lighten-1">
<ul>
{filteredTickets.length === 0 ? (
<HorizontalCard />
) : (
filteredTickets.map((t: Ticket) => (
<HorizontalCard
key={t.id}
title={t.title}
remainingDays={t.endingDate}
link={`/tickets/${t.id}`}
validateTicket={async (e: MouseEvent) => {
e.preventDefault();
await put<HttpResponse<Ticket>>(
`${Constants.ticketsURI}/${t.id}/closed`,
{}
);
}}
/>
))
)}
</ul>
</div>
</>
);
};

View file

@ -1,21 +1,36 @@
import React, { FC } from "react"; import React, { FC } from "react";
import { Header } from "../components/Header"; import { Header } from "../components/Header";
import { Avatar } from "../components/Avatar"; import { UserAvatar } from "./Avatars/UserAvatar";
import {
Grid,
// makeStyles, Theme
} from "@material-ui/core";
interface IProps { interface IProps {
fullName: string; fullName: string;
presentation: string; presentation: string;
picture: string; picture: string;
} }
// const useStyles = makeStyles((theme: Theme) => ({
// root: {
// paddingTop: theme.spacing(2),
// flexGrow: 1,
// },
// }));
export const UserHeader: FC<IProps> = ({ fullName, presentation, picture }) => { export const UserHeader: FC<IProps> = ({ fullName, presentation, picture }) => {
// const classes = useStyles();
return ( return (
<div className="row valign-wrapper"> // <div className={classes.root}>
<div className="col s2"> <Grid container>
<Avatar picture={picture} /> <Grid item xs={2}>
</div> <UserAvatar picture={picture} alt="" />
<div className="col s10"> </Grid>
<Grid item xs>
<Header title={fullName} description={presentation} /> <Header title={fullName} description={presentation} />
</div> </Grid>
</div> </Grid>
// </div>
); );
}; };

View file

@ -14,6 +14,7 @@ export const UserController: FC = () => {
const [user, setUser] = useState<User>({} as User); const [user, setUser] = useState<User>({} as User);
const [hasError, setHasError] = useState(false); const [hasError, setHasError] = useState(false);
const [error, setError] = useState(""); const [error, setError] = useState("");
const [allUsers, setAllUsers] = useState<User[]>([]);
const { id } = useParams(); const { id } = useParams();
async function httpGetUser(id: string): Promise<void> { async function httpGetUser(id: string): Promise<void> {
@ -32,9 +33,24 @@ export const UserController: FC = () => {
} }
} }
async function httpGetAllUsers(): Promise<void> {
try {
const response: HttpResponse<User> = await get<User>(
`${Constants.usersURI}`
);
if (response.parsedBody !== undefined) {
setAllUsers((response.parsedBody as unknown) as User[]);
}
} catch (ex) {
setHasError(true);
setError(ex);
}
}
useEffect(() => { useEffect(() => {
if (id !== undefined) { if (id !== undefined) {
httpGetUser(id); httpGetUser(id);
httpGetAllUsers();
} else { } else {
setHasError(true); setHasError(true);
setError("Bad Request"); setError("Bad Request");
@ -45,6 +61,6 @@ export const UserController: FC = () => {
return <ErrorController error={error} />; return <ErrorController error={error} />;
} }
const viewModel = new UserVM(user); const viewModel = new UserVM(user, allUsers);
return isLoading ? <Preloader /> : <UserPage viewModel={viewModel} />; return isLoading ? <Preloader /> : <UserPage viewModel={viewModel} />;
}; };

View file

@ -0,0 +1,41 @@
import React, { FC } from "react";
import CssBaseline from "@material-ui/core/CssBaseline";
import { makeStyles } from "@material-ui/core/styles";
import ButtonAppBar from "../components/ButtonAppBar";
import Footer from "../components/Footer";
/**
* @function useStyles creates the css styles used in the following component.
*/
const useStyles = makeStyles((theme) => ({
// root style allow for fixed footer
root: {
display: "flex",
flexDirection: "column",
minHeight: "100vh",
backgroundColor: "#f8f9fa",
},
}));
/**
* MainLayout is the principal page layout. It mainly ensure the footer is fixed
* to the page bottom.
*
* @param children - The encapsulated component.
*/
const MainLayout: FC = ({ children }) => {
const classes = useStyles();
return (
<div className={classes.root}>
<header>
<ButtonAppBar />
</header>
{/* <BreadCrumb /> */}
<CssBaseline />
{children}
<Footer />
</div>
);
};
export default MainLayout;

View file

@ -0,0 +1,43 @@
import React, { FC, ReactNode } from "react";
import { makeStyles, Theme, Container } from "@material-ui/core";
/**
* @function useStyles creates the css styles used in the following component.
*/
const useStyles = makeStyles((theme: Theme) => ({
// root style allow for fixed footer
header: {
margin: theme.spacing(1),
paddingTop: theme.spacing(2),
paddingBottom: theme.spacing(2),
flexGrow: 1,
},
content: {
margin: theme.spacing(1),
marginTop: theme.spacing(2),
marginBottom: theme.spacing(2),
},
}));
interface IProps {
header: ReactNode;
content: ReactNode;
}
/**
* PageLayout divide the page in 2 parts: Header and Content, to ensure cohesion.
*
* @param Header - The encapsulated component.
* @param Content - The encapsulated component.
*/
const PageLayout: FC<IProps> = ({ header, content }) => {
const classes = useStyles();
return (
<Container maxWidth="md">
<div className={classes.header}>{header}</div>
<div className={classes.content}>{content}</div>
</Container>
);
};
export default PageLayout;

View file

@ -1,21 +1,23 @@
import React from "react"; import React from "react";
import { LogInForm } from "../components/LogInForm"; // import { LogInForm } from "../components/LogInForm";
import { ProfileSelector } from "../components/ProfileSelector"; // import { ProfileSelector } from "../components/ProfileSelector";
import SignInSide from "../components/SignInSide";
export const HomePage: React.FC = () => { export const HomePage: React.FC = () => {
return ( return (
<div className="section"> // <div className="section">
<div className="container center"> // <div className="container center">
<h1 className="center">Ticket Manager</h1> // <h1 className="center">Ticket Manager</h1>
<div className="row"> // <div className="row">
<div className="col s6"> // <div className="col s6">
<ProfileSelector /> // <ProfileSelector />
</div> // </div>
<div className="col s6"> // <div className="col s6">
<LogInForm /> // <LogInForm />
</div> // </div>
</div> // </div>
</div> // </div>
</div> // </div>
<SignInSide />
); );
}; };

View file

@ -1,18 +0,0 @@
import React, { FC } from "react";
import { AppRouter } from "../utils/router";
import { NavBar } from "../components/Navbar";
const Layout: FC = () => {
return (
<>
<header>
<NavBar />
</header>
{/* <BreadCrumb /> */}
<AppRouter />
{/* <Footer /> */}
</>
);
};
export default Layout;

View file

@ -1,10 +1,13 @@
import React, { FC } from "react"; import React, { FC } from "react";
import PageLayout from "../layouts/PageLayout";
import { Header } from "../components/Header";
interface IProps {} interface IProps {}
export const NotFoundPage: FC<IProps> = () => { export const NotFoundPage: FC<IProps> = () => {
return ( return (
<div className="section"> <PageLayout
<p>error</p> header={<Header title="Error page" description="Something went wrong" />}
</div> content={<p>error</p>}
/>
); );
}; };

View file

@ -1,16 +1,27 @@
import React, { FC, useState } from "react"; import React, { FC, useState } from "react";
import ProjectVM from "../VM/ProjectVM"; import { Grid, makeStyles, Theme } from "@material-ui/core";
import { Header } from "../components/Header"; import { Header } from "../components/Header";
import { AvatarList } from "../components/AvatarList"; import { AvatarList } from "../components/Avatars/AvatarList";
import { ProgressBar } from "../components/ProgressBar"; import { ProgressBar } from "../components/Progress/ProgressBar";
import { TabRouter } from "../components/TabRouter"; import { FloatingButton } from "../components/Buttons/FloatingButton";
import { FloatingButton } from "../components/FloatingButton"; import { UsersModal } from "../components/Modals/UsersModal";
import { UsersModal } from "../components/UsersModal"; import { ProjectTabPanel } from "../components/Panels/ProjectTabPanel";
import ProjectVM from "../VM/ProjectVM";
import PageLayout from "../layouts/PageLayout";
import { ProgressInfo } from "../components/Progress/ProgressInfo";
interface IProps { interface IProps {
viewModel: ProjectVM; viewModel: ProjectVM;
} }
const useStyles = makeStyles((theme: Theme) => ({
root: {
marginTop: theme.spacing(4),
marginBottom: theme.spacing(4),
flexGrow: 1,
},
}));
export const ProjectPage: FC<IProps> = ({ viewModel }) => { export const ProjectPage: FC<IProps> = ({ viewModel }) => {
const { const {
// id, // id,
@ -24,46 +35,65 @@ export const ProjectPage: FC<IProps> = ({ viewModel }) => {
ticketsTotalCount, ticketsTotalCount,
remainingDays, remainingDays,
files, files,
activities, // activities,
allProjects allProjects,
} = viewModel; } = viewModel;
const tabNames: string[] = ["Tickets", "Files"]; //, "Activity"]; const tabNames: string[] = ["Tickets"]; //, "Files", "Activity"];
const [showModal, setShowModal] = useState<boolean>(false); const [showModal, setShowModal] = useState<boolean>(false);
return ( const classes = useStyles();
<div className="section">
<div className="container"> const Content: FC = () => {
<Header title={title} description={description} /> return (
<div className="row valign-wrapper"> <>
<AvatarList users={users} /> <UsersModal
<FloatingButton show={showModal}
icon="add" users={users}
color="indigo lighten-3" allUsers={allUsers}
size="small" handleClose={() => setShowModal(false)}
onClick={() => setShowModal(true)} />
/>
<UsersModal <Grid container>
show={showModal} <Grid
users={users} container
allUsers={allUsers} direction="row"
handleClose={() => setShowModal(false)} justify="flex-start"
alignItems="center"
>
<AvatarList users={users} />
<FloatingButton
icon="add"
color="default"
size="small"
onClick={() => setShowModal(true)}
/>
</Grid>
</Grid>
<div className={classes.root}>
<ProgressBar value={progression} />
<ProgressInfo
tasksDone={ticketsDone}
tasksTotalCount={ticketsTotalCount}
remainingDays={remainingDays}
/> />
</div> </div>
<ProgressBar <ProjectTabPanel
value={progression}
tasksDone={ticketsDone}
tasksTotalCount={ticketsTotalCount}
remainingDays={remainingDays}
/>
<TabRouter
tabNames={tabNames} tabNames={tabNames}
tickets={tickets} tickets={tickets}
files={files} files={files}
activities={activities} // activities={activities}
allProjects={allProjects} allProjects={allProjects}
/> />
</div> </>
</div> );
};
return (
<PageLayout
header={<Header title={title} description={description} />}
content={<Content />}
/>
); );
}; };

View file

@ -1,14 +1,33 @@
import React, { FC } from "react"; import React, { FC } from "react";
import { Header } from "../components/Header";
import { AvatarList } from "../components/AvatarList";
import { TicketVM } from "../VM/TicketVM";
import { getRemainingdays } from "../utils/methods";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { makeStyles, Theme, Grid, Typography } from "@material-ui/core";
import { Timer } from "@material-ui/icons";
import PageLayout from "../layouts/PageLayout";
import { TicketVM } from "../VM/TicketVM";
import { Header } from "../components/Header";
import { AvatarList } from "../components/Avatars/AvatarList";
import TicketChipsArray from "../components/Cards/TicketChipsArray";
import { getRemainingdays } from "../utils/methods";
interface IProps { interface IProps {
viewModel: TicketVM; viewModel: TicketVM;
} }
const useStyles = makeStyles((theme: Theme) => ({
root: {
margin: theme.spacing(1),
// flexGrow: 1,
},
table: {
margin: "auto",
maxWidth: 650,
alignItems: "center",
},
subtitle: {
marginTop: 20,
},
}));
export const TicketPage: FC<IProps> = ({ viewModel }) => { export const TicketPage: FC<IProps> = ({ viewModel }) => {
const { const {
title, title,
@ -19,61 +38,98 @@ export const TicketPage: FC<IProps> = ({ viewModel }) => {
status, status,
category, category,
impact, impact,
difficulty difficulty,
} = viewModel; } = viewModel;
const daysToEnd: number = getRemainingdays(endingDate); const daysToEnd: number = getRemainingdays(endingDate);
// let notes: string = ""; // let notes: string = "";
const classes = useStyles();
return ( const Content: FC = () => {
<div className="section"> return (
<div className="container"> <>
<Header title={title} description={description} />
<AvatarList users={users} /> <AvatarList users={users} />
<div className="row section"> <div className={classes.root}>
<div className="col s9"> <Grid container>
<h5> <Grid item xs={9}>
<b>In project: </b>{" "} <Typography
<Link to={`/projects/${project.id}`}>{project.title}</Link> variant="h5"
</h5> component="h5"
</div> className={classes.subtitle}
<div className="col s3"> >
<i className="left material-icons">timer</i> <b>Project: </b>
<span>Due in {daysToEnd} days</span> <Link to={`/projects/${project.id}`}>{project.title}</Link>
</div> </Typography>
</Grid>
<Grid item xs>
<Timer /> <span>Due in {daysToEnd} days</span>
</Grid>
</Grid>
</div> </div>
<div className="section white center"> <div className={classes.table}>
<div className="chip"> {/* <InfoTable
<span className="indigo-text">Status: </span> {status} status={status}
{/* <i className="close material-icons">close</i> */} category={category}
</div> impact={impact}
difficulty={difficulty}
<div className="chip"> /> */}
<span className="orange-text">Category: </span> {category} <TicketChipsArray
{/* <i className="close material-icons">close</i> */} status={status}
</div> category={category}
impact={impact}
<div className="chip"> difficulty={difficulty}
<span className="green-text">Impact: </span> {impact} />
{/* <i className="close material-icons">close</i> */}
</div>
<div className="chip">
<span className="red-text">Difficulty: </span> {difficulty}
{/* <i className="close material-icons">close</i> */}
</div>
{/* <textarea {/* <textarea
id="notes" id="notes"
className="materialize-textarea validate" className="materialize-textarea validate"
value={notes} value={notes}
// onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => // onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
// setDescription(e.target.value) // setDescription(e.target.value)
// } // }
></textarea> */} ></textarea> */}
</div> </div>
</div> </>
</div> );
};
return (
<PageLayout
header={<Header title={title} description={description} />}
content={<Content />}
/>
); );
}; };
// interface InfoProps {
// status: string;
// category: string;
// impact: string;
// difficulty: string;
// }
// const InfoTable: FC<InfoProps> = (info: InfoProps) => {
// const classes = useStyles();
// return (
// <TableContainer component={Paper}>
// <Table className={classes.table} aria-label="simple table">
// <TableHead>
// <TableRow>
// <TableCell>Status</TableCell>
// <TableCell>Category</TableCell>
// <TableCell>Impact</TableCell>
// <TableCell>Difficulty</TableCell>
// </TableRow>
// </TableHead>
// <TableBody>
// <TableRow>
// <TableCell>{info.status}</TableCell>
// <TableCell>{info.category}</TableCell>
// <TableCell>{info.impact}</TableCell>
// <TableCell>{info.difficulty}</TableCell>
// </TableRow>
// </TableBody>
// </Table>
// </TableContainer>
// );
// };

View file

@ -1,28 +1,41 @@
import React, { FC } from "react"; import React, { FC } from "react";
import { UserVM } from "../VM/UserVM"; import { UserVM } from "../VM/UserVM";
import { UserHeader } from "../components/UserHeader"; import { UserHeader } from "../components/UserHeader";
import { UserTabRouter } from "../components/UserTabRouter"; import { UserTabPanel } from "../components/Panels/UserTabPanel";
import PageLayout from "../layouts/PageLayout";
interface IProps { interface IProps {
viewModel: UserVM; viewModel: UserVM;
} }
export const UserPage: FC<IProps> = ({ viewModel }) => { export const UserPage: FC<IProps> = ({ viewModel }) => {
const { fullName, presentation, picture, projects, tickets } = viewModel; const {
fullName,
presentation,
picture,
projects,
tickets,
allUsers,
} = viewModel;
const tabNames: string[] = ["Projects", "Tickets"]; const tabNames: string[] = ["Projects", "Tickets"];
return ( return (
<div className="section"> <PageLayout
<div className="container"> header={
<UserHeader <UserHeader
picture={picture} picture={picture}
fullName={fullName} fullName={fullName}
presentation={presentation} presentation={presentation}
/> />
<UserTabRouter }
content={
<UserTabPanel
tabNames={tabNames} tabNames={tabNames}
projects={projects} projects={projects}
tickets={tickets} tickets={tickets}
allUsers={allUsers}
/> />
</div> }
</div> />
); );
}; };

View file

@ -0,0 +1,3 @@
const Category: string[] = ["Product", "Tech", "Design", "Marketing", "Test"];
export default Category;

View file

@ -0,0 +1,3 @@
const Difficulty: string[] = ["Easy", "Medium", "Hard"];
export default Difficulty;

View file

@ -0,0 +1,3 @@
const Impact: string[] = ["High", "Medium", "Low"];
export default Impact;

View file

@ -0,0 +1,24 @@
import React, { useEffect } from "react";
import { Route } from "react-router-dom";
import { useAuth0 } from "../authentication/auth0";
export const PrivateRoute = ({ component: Component, path, ...rest }) => {
const { loading, isAuthenticated, loginWithRedirect } = useAuth0();
useEffect(() => {
if (loading || isAuthenticated) {
return;
}
const fn = async () => {
await loginWithRedirect({
appState: { targetUrl: window.location.pathname }
});
};
fn();
}, [loading, isAuthenticated, loginWithRedirect, path]);
const render = props =>
isAuthenticated === true ? <Component {...props} /> : null;
return <Route path={path} render={render} {...rest} />;
};

View file

@ -1,45 +1,37 @@
import React from "react"; import React from "react";
import { Router, Route, Switch } from "react-router-dom"; import { Route, Switch } from "react-router-dom";
import * as creacteHistory from "history";
import { HomeController } from "../controllers/HomeController"; import { HomeController } from "../controllers/HomeController";
import { ProjectController } from "../controllers/ProjectController"; import { ProjectController } from "../controllers/ProjectController";
import { UserController } from "../controllers/UserController"; import { UserController } from "../controllers/UserController";
import { TicketController } from "../controllers/TicketController"; import { TicketController } from "../controllers/TicketController";
import { NotFoundPage } from "../pages/NotFoundPage"; import { NotFoundPage } from "../pages/NotFoundPage";
// import { TestPage } from "../pages/TestPage";
export const history = creacteHistory.createBrowserHistory();
export const AppRouter = () => { export const AppRouter = () => {
return ( return (
<Router history={history}> <Switch>
<div className="grey lighten-3"> {/* <PrivateRoute exact path="/test">
<Switch> <TestPage />
{/* <Route exact path="/"> </PrivateRoute> */}
<TestPage />
</Route> */}
<Route exact path="/"> <Route exact path="/">
<HomeController /> <HomeController />
</Route> </Route>
<Route path="/users/:id"> <Route path="/users/:id">
<UserController /> <UserController />
</Route> </Route>
<Route path="/projects/:id"> <Route path="/projects/:id">
<ProjectController /> <ProjectController />
</Route> </Route>
<Route path="/tickets/:id"> <Route path="/tickets/:id">
<TicketController /> <TicketController />
</Route> </Route>
<Route path="/404"> <Route path="/404">
<NotFoundPage /> <NotFoundPage />
</Route> </Route>
</Switch> </Switch>
</div>
</Router>
); );
}; };