Dwayne Harris 5 years ago
parent
commit
9ba11bfb60
  1. 278
      package-lock.json
  2. 34
      package.json
  3. 48
      src/actions/apps.ts
  4. 14
      src/actions/groups.ts
  5. 106
      src/components/app.tsx
  6. 79
      src/components/app/app.tsx
  7. 28
      src/components/app/index.ts
  8. 23
      src/components/create-group-form.tsx
  9. 26
      src/components/create-group-form/index.ts
  10. 83
      src/components/create-group-step.tsx
  11. 55
      src/components/create-group-step/create-group-step.tsx
  12. 49
      src/components/create-group-step/index.ts
  13. 24
      src/components/create-user-form.tsx
  14. 26
      src/components/create-user-form/index.ts
  15. 101
      src/components/create-user-step.tsx
  16. 54
      src/components/create-user-step/create-user-step.tsx
  17. 71
      src/components/create-user-step/index.ts
  18. 0
      src/components/footer.tsx
  19. 25
      src/components/forms/checkbox-field.tsx
  20. 24
      src/components/forms/checkbox-field/checkbox-field.tsx
  21. 23
      src/components/forms/checkbox-field/index.ts
  22. 24
      src/components/forms/password-field.tsx
  23. 24
      src/components/forms/password-field/index.ts
  24. 22
      src/components/forms/select-field.tsx
  25. 24
      src/components/forms/select-field/index.ts
  26. 29
      src/components/forms/text-field.tsx
  27. 24
      src/components/forms/text-field/index.ts
  28. 25
      src/components/forms/textarea-field.tsx
  29. 24
      src/components/forms/textarea-field/index.ts
  30. 0
      src/components/group-info.tsx
  31. 55
      src/components/group-invitations.tsx
  32. 38
      src/components/group-invitations/index.ts
  33. 0
      src/components/group-list-item.tsx
  34. 0
      src/components/group-list.tsx
  35. 27
      src/components/group-logs.tsx
  36. 26
      src/components/group-logs/index.tsx
  37. 0
      src/components/member-list-item.tsx
  38. 36
      src/components/member-list.tsx
  39. 26
      src/components/member-list/index.ts
  40. 25
      src/components/member-list/member-list.tsx
  41. 0
      src/components/navigation-menu.tsx
  42. 26
      src/components/notification-container.tsx
  43. 26
      src/components/notification-container/index.ts
  44. 6
      src/components/notification-container/notification-container.scss
  45. 0
      src/components/notification.tsx
  46. 0
      src/components/page-header.tsx
  47. 0
      src/components/pages/about.tsx
  48. 12
      src/components/pages/create-app.tsx
  49. 43
      src/components/pages/developers.tsx
  50. 22
      src/components/pages/directory/index.ts
  51. 94
      src/components/pages/group-admin.tsx
  52. 44
      src/components/pages/group-admin/index.ts
  53. 36
      src/components/pages/group.tsx
  54. 26
      src/components/pages/group/index.ts
  55. 26
      src/components/pages/groups.tsx
  56. 0
      src/components/pages/home.tsx
  57. 6
      src/components/pages/loading.tsx
  58. 101
      src/components/pages/login.tsx
  59. 56
      src/components/pages/login/index.ts
  60. 76
      src/components/pages/login/login.tsx
  61. 101
      src/components/pages/register-group.tsx
  62. 63
      src/components/pages/register-group/index.ts
  63. 61
      src/components/pages/register-group/register-group.tsx
  64. 89
      src/components/pages/register.tsx
  65. 52
      src/components/pages/register/register.tsx
  66. 60
      src/components/pages/self.tsx
  67. 35
      src/components/pages/self/index.ts
  68. 2
      src/components/spinner.tsx
  69. 25
      src/components/user-apps.tsx
  70. 16
      src/components/user-info.tsx
  71. 15
      src/components/user-info/index.ts
  72. 3
      src/components/user-info/user-info.scss
  73. 14
      src/reducers/groups.ts
  74. 18
      src/selectors/apps.ts
  75. 2
      src/selectors/groups.ts
  76. 4
      src/store/index.ts
  77. 43
      src/styles/app.scss
  78. 0
      src/styles/spinner.scss
  79. 29
      src/types/entities.ts
  80. 6
      src/types/store.ts
  81. 46
      src/utils/normalization.ts

278
package-lock.json

@ -13,32 +13,31 @@
}
},
"@fortawesome/fontawesome-common-types": {
"version": "0.2.24",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.24.tgz",
"integrity": "sha512-IPBT/1LdUVQpHcqdrh8uI2/86Fbu7933hkA/HweiCmP5QgF/8PecFM00gYvykxf0RZud8bg8zu+YfggDFUc1Kw=="
"version": "0.2.25",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.25.tgz",
"integrity": "sha512-3RuZPDuuPELd7RXtUqTCfed14fcny9UiPOkdr2i+cYxBoTOfQgxcDoq77fHiiHcgWuo1LoBUpvGxFF1H/y7s3Q=="
},
"@fortawesome/fontawesome-svg-core": {
"version": "1.2.24",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.24.tgz",
"integrity": "sha512-9uVGOEZwviZKbkOVX8nn8cErVqOHBAd1Fqd2OH7Iwu0vxGWdb3fFOMhaAyMXUHZpq1u5C9/HClCV49ci4WmJAg==",
"version": "1.2.25",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.25.tgz",
"integrity": "sha512-MotKnn53JKqbkLQiwcZSBJVYtTgIKFbh7B8+kd05TSnfKYPFmjKKI59o2fpz5t0Hzl35vVGU6+N4twoOpZUrqA==",
"requires": {
"@fortawesome/fontawesome-common-types": "^0.2.24"
"@fortawesome/fontawesome-common-types": "^0.2.25"
}
},
"@fortawesome/free-solid-svg-icons": {
"version": "5.11.1",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.11.1.tgz",
"integrity": "sha512-bB3hXON1K6mVOetTTg5VXZ4CAHg866p7MqenDkJ/eVcbWbGQRE45ojHEwkf37tWx3E8z6lcEameRwU9r5tGwjg==",
"version": "5.11.2",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.11.2.tgz",
"integrity": "sha512-zBue4i0PAZJUXOmLBBvM7L0O7wmsDC8dFv9IhpW5QL4kT9xhhVUsYg/LX1+5KaukWq4/cbDcKT+RT1aRe543sg==",
"requires": {
"@fortawesome/fontawesome-common-types": "^0.2.24"
"@fortawesome/fontawesome-common-types": "^0.2.25"
}
},
"@fortawesome/react-fontawesome": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.4.tgz",
"integrity": "sha512-GwmxQ+TK7PEdfSwvxtGnMCqrfEm0/HbRHArbUudsYiy9KzVCwndxa2KMcfyTQ8El0vROrq8gOOff09RF1oQe8g==",
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.5.tgz",
"integrity": "sha512-WYDKTgyAWOncujWhhzhW7k8sgO5Eo2pZTUL51yNzSQNBUwwr6rNKg/JUSE3iebaU1XShHw74aKc1kJ+jvtRNew==",
"requires": {
"humps": "^2.0.1",
"prop-types": "^15.5.10"
}
},
@ -189,9 +188,9 @@
}
},
"@types/lodash": {
"version": "4.14.138",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.138.tgz",
"integrity": "sha512-A4uJgHz4hakwNBdHNPdxOTkYmXNgmUAKLbXZ7PKGslgeV0Mb8P3BlbYfPovExek1qnod4pDfRbxuzcVs3dlFLg==",
"version": "4.14.141",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.141.tgz",
"integrity": "sha512-v5NYIi9qEbFEUpCyikmnOYe4YlP8BMUdTcNCAquAKzu+FA7rZ1onj9x80mbnDdOW/K5bFf3Tv5kJplP33+gAbQ==",
"dev": true
},
"@types/mime": {
@ -222,9 +221,9 @@
"dev": true
},
"@types/prop-types": {
"version": "15.7.1",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.1.tgz",
"integrity": "sha512-CFzn9idOEpHrgdw8JsoTkaDDyRWk1jrzIV8djzcgpq0y9tG4B4lFT+Nxh52DVpDXV+n4+NPNv7M1Dj5uMp6XFg==",
"version": "15.7.3",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz",
"integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==",
"dev": true
},
"@types/range-parser": {
@ -234,9 +233,9 @@
"dev": true
},
"@types/react": {
"version": "16.9.2",
"resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.2.tgz",
"integrity": "sha512-jYP2LWwlh+FTqGd9v7ynUKZzjj98T8x7Yclz479QdRhHfuW9yQ+0jjnD31eXSXutmBpppj5PYNLYLRfnZJvcfg==",
"version": "16.9.5",
"resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.5.tgz",
"integrity": "sha512-jQ12VMiFOWYlp+j66dghOWcmDDwhca0bnlcTxS4Qz/fh5gi6wpaZDthPEu/Gc/YlAuO87vbiUXL8qKstFvuOaA==",
"dev": true,
"requires": {
"@types/prop-types": "*",
@ -253,18 +252,18 @@
}
},
"@types/react-dom": {
"version": "16.9.0",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.0.tgz",
"integrity": "sha512-OL2lk7LYGjxn4b0efW3Pvf2KBVP0y1v3wip1Bp7nA79NkOpElH98q3WdCEdDj93b2b0zaeBG9DvriuKjIK5xDA==",
"version": "16.9.1",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.1.tgz",
"integrity": "sha512-1S/akvkKr63qIUWVu5IKYou2P9fHLb/P2VAwyxVV85JGaGZTcUniMiTuIqM3lXFB25ej6h+CYEQ27ERVwi6eGA==",
"dev": true,
"requires": {
"@types/react": "*"
}
},
"@types/react-redux": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.2.tgz",
"integrity": "sha512-Iim6UCtD0mZX9U3jBuT6ZObBZ8UlakoOgefiRgi5wakfbNnXd3TUwwUMgi3Ijc0fxsPLZ5ULoz0oDy15YIaLmQ==",
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.4.tgz",
"integrity": "sha512-SUV/7d+4L7C1Db/D4pqASgN1V1U2HnDEhEol9lYpPSguS76xFboZzf5ha2hTz6v31cUewyC7WksMh1q8JxhebQ==",
"dev": true,
"requires": {
"@types/hoist-non-react-statics": "^3.3.0",
@ -274,9 +273,9 @@
}
},
"@types/react-router": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.0.3.tgz",
"integrity": "sha512-j2Gge5cvxca+5lK9wxovmGPgpVJMwjyu5lTA/Cd6fLGoPq7FXcUE1jFkEdxeyqGGz8VfHYSHCn5Lcn24BzaNKA==",
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.1.tgz",
"integrity": "sha512-S7SlFAPb7ZKr6HHMW0kLHGcz8pyJSL0UdM+JtlWthDqKUWwr7E6oPXuHgkofDI8dKCm16slg8K8VCf5pZJquaA==",
"dev": true,
"requires": {
"@types/history": "*",
@ -284,9 +283,9 @@
}
},
"@types/react-router-dom": {
"version": "4.3.5",
"resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-4.3.5.tgz",
"integrity": "sha512-eFajSUASYbPHg2BDM1G8Btx+YqGgvROPIg6sBhl3O4kbDdYXdFdfrgQFf/pcBuQVObjfT9AL/dd15jilR5DIEA==",
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.1.0.tgz",
"integrity": "sha512-YCh8r71pL5p8qDwQf59IU13hFy/41fDQG/GeOI3y+xmD4o0w3vEPxE8uBe+dvOgMoDl0W1WUZsWH0pxc1mcZyQ==",
"dev": true,
"requires": {
"@types/history": "*",
@ -372,9 +371,9 @@
}
},
"@types/webpack": {
"version": "4.39.1",
"resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.39.1.tgz",
"integrity": "sha512-rgO9ihNu/l72Sjx3shqwc9r6gi+tOMsqxhMEZhOEVIZt82GFOeUyEdpTk1BO2HqEHLS/XJW8ldUTIIfIMMyYFQ==",
"version": "4.39.2",
"resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.39.2.tgz",
"integrity": "sha512-3c7+vcmyyIi3RBoOdXs8k3E9rQVIy6yOBqK0DFk6lnJ76JUfbDBWbEf1JflzyPQf56W4ToE+2YPnbxbucniW5w==",
"dev": true,
"requires": {
"@types/anymatch": "*",
@ -383,14 +382,6 @@
"@types/uglify-js": "*",
"@types/webpack-sources": "*",
"source-map": "^0.6.0"
},
"dependencies": {
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
}
}
},
"@types/webpack-bundle-analyzer": {
@ -424,14 +415,6 @@
"@types/node": "*",
"@types/source-list-map": "*",
"source-map": "^0.6.1"
},
"dependencies": {
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
}
}
},
"@types/zxcvbn": {
@ -884,9 +867,9 @@
"dev": true
},
"array-flatten": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz",
"integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==",
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=",
"dev": true
},
"array-union": {
@ -1133,9 +1116,9 @@
}
},
"bluebird": {
"version": "3.5.5",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.5.tgz",
"integrity": "sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w==",
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.0.tgz",
"integrity": "sha512-aBQ1FxIa7kSWCcmKHlcHFlT2jt6J/l4FzC7KcPELkOJOsPOb/bccdhmIrKDfXhwFrmc7vDoDrrepFvGqjyXGJg==",
"dev": true
},
"bn.js": {
@ -1160,14 +1143,6 @@
"qs": "6.7.0",
"raw-body": "2.4.0",
"type-is": "~1.6.17"
},
"dependencies": {
"bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==",
"dev": true
}
}
},
"bonjour": {
@ -1182,6 +1157,14 @@
"dns-txt": "^2.0.2",
"multicast-dns": "^6.0.1",
"multicast-dns-service-types": "^1.1.0"
},
"dependencies": {
"array-flatten": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz",
"integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==",
"dev": true
}
}
},
"boolbase": {
@ -1328,9 +1311,9 @@
"dev": true
},
"bytes": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
"integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=",
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==",
"dev": true
},
"cacache": {
@ -1520,9 +1503,9 @@
}
},
"chownr": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.2.tgz",
"integrity": "sha512-GkfeAQh+QNy3wquu9oIZr6SS5x7wGdSgNQvD10X3r+AZr1Oys22HW8kAmDMvNg2+Dm0TeGaEuO8gFwdBXxwO8A==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.3.tgz",
"integrity": "sha512-i70fVHhmV3DtTl6nqvZOnIjbY0Pe4kAUjwHj8z0zAdgBtYrJyYwLKCCuRBQ5ppkyL0AkN7HKRnETdmdp1zqNXw==",
"dev": true
},
"chrome-trace-event": {
@ -1652,9 +1635,9 @@
}
},
"commander": {
"version": "2.20.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz",
"integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==",
"version": "2.20.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.1.tgz",
"integrity": "sha512-cCuLsMhJeWQ/ZpsFTbE765kvVfoeSddc4nU3up4fV+fDBcfUXnbITJ+JzhkdjzOqhURjZgujxaioam4RM9yGUg==",
"dev": true
},
"commondir": {
@ -1691,6 +1674,14 @@
"on-headers": "~1.0.2",
"safe-buffer": "5.1.2",
"vary": "~1.1.2"
},
"dependencies": {
"bytes": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
"integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=",
"dev": true
}
}
},
"concat-map": {
@ -1925,9 +1916,9 @@
"dev": true
},
"csstype": {
"version": "2.6.6",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.6.tgz",
"integrity": "sha512-RpFbQGUE74iyPgvr46U9t1xoQBM8T4BL8SxrN66Le2xYAPSaDJJKeztV3awugusb3g3G9iL8StmkBBXhcbbXhg==",
"version": "2.6.7",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.7.tgz",
"integrity": "sha512-9Mcn9sFbGBAdmimWb2gLVDtFJzeKtDGIr76TUqmjZrw9LFXBMSU70lcs+C0/7fyCd6iBDqmksUcCOUIkisPHsQ==",
"dev": true
},
"currently-unhandled": {
@ -2532,14 +2523,6 @@
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
},
"dependencies": {
"array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=",
"dev": true
}
}
},
"extend": {
@ -4219,11 +4202,6 @@
"integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=",
"dev": true
},
"humps": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/humps/-/humps-2.0.1.tgz",
"integrity": "sha1-3QLqYIG9BWjcXQcxhEY5V7qe+ao="
},
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@ -4368,9 +4346,9 @@
"dev": true
},
"is-absolute-url": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-3.0.2.tgz",
"integrity": "sha512-+5g/wLlcm1AcxSP7014m6GvbPHswDx980vD/3bZaap8aGV9Yfs7Q6y6tfaupgZ5O74Byzc8dGrSCJ+bFXx0KdA==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-3.0.3.tgz",
"integrity": "sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==",
"dev": true
},
"is-accessor-descriptor": {
@ -5190,9 +5168,9 @@
}
},
"node-forge": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.8.2.tgz",
"integrity": "sha512-mXQ9GBq1N3uDCyV1pdSzgIguwgtVpM7f5/5J4ipz12PKWElmPpVWLDuWl8iXmhysr21+WmX/OJ5UKx82wjomgg==",
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.0.tgz",
"integrity": "sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ==",
"dev": true
},
"node-gyp": {
@ -6178,14 +6156,6 @@
"http-errors": "1.7.2",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
},
"dependencies": {
"bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==",
"dev": true
}
}
},
"re-reselect": {
@ -6194,9 +6164,9 @@
"integrity": "sha512-JsecfN+JlckncVXTWFWjn0Vk6uInl8GSf4eEd9tTk5qXHlgqkPdILpnYpgZcISXNYAzvfvsCZviaDk8AxyS5sg=="
},
"react": {
"version": "16.9.0",
"resolved": "https://registry.npmjs.org/react/-/react-16.9.0.tgz",
"integrity": "sha512-+7LQnFBwkiw+BobzOF6N//BdoNw0ouwmSJTEm9cglOOmsg/TMiFHZLe2sEoN5M7LgJTj9oHH0gxklfnQe66S1w==",
"version": "16.10.2",
"resolved": "https://registry.npmjs.org/react/-/react-16.10.2.tgz",
"integrity": "sha512-MFVIq0DpIhrHFyqLU0S3+4dIcBhhOvBE8bJ/5kHPVOVaGdo0KuiQzpcjCPsf585WvhypqtrMILyoE2th6dT+Lw==",
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
@ -6212,14 +6182,14 @@
}
},
"react-dom": {
"version": "16.9.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.9.0.tgz",
"integrity": "sha512-YFT2rxO9hM70ewk9jq0y6sQk8cL02xm4+IzYBz75CQGlClQQ1Bxq0nhHF6OtSbit+AIahujJgb/CPRibFkMNJQ==",
"version": "16.10.2",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.10.2.tgz",
"integrity": "sha512-kWGDcH3ItJK4+6Pl9DZB16BXYAZyrYQItU4OMy0jAkv5aNqc+mAKb4TpFtAteI6TJZu+9ZlNhaeNQSVQDHJzkw==",
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
"prop-types": "^15.6.2",
"scheduler": "^0.15.0"
"scheduler": "^0.16.2"
}
},
"react-is": {
@ -6241,9 +6211,9 @@
}
},
"react-router": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.0.1.tgz",
"integrity": "sha512-EM7suCPNKb1NxcTZ2LEOWFtQBQRQXecLxVpdsP4DW4PbbqYWeRiLyV/Tt1SdCrvT2jcyXAXmVTmzvSzrPR63Bg==",
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.1.2.tgz",
"integrity": "sha512-yjEuMFy1ONK246B+rsa0cUam5OeAQ8pyclRDgpxuSCrAlJ1qN9uZ5IgyKC7gQg0w8OM50NXHEegPh/ks9YuR2A==",
"requires": {
"@babel/runtime": "^7.1.2",
"history": "^4.9.0",
@ -6273,15 +6243,15 @@
}
},
"react-router-dom": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.0.1.tgz",
"integrity": "sha512-zaVHSy7NN0G91/Bz9GD4owex5+eop+KvgbxXsP/O+iW1/Ln+BrJ8QiIR5a6xNPtrdTvLkxqlDClx13QO1uB8CA==",
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.1.2.tgz",
"integrity": "sha512-7BPHAaIwWpZS074UKaw1FjVdZBSVWEk8IuDXdB+OkLb8vd/WRQIpA4ag9WQk61aEfQs47wHyjWUoUGGZxpQXew==",
"requires": {
"@babel/runtime": "^7.1.2",
"history": "^4.9.0",
"loose-envify": "^1.3.1",
"prop-types": "^15.6.2",
"react-router": "5.0.1",
"react-router": "5.1.2",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
}
@ -6995,9 +6965,9 @@
}
},
"scheduler": {
"version": "0.15.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.15.0.tgz",
"integrity": "sha512-xAefmSfN6jqAa7Kuq7LIJY0bwAPG3xlCj0HMEBQk1lxYiDKZscY2xJ5U/61ZTrYbmNQbXa+gc7czPkVo11tnCg==",
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.16.2.tgz",
"integrity": "sha512-BqYVWqwz6s1wZMhjFvLfVR5WXP7ZY32M/wYPo04CcuPM7XZEbV2TBNW7Z0UkguPTl0dWMA59VbNXxK6q+pHItg==",
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1"
@ -7042,12 +7012,12 @@
"dev": true
},
"selfsigned": {
"version": "1.10.6",
"resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.6.tgz",
"integrity": "sha512-i3+CeqxL7DpAazgVpAGdKMwHuL63B5nhJMh9NQ7xmChGkA3jNFflq6Jyo1LLJYcr3idWiNOPWHCrm4zMayLG4w==",
"version": "1.10.7",
"resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.7.tgz",
"integrity": "sha512-8M3wBCzeWIJnQfl43IKwOmC4H/RAp50S8DF60znzjW5GVqTcSe2vWclt7hmYVPkKPlHWOu5EaWOMZ2Y6W8ZXTA==",
"dev": true,
"requires": {
"node-forge": "0.8.2"
"node-forge": "0.9.0"
}
},
"semver": {
@ -7779,9 +7749,9 @@
}
},
"terser": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-4.3.1.tgz",
"integrity": "sha512-pnzH6dnFEsR2aa2SJaKb1uSCl3QmIsJ8dEkj0Fky+2AwMMcC9doMqLOQIH6wVTEKaVfKVvLSk5qxPBEZT9mywg==",
"version": "4.3.8",
"resolved": "https://registry.npmjs.org/terser/-/terser-4.3.8.tgz",
"integrity": "sha512-otmIRlRVmLChAWsnSFNO0Bfk6YySuBp6G9qrHiJwlLDd4mxe2ta4sjI7TzIR+W1nBMjilzrMcPOz9pSusgx3hQ==",
"dev": true,
"requires": {
"commander": "^2.20.0",
@ -7940,9 +7910,9 @@
"dev": true
},
"ts-loader": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-6.1.2.tgz",
"integrity": "sha512-dudxFKm0Ellrg/gLNlu+97/UgwvoMK0SdUVImPUSzq3IcRUVtShylZvcMX+CgvCQL1BEKb913NL0gAP1GA/OFw==",
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-6.2.0.tgz",
"integrity": "sha512-Da8h3fD+HiZ9GvZJydqzk3mTC9nuOKYlJcpuk+Zv6Y1DPaMvBL+56GRzZFypx2cWrZFMsQr869+Ua2slGoLxvQ==",
"dev": true,
"requires": {
"chalk": "^2.3.0",
@ -8296,9 +8266,9 @@
}
},
"webpack": {
"version": "4.40.2",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-4.40.2.tgz",
"integrity": "sha512-5nIvteTDCUws2DVvP9Qe+JPla7kWPPIDFZv55To7IycHWZ+Z5qBdaBYPyuXWdhggTufZkQwfIK+5rKQTVovm2A==",
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-4.41.0.tgz",
"integrity": "sha512-yNV98U4r7wX1VJAj5kyMsu36T8RPPQntcb5fJLOsMz/pt/WrKC0Vp1bAlqPLkA1LegSwQwf6P+kAbyhRKVQ72g==",
"dev": true,
"requires": {
"@webassemblyjs/ast": "1.8.5",
@ -8432,9 +8402,9 @@
}
},
"webpack-bundle-analyzer": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-3.5.1.tgz",
"integrity": "sha512-CDdaT3TTu4F9X3tcDq6PNJOiNGgREOM0WdN2vVAoUUn+M6NLB5kJ543HImCWbrDwOpbpGARSwU8r+u0Pl367kA==",
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-3.5.2.tgz",
"integrity": "sha512-g9spCNe25QYUVqHRDkwG414GTok2m7pTTP0wr6l0J50Z3YLS04+BGodTqqoVBL7QfU/U/9p/oiI5XFOyfZ7S/A==",
"dev": true,
"requires": {
"acorn": "^6.0.7",
@ -8483,9 +8453,9 @@
}
},
"webpack-dev-middleware": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.7.1.tgz",
"integrity": "sha512-5MWu9SH1z3hY7oHOV6Kbkz5x7hXbxK56mGHNqHTe6d+ewxOwKUxoUJBs7QIaJb33lPjl9bJZ3X0vCoooUzC36A==",
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.7.2.tgz",
"integrity": "sha512-1xC42LxbYoqLNAhV6YzTYacicgMZQTqRd27Sim9wn5hJrX3I5nxYy1SxSd4+gjUFsz1dQFj+yEe6zEVmSkeJjw==",
"dev": true,
"requires": {
"memory-fs": "^0.4.1",
@ -8504,9 +8474,9 @@
}
},
"webpack-dev-server": {
"version": "3.8.1",
"resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.8.1.tgz",
"integrity": "sha512-9F5DnfFA9bsrhpUCAfQic/AXBVHvq+3gQS+x6Zj0yc1fVVE0erKh2MV4IV12TBewuTrYeeTIRwCH9qLMvdNvTw==",
"version": "3.8.2",
"resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.8.2.tgz",
"integrity": "sha512-0xxogS7n5jHDQWy0WST0q6Ykp7UGj4YvWh+HVN71JoE7BwPxMZrwgraBvmdEMbDVMBzF0u+mEzn8TQzBm5NYJQ==",
"dev": true,
"requires": {
"ansi-html": "0.0.7",
@ -8518,18 +8488,18 @@
"del": "^4.1.1",
"express": "^4.17.1",
"html-entities": "^1.2.1",
"http-proxy-middleware": "^0.19.1",
"http-proxy-middleware": "0.19.1",
"import-local": "^2.0.0",
"internal-ip": "^4.3.0",
"ip": "^1.1.5",
"is-absolute-url": "^3.0.2",
"is-absolute-url": "^3.0.3",
"killable": "^1.0.1",
"loglevel": "^1.6.4",
"opn": "^5.5.0",
"p-retry": "^3.0.1",
"portfinder": "^1.0.24",
"schema-utils": "^1.0.0",
"selfsigned": "^1.10.6",
"selfsigned": "^1.10.7",
"semver": "^6.3.0",
"serve-index": "^1.9.1",
"sockjs": "0.3.19",
@ -8538,7 +8508,7 @@
"strip-ansi": "^3.0.1",
"supports-color": "^6.1.0",
"url": "^0.11.0",
"webpack-dev-middleware": "^3.7.1",
"webpack-dev-middleware": "^3.7.2",
"webpack-log": "^2.0.0",
"ws": "^6.2.1",
"yargs": "12.0.5"
@ -8858,9 +8828,9 @@
"dev": true
},
"yallist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz",
"integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==",
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
"dev": true
},
"yargs": {

34
package.json

@ -18,16 +18,16 @@
"devDependencies": {
"@types/classnames": "^2.2.9",
"@types/html-webpack-plugin": "^3.2.1",
"@types/lodash": "^4.14.138",
"@types/lodash": "^4.14.141",
"@types/mini-css-extract-plugin": "^0.8.0",
"@types/react": "^16.9.2",
"@types/react": "^16.9.5",
"@types/react-avatar-editor": "^10.3.4",
"@types/react-dom": "^16.9.0",
"@types/react-redux": "^7.1.2",
"@types/react-router-dom": "^4.3.5",
"@types/react-dom": "^16.9.1",
"@types/react-redux": "^7.1.4",
"@types/react-router-dom": "^5.1.0",
"@types/redux-logger": "^3.0.7",
"@types/uuid": "^3.4.5",
"@types/webpack": "^4.39.1",
"@types/webpack": "^4.39.2",
"@types/webpack-bundle-analyzer": "^2.13.3",
"@types/webpack-dev-server": "^3.1.7",
"@types/zxcvbn": "^4.4.0",
@ -39,29 +39,29 @@
"npm-run-all": "^4.1.5",
"sass-loader": "^8.0.0",
"style-loader": "^1.0.0",
"ts-loader": "^6.1.2",
"ts-loader": "^6.2.0",
"ts-node": "^8.4.1",
"typescript": "^3.6.3",
"webpack": "^4.40.2",
"webpack-bundle-analyzer": "^3.5.1",
"webpack": "^4.41.0",
"webpack-bundle-analyzer": "^3.5.2",
"webpack-cli": "^3.3.9",
"webpack-dev-server": "^3.8.1"
"webpack-dev-server": "^3.8.2"
},
"dependencies": {
"@fortawesome/fontawesome-common-types": "^0.2.24",
"@fortawesome/fontawesome-svg-core": "^1.2.24",
"@fortawesome/free-solid-svg-icons": "^5.11.1",
"@fortawesome/react-fontawesome": "^0.1.4",
"@fortawesome/fontawesome-common-types": "^0.2.25",
"@fortawesome/fontawesome-svg-core": "^1.2.25",
"@fortawesome/free-solid-svg-icons": "^5.11.2",
"@fortawesome/react-fontawesome": "^0.1.5",
"classnames": "^2.2.6",
"history": "^4.10.1",
"lodash": "^4.17.15",
"moment": "^2.24.0",
"re-reselect": "^3.4.0",
"react": "^16.9.0",
"react": "^16.10.2",
"react-avatar-editor": "^11.0.7",
"react-dom": "^16.9.0",
"react-dom": "^16.10.2",
"react-redux": "^7.1.1",
"react-router-dom": "^5.0.1",
"react-router-dom": "^5.1.2",
"redux": "^4.0.4",
"redux-logger": "^3.0.6",
"redux-thunk": "^2.3.0",

48
src/actions/apps.ts

@ -0,0 +1,48 @@
import { apiFetch } from 'src/api'
import { setEntities } from 'src/actions/entities'
import { startRequest, finishRequest } from 'src/actions/requests'
import { objectToQuerystring } from 'src/utils'
import { normalize } from 'src/utils/normalization'
import { AppThunkAction, RequestKey, EntityType, App }from 'src/types'
interface AppsResponse {
apps: App[]
continuation?: string
}
export const fetchApps = (sort?: string, continuation?: string): AppThunkAction => async dispatch => {
dispatch(startRequest(RequestKey.FetchApps))
try {
const response = await apiFetch<AppsResponse>({
path: `/api/apps?${objectToQuerystring({ sort, continuation })}`,
})
const apps = normalize(response.apps, EntityType.App)
dispatch(setEntities(apps.entities))
dispatch(finishRequest(RequestKey.FetchApps, true))
} catch (err) {
dispatch(finishRequest(RequestKey.FetchApps, false))
throw err
}
}
export const fetchSelfApps = (sort?: string): AppThunkAction => async dispatch => {
dispatch(startRequest(RequestKey.FetchSelfApps))
try {
const response = await apiFetch<AppsResponse>({
path: `/api/self/apps?${objectToQuerystring({ sort })}`,
})
const apps = normalize(response.apps, EntityType.App)
dispatch(setEntities(apps.entities))
dispatch(finishRequest(RequestKey.FetchSelfApps, true))
} catch (err) {
dispatch(finishRequest(RequestKey.FetchSelfApps, false))
throw err
}
}

14
src/actions/directory.ts → src/actions/groups.ts

@ -9,34 +9,34 @@ import { normalize } from 'src/utils/normalization'
import { AppThunkAction, Entity, RequestKey, EntityType, User } from 'src/types'
export interface SetGroupsAction extends Action {
type: 'DIRECTORY_SET_GROUPS'
type: 'GROUPS_SET_GROUPS'
payload: string[]
}
export interface AppendGroupsAction extends Action {
type: 'DIRECTORY_APPEND_GROUPS',
type: 'GROUPS_APPEND_GROUPS',
payload: string[]
}
export interface SetContinuationAction extends Action {
type: 'DIRECTORY_SET_CONTINUATION'
type: 'GROUPS_SET_CONTINUATION'
payload: string
}
export type DirectoryActions = SetGroupsAction | AppendGroupsAction | SetContinuationAction
export type GroupsActions = SetGroupsAction | AppendGroupsAction | SetContinuationAction
export const setGroups = (groups: string[]): SetGroupsAction => ({
type: 'DIRECTORY_SET_GROUPS',
type: 'GROUPS_SET_GROUPS',
payload: groups,
})
export const appendGroups = (groups: string[]): AppendGroupsAction => ({
type: 'DIRECTORY_APPEND_GROUPS',
type: 'GROUPS_APPEND_GROUPS',
payload: groups,
})
export const setContinuation = (continuation: string): SetContinuationAction => ({
type: 'DIRECTORY_SET_CONTINUATION',
type: 'GROUPS_SET_CONTINUATION',
payload: continuation,
})

106
src/components/app.tsx

@ -0,0 +1,106 @@
import React, { FC, useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { BrowserRouter as Router, Route, Switch, Link } from 'react-router-dom'
import { fetchSelf, setChecked } from 'src/actions/authentication'
import { getFetching } from 'src/selectors'
import { getCollapsed } from 'src/selectors/menu'
import { LOCAL_STORAGE_ACCESS_TOKEN_KEY } from 'src/constants'
import { AppState, AppThunkDispatch } from 'src/types'
import Footer from './footer'
import NavigationMenu from './navigation-menu'
import NotificationContainer from './notification-container'
import Spinner from './spinner'
import UserInfo from './user-info'
import About from './pages/about'
import Developers from './pages/developers'
import Group from './pages/group'
import GroupAdmin from './pages/group-admin'
import Groups from './pages/groups'
import Home from './pages/home'
import Login from './pages/login'
import Register from './pages/register'
import RegisterGroup from './pages/register-group'
import Self from './pages/self'
import '../styles/app.scss'
import '../styles/spinner.scss'
const App: FC = () => {
const collapsed = useSelector<AppState, boolean>(getCollapsed)
const fetching = useSelector<AppState, boolean>(getFetching)
const dispatch = useDispatch<AppThunkDispatch>()
const mainMenuWidth = 275
const mainColumnMargin = collapsed ? 0 : mainMenuWidth
useEffect(() => {
if (localStorage.getItem(LOCAL_STORAGE_ACCESS_TOKEN_KEY)) {
dispatch(fetchSelf())
} else {
dispatch(setChecked())
}
}, [])
return (
<Router>
<div>
<div id="main-menu" style={{ width: mainMenuWidth }}>
<div id="header">
<Link className="has-text-white is-size-3" to="/">Flexor</Link>
<hr className="has-background-grey-lighter" />
</div>
<NavigationMenu />
{fetching && <Spinner />}
<UserInfo />
<Footer />
</div>
<div id="main-column" style={{ marginRight: mainColumnMargin }}>
<Switch>
<Route path="/">
<Home />
</Route>
<Route path="/login">
<Login />
</Route>
<Route path="/register">
<Register />
</Route>
<Route path="/c/:id">
<Group />
</Route>
<Route path="/c/:id/admin/:tab?">
<GroupAdmin />
</Route>
<Route path="/c/:id/register">
<RegisterGroup />
</Route>
<Route path="/communities">
<Groups />
</Route>
<Route path="/self/:tab?">
<Self />
</Route>
<Route path="/developers">
<Developers />
</Route>
<Route path="/about">
<About />
</Route>
</Switch>
</div>
<NotificationContainer />
</div>
</Router>
)
}
export default App

79
src/components/app/app.tsx

@ -1,79 +0,0 @@
import React, { FC, useEffect } from 'react'
import { BrowserRouter as Router, Route, Link } from 'react-router-dom'
import { LOCAL_STORAGE_ACCESS_TOKEN_KEY } from 'src/constants'
import Footer from '../footer'
import NavigationMenu from '../navigation-menu'
import NotificationContainer from '../notification-container'
import Spinner from '../spinner'
import UserInfo from '../user-info'
import About from '../pages/about'
import Developers from '../pages/developers'
import Directory from '../pages/directory'
import Group from '../pages/group'
import GroupAdmin from '../pages/group-admin'
import Home from '../pages/home'
import Login from '../pages/login'
import Register from '../pages/register'
import RegisterGroup from '../pages/register-group'
import Self from '../pages/self'
import './app.scss'
interface Props {
collapsed: boolean
fetching: boolean
fetchSelf: () => void
setChecked: () => void
}
const App: FC<Props> = ({ collapsed, fetching, fetchSelf, setChecked }) => {
const mainMenuWidth = 275
const mainColumnMargin = collapsed ? 0 : mainMenuWidth
useEffect(() => {
if (localStorage.getItem(LOCAL_STORAGE_ACCESS_TOKEN_KEY)) {
fetchSelf()
} else {
setChecked()
}
}, [])
return (
<Router>
<div>
<div id="main-menu" style={{ width: mainMenuWidth }}>
<div id="header">
<Link className="has-text-white is-size-3" to="/">Flexor</Link>
<hr className="has-background-grey-lighter" />
</div>
<NavigationMenu />
{fetching && <Spinner />}
<UserInfo />
<Footer />
</div>
<div id="main-column" style={{ marginRight: mainColumnMargin }}>
<Route exact path="/" component={Home} />
<Route path="/login" component={Login} />
<Route path="/register" component={Register} />
<Route path="/c/:id" component={Group} exact />
<Route path="/c/:id/admin/:tab?" component={GroupAdmin} />
<Route path="/c/:id/register" component={RegisterGroup} />
<Route path="/communities" component={Directory} />
<Route path="/self/:tab?" component={Self} />
<Route path="/developers" component={Developers} />
<Route path="/about" component={About} />
</div>
<NotificationContainer />
</div>
</Router>
)
}
export default App

28
src/components/app/index.ts

@ -1,28 +0,0 @@
import { connect } from 'react-redux'
import { fetchSelf, setChecked } from 'src/actions/authentication'
import { getFetching } from 'src/selectors'
import { getCollapsed } from 'src/selectors/menu'
import { AppState, AppThunkDispatch } from 'src/types'
import App from './app'
const mapStateToProps = (state: AppState) => ({
collapsed: getCollapsed(state),
fetching: getFetching(state),
})
const mapDispatchToProps = (dispatch: AppThunkDispatch) => ({
fetchSelf: () => {
dispatch(fetchSelf())
},
setChecked: () => {
dispatch(setChecked())
}
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(App)

23
src/components/create-group-form/create-group-form.tsx → src/components/create-group-form.tsx

@ -1,17 +1,24 @@
import React, { FC, FocusEventHandler } from 'react'
import { useDispatch } from 'react-redux'
import { Link } from 'react-router-dom'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faUpload, faIdCard } from '@fortawesome/free-solid-svg-icons'
import CheckboxField from '../forms/checkbox-field'
import TextField from '../forms/text-field'
import SelectField from '../forms/select-field'
import { checkGroupAvailability } from 'src/actions/registration'
interface Props {
checkAvailability: FocusEventHandler<HTMLInputElement>
}
import CheckboxField from './forms/checkbox-field'
import TextField from './forms/text-field'
import SelectField from './forms/select-field'
const CreateGroupForm: FC = () => {
const dispatch = useDispatch()
const checkAvailability = (value: string) => {
if (value.length > 3) {
dispatch(checkGroupAvailability(value))
}
}
const CreateGroupForm: FC<Props> = ({ checkAvailability }) => {
const registrationOptions = {
open: 'Anyone can join',
approval: 'Users must be approved',
@ -20,7 +27,7 @@ const CreateGroupForm: FC<Props> = ({ checkAvailability }) => {
return (
<div className="container">
<TextField name="group-name" label="Community Name" onBlur={checkAvailability} />
<TextField name="group-name" label="Community Name" onBlur={e => checkAvailability(e.target.value)} />
<br />
<SelectField name="group-registration" label="Registration" options={registrationOptions} icon={faIdCard} />

26
src/components/create-group-form/index.ts

@ -1,26 +0,0 @@
import { FocusEventHandler } from 'react'
import { connect } from 'react-redux'
import { checkGroupAvailability } from 'src/actions/registration'
import { AppThunkDispatch } from 'src/types'
import CreateGroupForm from './create-group-form'
const mapDispatchToProps = (dispatch: AppThunkDispatch) => {
const checkAvailability: FocusEventHandler<HTMLInputElement> = event => {
const value = event.target.value
if (value.length > 3) {
dispatch(checkGroupAvailability(event.target.value))
}
}
return {
checkAvailability,
}
}
export default connect(
null,
mapDispatchToProps
)(CreateGroupForm)

83
src/components/create-group-step.tsx

@ -0,0 +1,83 @@
import React, { FC } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faBuilding, faArrowLeft } from '@fortawesome/free-solid-svg-icons'
import { setFieldNotification } from 'src/actions/forms'
import { showNotification } from 'src/actions/notifications'
import { setStep } from 'src/actions/registration'
import { getFieldValue } from 'src/selectors/forms'
import { MAX_ID_LENGTH } from 'src/constants'
import { AppState, AppThunkDispatch, NotificationType } from 'src/types'
import CreateGroupForm from './create-group-form'
interface Props {
register: () => void
}
const CreateGroupStep: FC<Props> = ({ register }) => {
const name = useSelector<AppState, string>(state => getFieldValue<string>(state, 'group-name', ''))
const registration = useSelector<AppState, string>(state => getFieldValue<string>(state, 'group-registration', ''))
const agree = useSelector<AppState, boolean>(state => getFieldValue<boolean>(state, 'group-agree', false))
const dispatch = useDispatch<AppThunkDispatch>()
const next = (name: string, registration: string, agree: boolean) => {
let invalid = false
if (!name) {
dispatch(setFieldNotification('group-name', NotificationType.Error, 'This is required'))
invalid = true
}
if (name.length > MAX_ID_LENGTH) {
dispatch(setFieldNotification('group-name', NotificationType.Error, `This must be less than ${MAX_ID_LENGTH} characters`))
invalid = true
}
if (!agree) {
dispatch(setFieldNotification('group-agree', NotificationType.Error, 'You must agree to the terms and conditions to continue'))
dispatch(showNotification(NotificationType.Error, 'You must agree to the terms and conditions to continue.'))
invalid = true
}
if (invalid) return
register()
}
return (
<div className="centered-content">
<div className="centered-content-icon has-background-primary">
<span className="icon is-large has-text-white">
<FontAwesomeIcon icon={faBuilding} size="2x" />
</span>
</div>
<CreateGroupForm />
<hr />
<nav className="level">
<div className="level-left">
<p className="level-item">
<button className="button" onClick={() => dispatch(setStep(0))}>
<span className="icon is-small">
<FontAwesomeIcon icon={faArrowLeft} />
</span>
<span>Your Account</span>
</button>
</p>
</div>
<div className="level-right">
<p className="level-item">
<button className="button is-success" onClick={() => next(name, registration, agree)}>Finish</button>
</p>
</div>
</nav>
</div>
)
}
export default CreateGroupStep

55
src/components/create-group-step/create-group-step.tsx

@ -1,55 +0,0 @@
import React, { FC } from 'react'
import noop from 'lodash/noop'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faBuilding, faArrowLeft } from '@fortawesome/free-solid-svg-icons'
import CreateGroupForm from '../create-group-form'
export interface Props {
name?: string
registration?: string
agree?: boolean
previous?: () => void
next?: (name: string, registration: string, agree: boolean) => void
register: () => void
}
const CreateGroupStep: FC<Props> = ({
name = '',
registration = '',
agree = false,
previous = noop,
next = noop,
}) => (
<div className="centered-content">
<div className="centered-content-icon has-background-primary">
<span className="icon is-large has-text-white">
<FontAwesomeIcon icon={faBuilding} size="2x" />
</span>
</div>
<CreateGroupForm />
<hr />
<nav className="level">
<div className="level-left">
<p className="level-item">
<button className="button" onClick={() => previous()}>
<span className="icon is-small">
<FontAwesomeIcon icon={faArrowLeft} />
</span>
<span>Your Account</span>
</button>
</p>
</div>
<div className="level-right">
<p className="level-item">
<button className="button is-success" onClick={() => next(name, registration, agree)}>Finish</button>
</p>
</div>
</nav>
</div>
)
export default CreateGroupStep

49
src/components/create-group-step/index.ts

@ -1,49 +0,0 @@
import { connect } from 'react-redux'
import { setFieldNotification } from 'src/actions/forms'
import { showNotification } from 'src/actions/notifications'
import { setStep } from 'src/actions/registration'
import { getFieldValue } from 'src/selectors/forms'
import { MAX_ID_LENGTH } from 'src/constants'
import { AppState, AppThunkDispatch, NotificationType } from 'src/types'
import CreateGroupStep, { Props } from './create-group-step'
const mapStateToProps = (state: AppState) => ({
name: getFieldValue<string>(state, 'group-name', ''),
registration: getFieldValue<string>(state, 'group-registration', ''),
agree: getFieldValue<boolean>(state, 'group-agree', false),
})
const mapDispatchToProps = (dispatch: AppThunkDispatch, ownProps: Props) => ({
previous: () => {
dispatch(setStep(0))
},
next: (name: string, registration: string, agree: boolean) => {
let invalid = false
if (!name) {
dispatch(setFieldNotification('group-name', NotificationType.Error, 'This is required'))
invalid = true
}
if (name.length > MAX_ID_LENGTH) {
dispatch(setFieldNotification('group-name', NotificationType.Error, `This must be less than ${MAX_ID_LENGTH} characters`))
invalid = true
}
if (!agree) {
dispatch(setFieldNotification('group-agree', NotificationType.Error, 'You must agree to the terms and conditions to continue'))
dispatch(showNotification(NotificationType.Error, 'You must agree to the terms and conditions to continue.'))
invalid = true
}
if (invalid) return
if (ownProps.register) ownProps.register()
},
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(CreateGroupStep)

24
src/components/create-user-form/create-user-form.tsx → src/components/create-user-form.tsx

@ -1,19 +1,25 @@
import React, { FC, FocusEventHandler } from 'react'
import React, { FC } from 'react'
import { useDispatch } from 'react-redux'
import { Link } from 'react-router-dom'
import { faEnvelope, faIdCard } from '@fortawesome/free-solid-svg-icons'
import { checkUserAvailability } from 'src/actions/registration'
import CheckboxField from '../forms/checkbox-field'
import TextField from '../forms/text-field'
import PasswordField from '../forms/password-field'
import CheckboxField from './forms/checkbox-field'
import TextField from './forms/text-field'
import PasswordField from './forms/password-field'
interface Props {
checkAvailability: FocusEventHandler<HTMLInputElement>
}
const CreateUserForm: FC = () => {
const dispatch = useDispatch()
const checkAvailability = (value: string) => {
if (value.length > 3) {
dispatch(checkUserAvailability(value))
}
}
const CreateUserForm: FC<Props> = ({ checkAvailability }) => {
return (
<div className="container">
<TextField icon={faIdCard} name="user-id" label="Username" placeholder="Your Username/ID" onBlur={checkAvailability} />
<TextField icon={faIdCard} name="user-id" label="Username" placeholder="Your Username/ID" onBlur={e => checkAvailability(e.target.value)} />
<br />
<TextField name="user-name" label="Display Name" placeholder="Whatever you want to go by" />
<br />

26
src/components/create-user-form/index.ts

@ -1,26 +0,0 @@
import { FocusEventHandler } from 'react'
import { connect } from 'react-redux'
import { checkUserAvailability } from 'src/actions/registration'
import { AppThunkDispatch } from 'src/types'
import CreateUserForm from './create-user-form'
const mapDispatchToProps = (dispatch: AppThunkDispatch) => {
const checkAvailability: FocusEventHandler<HTMLInputElement> = event => {
const value = event.target.value
if (value.length > 3) {
dispatch(checkUserAvailability(event.target.value))
}
}
return {
checkAvailability,
}
}
export default connect(
null,
mapDispatchToProps
)(CreateUserForm)

101
src/components/create-user-step.tsx

@ -0,0 +1,101 @@
import React, { FC } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import zxcvbn from 'zxcvbn'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faUser, faArrowRight } from '@fortawesome/free-solid-svg-icons'
import { setFieldNotification } from 'src/actions/forms'
import { showNotification } from 'src/actions/notifications'
import { setStep } from 'src/actions/registration'
import { getFieldValue } from 'src/selectors/forms'
import { MAX_ID_LENGTH, MAX_NAME_LENGTH } from 'src/constants'
import { AppState, AppThunkDispatch, NotificationType } from 'src/types'
import CreateUserForm from './create-user-form'
const CreateUserStep: FC = () => {
const userId = useSelector<AppState, string>(state => getFieldValue<string>(state, 'user-id', ''))
const name = useSelector<AppState, string>(state => getFieldValue<string>(state, 'user-name', ''))
const email = useSelector<AppState, string>(state => getFieldValue<string>(state, 'user-email', ''))
const password = useSelector<AppState, string>(state => getFieldValue<string>(state, 'password', ''))
const agree = useSelector<AppState, boolean>(state => getFieldValue<boolean>(state, 'user-agree', false))
const dispatch = useDispatch<AppThunkDispatch>()
const next = (userId: string, name: string, email: string, password: string, agree: boolean) => {
let invalid = false
if (!userId) {
dispatch(setFieldNotification('user-id', NotificationType.Error, 'This is required'))
invalid = true
}
if (userId.length > MAX_ID_LENGTH) {
dispatch(setFieldNotification('user-id', NotificationType.Error, `This must be less than ${MAX_ID_LENGTH} characters`))
invalid = true
}
if (name.length > MAX_NAME_LENGTH) {
dispatch(setFieldNotification('user-name', NotificationType.Error, `This must be less than ${MAX_NAME_LENGTH} characters`))
invalid = true
}
if (email === '') {
dispatch(setFieldNotification('user-email', NotificationType.Error, 'This is required'))
invalid = true
}
if (!agree) {
dispatch(setFieldNotification('user-agree', NotificationType.Error, 'You must agree to the terms and conditions to continue'))
dispatch(showNotification(NotificationType.Error, 'You must agree to the terms and conditions to continue.'))
invalid = true
}
if (password === '') {
dispatch(setFieldNotification('password', NotificationType.Error, 'This is required'))
invalid = true
} else {
const { score } = zxcvbn(password)
if (score === 0) {
dispatch(setFieldNotification('password', NotificationType.Error, 'Try another password'))
invalid = true
}
}
if (invalid) return
dispatch(setStep(1))
}
return (
<div className="centered-content">
<div className="centered-content-icon has-background-primary">
<span className="icon is-large has-text-white">
<FontAwesomeIcon icon={faUser} size="2x" />
</span>
</div>
<CreateUserForm />
<hr />
<nav className="level">
<div className="level-left">
<p className="level-item"></p>
</div>
<div className="level-right">
<p className="level-item">
<button className="button is-success" onClick={() => next(userId, name, email, password, agree)}>
<span>Community</span>
<span className="icon is-small">
<FontAwesomeIcon icon={faArrowRight} />
</span>
</button>
</p>
</div>
</nav>
</div>
)
}
export default CreateUserStep

54
src/components/create-user-step/create-user-step.tsx

@ -1,54 +0,0 @@
import React, { FC } from 'react'
import noop from 'lodash/noop'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faUser, faArrowRight } from '@fortawesome/free-solid-svg-icons'
import CreateUserForm from '../create-user-form'
export interface Props {
userId?: string
name?: string
email?: string
password?: string
agree?: boolean
next?: (userId: string, name: string, email: string, password: string, agree: boolean) => void
}
const CreateUserStep: FC<Props> = ({
userId = '',
name = '',
email = '',
password = '',
agree = false,
next = noop,
}) => (
<div className="centered-content">
<div className="centered-content-icon has-background-primary">
<span className="icon is-large has-text-white">
<FontAwesomeIcon icon={faUser} size="2x" />
</span>
</div>
<CreateUserForm />
<hr />
<nav className="level">
<div className="level-left">
<p className="level-item"></p>
</div>
<div className="level-right">
<p className="level-item">
<button className="button is-success" onClick={() => next(userId, name, email, password, agree)}>
<span>Community</span>
<span className="icon is-small">
<FontAwesomeIcon icon={faArrowRight} />
</span>
</button>
</p>
</div>
</nav>
</div>
)
export default CreateUserStep

71
src/components/create-user-step/index.ts

@ -1,71 +0,0 @@
import { connect } from 'react-redux'
import zxcvbn from 'zxcvbn'
import { setFieldNotification } from 'src/actions/forms'
import { showNotification } from 'src/actions/notifications'
import { setStep } from 'src/actions/registration'
import { getFieldValue } from 'src/selectors/forms'
import { MAX_ID_LENGTH, MAX_NAME_LENGTH } from 'src/constants'
import { AppState, AppThunkDispatch, NotificationType } from 'src/types'
import CreateUserStep from './create-user-step'
const mapStateToProps = (state: AppState) => ({
userId: getFieldValue<string>(state, 'user-id', ''),
name: getFieldValue<string>(state, 'user-name', ''),
email: getFieldValue<string>(state, 'user-email', ''),
password: getFieldValue<string>(state, 'password', ''),
agree: getFieldValue<boolean>(state, 'user-agree', false),
})
const mapDispatchToProps = (dispatch: AppThunkDispatch) => ({
next: (userId: string, name: string, email: string, password: string, agree: boolean) => {
let invalid = false
if (!userId) {
dispatch(setFieldNotification('user-id', NotificationType.Error, 'This is required'))
invalid = true
}
if (userId.length > MAX_ID_LENGTH) {
dispatch(setFieldNotification('user-id', NotificationType.Error, `This must be less than ${MAX_ID_LENGTH} characters`))
invalid = true
}
if (name.length > MAX_NAME_LENGTH) {
dispatch(setFieldNotification('user-name', NotificationType.Error, `This must be less than ${MAX_NAME_LENGTH} characters`))
invalid = true
}
if (email === '') {
dispatch(setFieldNotification('user-email', NotificationType.Error, 'This is required'))
invalid = true
}
if (!agree) {
dispatch(setFieldNotification('user-agree', NotificationType.Error, 'You must agree to the terms and conditions to continue'))
dispatch(showNotification(NotificationType.Error, 'You must agree to the terms and conditions to continue.'))
invalid = true
}
if (password === '') {
dispatch(setFieldNotification('password', NotificationType.Error, 'This is required'))
invalid = true
} else {
const { score } = zxcvbn(password)
if (score === 0) {
dispatch(setFieldNotification('password', NotificationType.Error, 'Try another password'))
invalid = true
}
}
if (invalid) return
dispatch(setStep(1))
},
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(CreateUserStep)

0
src/components/footer/index.tsx → src/components/footer.tsx

25
src/components/forms/checkbox-field.tsx

@ -0,0 +1,25 @@
import React, { FC } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { setFieldValue } from 'src/actions/forms'
import { getFieldValue } from 'src/selectors/forms'
import { AppState } from 'src/types'
interface Props {
name: string
}
const CheckboxField: FC<Props> = ({ name, children }) => {
const value = useSelector<AppState, boolean>(state => getFieldValue<boolean>(state, name, false))
const dispatch = useDispatch()
return (
<label className="checkbox">
<input type="checkbox" checked={value} onChange={e => dispatch(setFieldValue(name, e.target.checked))} />
&nbsp;&nbsp;
{children}
</label>
)
}
export default CheckboxField

24
src/components/forms/checkbox-field/checkbox-field.tsx

@ -1,24 +0,0 @@
import React, { FC } from 'react'
import noop from 'lodash/noop'
export interface Props {
name: string
value?: boolean
setValue?: (value: boolean) => void
}
const PasswordField: FC<Props> = ({
value = false,
setValue = noop,
children,
}) => {
return (
<label className="checkbox">
<input type="checkbox" checked={value} onChange={(e) => setValue(e.target.checked)} />
&nbsp;&nbsp;
{children}
</label>
)
}
export default PasswordField

23
src/components/forms/checkbox-field/index.ts

@ -1,23 +0,0 @@
import { Dispatch } from 'redux'
import { connect } from 'react-redux'
import { setFieldValue } from 'src/actions/forms'
import { getFieldValue } from 'src/selectors/forms'
import { AppState } from 'src/types'
import CheckboxField, { Props } from './checkbox-field'
const mapStateToProps = (state: AppState, ownProps: Props) => ({
value: getFieldValue<boolean>(state, ownProps.name, false),
})
const mapDispatchToProps = (dispatch: Dispatch, ownProps: Props) => ({
setValue: (value: boolean) => {
dispatch(setFieldValue(ownProps.name, value))
}
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(CheckboxField)

24
src/components/forms/password-field/password-field.tsx → src/components/forms/password-field.tsx

@ -1,31 +1,32 @@
import React, { FC, ReactNode } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import classNames from 'classnames'
import noop from 'lodash/noop'
import zxcvbn from 'zxcvbn'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { IconDefinition } from '@fortawesome/fontawesome-common-types'
import { faKey, faExclamationTriangle, faCheckCircle } from '@fortawesome/free-solid-svg-icons'
import { setFieldValue } from 'src/actions/forms'
import { getFieldValue, getFieldNotification } from 'src/selectors/forms'
import { notificationTypeToClassName } from 'src/utils'
import { FormNotification, ClassDictionary } from 'src/types'
import { AppState, FormNotification, ClassDictionary } from 'src/types'
export interface Props {
placeholder?: string
userInputs?: string[]
value?: string
notification?: FormNotification
showStrength?: boolean
setValue?: (value: string) => void
}
const PasswordField: FC<Props> = ({
placeholder,
userInputs = [],
value = '',
notification,
showStrength = true,
setValue = noop,
}) => {
const value = useSelector<AppState, string>(state => getFieldValue<string>(state, 'password', ''))
const notification = useSelector<AppState, FormNotification | undefined>(state => getFieldNotification(state, 'password'))
const dispatch = useDispatch()
const inputClassDictionary: ClassDictionary = { input: true }
const controlClassDictionary: ClassDictionary = { control: true, 'has-icons-left': true }
const helpClassDictionary: ClassDictionary = { help: true }
@ -79,7 +80,12 @@ const PasswordField: FC<Props> = ({
<div className="field">
<label className="label">Password</label>
<div className={classNames(controlClassDictionary)}>
<input className={classNames(inputClassDictionary)} type="password" placeholder={placeholder} value={value} onChange={(e) => setValue(e.target.value)} />
<input
className={classNames(inputClassDictionary)}
type="password"
placeholder={placeholder}
value={value}
onChange={e => dispatch(setFieldValue('password', e.target.value))} />
<span className="icon is-small is-left">
<FontAwesomeIcon icon={faKey} />
</span>

24
src/components/forms/password-field/index.ts

@ -1,24 +0,0 @@
import { Dispatch } from 'redux'
import { connect } from 'react-redux'
import { setFieldValue } from 'src/actions/forms'
import { getFieldValue, getFieldNotification } from 'src/selectors/forms'
import { AppState } from 'src/types'
import PasswordField from './password-field'
const mapStateToProps = (state: AppState) => ({
value: getFieldValue<string>(state, 'password', ''),
notification: getFieldNotification(state, 'password'),
})
const mapDispatchToProps = (dispatch: Dispatch) => ({
setValue: (value: string) => {
dispatch(setFieldValue('password', value))
}
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(PasswordField)

22
src/components/forms/select-field/select-field.tsx → src/components/forms/select-field.tsx

@ -1,34 +1,36 @@
import React, { FC } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import classNames from 'classnames'
import noop from 'lodash/noop'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { IconDefinition } from '@fortawesome/fontawesome-svg-core'
import { setFieldValue } from 'src/actions/forms'
import { getFieldValue, getFieldNotification } from 'src/selectors/forms'
import { notificationTypeToClassName } from 'src/utils'
import { FormNotification, ClassDictionary } from 'src/types'
import { AppState, FormNotification, ClassDictionary } from 'src/types'
interface SelectOptions {
[value: string]: string
}
export interface Props {
interface Props {
name: string
label: string
options: SelectOptions
value?: string
notification?: FormNotification
icon?: IconDefinition
setValue?: (value: string) => void
}
const SelectField: FC<Props> = ({
name,
label,
options,
value,
notification,
icon,
setValue = noop,
}) => {
const value = useSelector<AppState, string>(state => getFieldValue<string>(state, name, ''))
const notification = useSelector<AppState, FormNotification | undefined>(state => getFieldNotification(state, name))
const dispatch = useDispatch()
const opts = Object.entries(options)
const controlClassDictionary: ClassDictionary = { control: true }
const helpClassDictionary: ClassDictionary = { help: true }
@ -49,7 +51,7 @@ const SelectField: FC<Props> = ({
<label className="label">{label}</label>
<div className={classNames(controlClassDictionary)}>
<div className="select">
<select value={value} onChange={e => setValue(e.target.value)}>
<select value={value} onChange={e => dispatch(setFieldValue(name, e.target.value))}>
{opts.map(([key, value]) => <option key={key} value={key}>{value}</option>)}
</select>
</div>

24
src/components/forms/select-field/index.ts

@ -1,24 +0,0 @@
import { Dispatch } from 'redux'
import { connect } from 'react-redux'
import { setFieldValue } from 'src/actions/forms'
import { getFieldValue, getFieldNotification } from 'src/selectors/forms'
import { AppState } from 'src/types'
import SelectField, { Props } from './select-field'
const mapStateToProps = (state: AppState, ownProps: Props) => ({
value: getFieldValue<string>(state, ownProps.name, ''),
notification: getFieldNotification(state, ownProps.name),
})
const mapDispatchToProps = (dispatch: Dispatch, ownProps: Props) => ({
setValue: (value: string) => {
dispatch(setFieldValue(ownProps.name, value))
}
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(SelectField)

29
src/components/forms/text-field/text-field.tsx → src/components/forms/text-field.tsx

@ -1,34 +1,37 @@
import React, { FC, FocusEventHandler } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import classNames from 'classnames'
import noop from 'lodash/noop'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { IconDefinition } from '@fortawesome/fontawesome-common-types'
import { setFieldValue } from 'src/actions/forms'
import { getFieldValue, getFieldNotification } from 'src/selectors/forms'
import { notificationTypeToClassName } from 'src/utils'
import { FormNotification, ClassDictionary } from 'src/types'
import { AppState, AppThunkDispatch, FormNotification, ClassDictionary } from 'src/types'
export interface Props {
interface Props {
name: string
label: string
type?: 'text' | 'email'
placeholder?: string
icon?: IconDefinition
value?: string
notification?: FormNotification
setValue?: (value: string) => void
onBlur?: FocusEventHandler
onBlur?: FocusEventHandler<HTMLInputElement>
}
const TextField: FC<Props> = ({
name,
label,
type = 'text',
placeholder,
icon,
value = '',
notification,
setValue = noop,
onBlur = noop,
}) => {
const value = useSelector<AppState, string>(state => getFieldValue<string>(state, name, ''))
const notification = useSelector<AppState, FormNotification | undefined>(state => getFieldNotification(state, name))
const dispatch = useDispatch()
const controlClassDictionary = { control: true, 'has-icons-left': !!icon }
const helpClassDictionary: ClassDictionary = { help: true }
const inputClassDictionary: ClassDictionary = { input: true }
@ -44,7 +47,13 @@ const TextField: FC<Props> = ({
<div className="field">
<label className="label">{label}</label>
<div className={classNames(controlClassDictionary)}>
<input className={classNames(inputClassDictionary)} type={type} placeholder={placeholder} value={value} onChange={(e) => setValue(e.target.value)} onBlur={onBlur} />
<input
className={classNames(inputClassDictionary)}
type={type}
placeholder={placeholder}
value={value}
onChange={e => dispatch(setFieldValue(name, e.target.value))}
onBlur={onBlur} />
{icon &&
<span className="icon is-small is-left">
<FontAwesomeIcon icon={icon} />

24
src/components/forms/text-field/index.ts

@ -1,24 +0,0 @@
import { Dispatch } from 'redux'
import { connect } from 'react-redux'
import { setFieldValue } from 'src/actions/forms'
import { getFieldValue, getFieldNotification } from 'src/selectors/forms'
import { AppState } from 'src/types'
import TextField, { Props } from './text-field'
const mapStateToProps = (state: AppState, ownProps: Props) => ({
value: getFieldValue<string>(state, ownProps.name, ''),
notification: getFieldNotification(state, ownProps.name),
})
const mapDispatchToProps = (dispatch: Dispatch, ownProps: Props) => ({
setValue: (value: string) => {
dispatch(setFieldValue(ownProps.name, value))
}
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(TextField)

25
src/components/forms/textarea-field/textarea-field.tsx → src/components/forms/textarea-field.tsx

@ -1,29 +1,31 @@
import React, { FC, FocusEventHandler } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import classNames from 'classnames'
import noop from 'lodash/noop'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { setFieldValue } from 'src/actions/forms'
import { getFieldValue, getFieldNotification } from 'src/selectors/forms'
import { notificationTypeToClassName } from 'src/utils'
import { FormNotification, ClassDictionary } from 'src/types'
import { AppState, FormNotification, ClassDictionary } from 'src/types'
export interface Props {
name: string
label: string
placeholder?: string
value?: string
notification?: FormNotification
setValue?: (value: string) => void
onBlur?: FocusEventHandler
}
const TextField: FC<Props> = ({
name,
label,
placeholder,
value = '',
notification,
setValue = noop,
onBlur = noop,
}) => {
const value = useSelector<AppState, string>(state => getFieldValue<string>(state, name, ''))
const notification = useSelector<AppState, FormNotification | undefined>(state => getFieldNotification(state, name))
const dispatch = useDispatch()
const helpClassDictionary: ClassDictionary = { help: true }
const inputClassDictionary: ClassDictionary = { textarea: true }
@ -38,7 +40,12 @@ const TextField: FC<Props> = ({
<div className="field">
<label className="label">{label}</label>
<div className="control">
<textarea className={classNames(inputClassDictionary)} placeholder={placeholder} value={value} onChange={(e) => setValue(e.target.value)} onBlur={onBlur} />
<textarea
className={classNames(inputClassDictionary)}
placeholder={placeholder}
value={value}
onChange={e => dispatch(setFieldValue(name, e.target.value))}
onBlur={onBlur} />
</div>
{notification &&
<p className={classNames(helpClassDictionary)}>{notification.message}</p>

24
src/components/forms/textarea-field/index.ts

@ -1,24 +0,0 @@
import { Dispatch } from 'redux'
import { connect } from 'react-redux'
import { setFieldValue } from 'src/actions/forms'
import { getFieldValue, getFieldNotification } from 'src/selectors/forms'
import { AppState } from 'src/types'
import TextareaField, { Props } from './textarea-field'
const mapStateToProps = (state: AppState, ownProps: Props) => ({
value: getFieldValue<string>(state, ownProps.name, ''),
notification: getFieldNotification(state, ownProps.name),
})
const mapDispatchToProps = (dispatch: Dispatch, ownProps: Props) => ({
setValue: (value: string) => {
dispatch(setFieldValue(ownProps.name, value))
}
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(TextareaField)

0
src/components/group-info/index.tsx → src/components/group-info.tsx

55
src/components/group-invitations/group-invitations.tsx → src/components/group-invitations.tsx

@ -1,34 +1,49 @@
import React, { FC, useEffect } from 'react'
import { Link } from 'react-router-dom'
import { useSelector, useDispatch } from 'react-redux'
import { Link, useHistory } from 'react-router-dom'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faCheckCircle, faStopwatch, faPauseCircle } from '@fortawesome/free-solid-svg-icons'
import noop from 'lodash/noop'
import moment from 'moment'
import { Invitation } from 'src/types'
import { handleApiError } from 'src/api/errors'
import { fetchInvitations, createInvitation } from 'src/actions/groups'
import { getInvitations } from 'src/selectors/groups'
import { getFieldValue } from 'src/selectors/forms'
import { AppState, Invitation, AppThunkDispatch } from 'src/types'
import SelectField from 'src/components/forms/select-field'
export interface Props {
interface Props {
group: string
invitations?: Invitation[]
expiration?: string
limit?: string
fetchInvitations?: () => void
createInvitation?: (expiration: string, limit: string) => void
}
const GroupInvitations: FC<Props> = ({
group,
invitations = [],
expiration = '0',
limit = '0',
fetchInvitations = noop,
createInvitation = noop,
}) => {
const GroupInvitations: FC<Props> = ({ group }) => {
const invitations = useSelector<AppState, Invitation[]>(getInvitations)
const expiration = useSelector<AppState, string>(state => getFieldValue<string>(state, 'expiration', '0'))
const limit = useSelector<AppState, string>(state => getFieldValue<string>(state, 'limit', '0'))
const dispatch = useDispatch<AppThunkDispatch>()
const history = useHistory()
const handleCreateInvitation = async () => {
try {
await dispatch(createInvitation(group, moment().add(expiration, 'day').valueOf(), parseInt(limit, 10)))
await dispatch(fetchInvitations(group))
} catch (err) {
handleApiError(err, dispatch, history)
}
}
useEffect(() => {
if (invitations.length === 0) fetchInvitations()
}, [group, fetchInvitations])
if (invitations.length === 0) {
try {
dispatch(fetchInvitations(group))
} catch (err) {
handleApiError(err, dispatch, history)
}
}
}, [group])
const expirationOptions = {
'0': 'No Expiration',
@ -56,7 +71,7 @@ const GroupInvitations: FC<Props> = ({
<div className="field">
<div className="label">&nbsp;</div>
<div className="control">
<button className="button is-primary" onClick={() => createInvitation(expiration, limit)}>
<button className="button is-primary" onClick={() => handleCreateInvitation()}>
<span className="icon is-small">
<FontAwesomeIcon icon={faCheckCircle} />
</span>

38
src/components/group-invitations/index.ts

@ -1,38 +0,0 @@
import { connect } from 'react-redux'
import moment from 'moment'
import { handleApiError } from 'src/api/errors'
import { fetchInvitations, createInvitation } from 'src/actions/directory'
import { getInvitations } from 'src/selectors/directory'
import { getFieldValue } from 'src/selectors/forms'
import { AppState, AppThunkDispatch } from 'src/types'
import GroupInvitations, { Props } from './group-invitations'
const mapStateToProps = (state: AppState) => ({
invitations: getInvitations(state),
expiration: getFieldValue<string>(state, 'expiration', '0'),
limit: getFieldValue<string>(state, 'limit', '0'),
})
const mapDispatchToProps = (dispatch: AppThunkDispatch, ownProps: Props) => ({
fetchInvitations: () => {
try {
dispatch(fetchInvitations(ownProps.group))
} catch (err) {
handleApiError(err, dispatch)
}
},
createInvitation: async (expiration: string, limit: string) => {
try {
await dispatch(createInvitation(ownProps.group, moment().add(expiration, 'day').valueOf(), parseInt(limit, 10)))
await dispatch(fetchInvitations(ownProps.group))
} catch (err) {
handleApiError(err, dispatch)
}
},
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(GroupInvitations)

0
src/components/group-list/group-list-item/index.tsx → src/components/group-list-item.tsx

0
src/components/group-list/index.tsx → src/components/group-list.tsx

27
src/components/group-logs/group-logs.tsx → src/components/group-logs.tsx

@ -1,19 +1,30 @@
import React, { FC, useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { Link } from 'react-router-dom'
import noop from 'lodash/noop'
import moment from 'moment'
import { GroupLog } from 'src/types'
export interface Props {
import { handleApiError } from 'src/api/errors'
import { fetchLogs } from 'src/actions/groups'
import { getLogs } from 'src/selectors/groups'
import { AppState, GroupLog } from 'src/types'
interface Props {
group: string
logs?: GroupLog[]
fetchLogs?: () => void
}
const MemberList: FC<Props> = ({ group, logs = [], fetchLogs = noop }) => {
const MemberList: FC<Props> = ({ group }) => {
const logs = useSelector<AppState, GroupLog[]>(getLogs)
const dispatch = useDispatch()
useEffect(() => {
if (logs.length === 0) fetchLogs()
}, [group, fetchLogs])
if (logs.length === 0) {
try {
dispatch(fetchLogs(group))
} catch (err) {
handleApiError(err, dispatch)
}
}
}, [group])
return (
<table className="table is-bordered is-striped is-narrow is-hoverable is-fullwidth">

26
src/components/group-logs/index.tsx

@ -1,26 +0,0 @@
import { connect } from 'react-redux'
import { handleApiError } from 'src/api/errors'
import { fetchLogs } from 'src/actions/directory'
import { getLogs } from 'src/selectors/directory'
import { AppState, AppThunkDispatch } from 'src/types'
import GroupLogs, { Props } from './group-logs'
const mapStateToProps = (state: AppState) => ({
logs: getLogs(state),
})
const mapDispatchToProps = (dispatch: AppThunkDispatch, ownProps: Props) => ({
fetchLogs: () => {
try {
dispatch(fetchLogs(ownProps.group))
} catch (err) {
handleApiError(err, dispatch)
}
},
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(GroupLogs)

0
src/components/member-list/member-list-item/index.tsx → src/components/member-list-item.tsx

36
src/components/member-list.tsx

@ -0,0 +1,36 @@
import React, { FC, useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { useHistory } from 'react-router-dom'
import { handleApiError } from 'src/api/errors'
import { fetchGroupMembers } from 'src/actions/groups'
import { getGroupMembers } from 'src/selectors/groups'
import { AppState, User, AppThunkDispatch } from 'src/types'
import MemberListItem from './member-list-item'
export interface Props {
group: string
}
const MemberList: FC<Props> = ({ group }) => {
const members = useSelector<AppState, User[]>(state => getGroupMembers(state, group))
const dispatch = useDispatch<AppThunkDispatch>()
const history = useHistory()
useEffect(() => {
try {
dispatch(fetchGroupMembers(group))
} catch (err) {
handleApiError(err, dispatch, history)
}
}, [group])
return (
<div className="is-flex">
{members.map(member => <MemberListItem key={member.id} member={member} />)}
</div>
)
}
export default MemberList

26
src/components/member-list/index.ts

@ -1,26 +0,0 @@
import { connect } from 'react-redux'
import { handleApiError } from 'src/api/errors'
import { fetchGroupMembers } from 'src/actions/directory'
import { getGroupMembers } from 'src/selectors/directory'
import { AppState, AppThunkDispatch } from 'src/types'
import MemberList, { Props } from './member-list'
const mapStateToProps = (state: AppState, ownProps: Props) => ({
members: getGroupMembers(state, ownProps.group),
})
const mapDispatchToProps = (dispatch: AppThunkDispatch, ownProps: Props) => ({
fetchGroupMembers: () => {
try {
dispatch(fetchGroupMembers(ownProps.group))
} catch (err) {
handleApiError(err, dispatch)
}
},
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(MemberList)

25
src/components/member-list/member-list.tsx

@ -1,25 +0,0 @@
import React, { FC, useEffect } from 'react'
import noop from 'lodash/noop'
import { User } from 'src/types'
import MemberListItem from './member-list-item'
export interface Props {
group: string
members?: User[]
fetchGroupMembers?: () => void
}
const MemberList: FC<Props> = ({ group, members = [], fetchGroupMembers = noop }) => {
useEffect(() => {
fetchGroupMembers()
}, [group, fetchGroupMembers])
return (
<div className="is-flex">
{members.map(member => <MemberListItem key={member.id} member={member} />)}
</div>
)
}
export default MemberList

0
src/components/navigation-menu/index.tsx → src/components/navigation-menu.tsx

26
src/components/notification-container/notification-container.tsx → src/components/notification-container.tsx

@ -1,18 +1,26 @@
import React, { FC } from 'react'
import { Notification as INotification } from 'src/types'
import { useSelector, useDispatch } from 'react-redux'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faDoorOpen } from '@fortawesome/free-solid-svg-icons'
import Notification from '../notification'
import './notification-container.scss'
import { setNotificationAuto, removeNotification } from 'src/actions/notifications'
import { getNotifications } from 'src/selectors'
import { AppState, Notification as INotification } from 'src/types'
interface Props {
notifications: INotification[]
setAuto: (id: string) => void
dismiss: (id: string) => void
}
import Notification from './notification'
const NotificationContainer: FC = () => {
const notifications = useSelector<AppState, INotification[]>(getNotifications)
const dispatch = useDispatch()
const setAuto = (id: string) => {
dispatch(setNotificationAuto(id))
}
const dismiss = (id: string) => {
dispatch(removeNotification(id))
}
const NotificationContainer: FC<Props> = ({ notifications, setAuto, dismiss }) => {
return (
<div id="notification-container">
{notifications.map(notification => {

26
src/components/notification-container/index.ts

@ -1,26 +0,0 @@
import { Dispatch } from 'redux'
import { connect } from 'react-redux'
import { setNotificationAuto, removeNotification } from 'src/actions/notifications'
import { getNotifications } from 'src/selectors'
import { AppState } from 'src/types'
import NotificationContainer from './notification-container'
const mapStateToProps = (state: AppState) => ({
notifications: getNotifications(state),
})
const mapDispatchToProps = (dispatch: Dispatch) => ({
setAuto: (id: string) => {
dispatch(setNotificationAuto(id))
},
dismiss: (id: string) => {
dispatch(removeNotification(id))
}
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(NotificationContainer)

6
src/components/notification-container/notification-container.scss

@ -1,6 +0,0 @@
div#notification-container {
bottom: 10px;
position: absolute;
left: 10px;
width: 40%;
}

0
src/components/notification/index.tsx → src/components/notification.tsx

0
src/components/page-header/index.tsx → src/components/page-header.tsx

0
src/components/pages/about/index.tsx → src/components/pages/about.tsx

12
src/components/pages/developers/index.tsx → src/components/pages/create-app.tsx

@ -3,20 +3,22 @@ import { setTitle } from 'src/utils'
import PageHeader from 'src/components/page-header'
const Developers: FC = () => {
const CreateApp: FC = () => {
useEffect(() => {
setTitle('Developers')
setTitle('Create a new App')
})
return (
<div>
<PageHeader title="Developers" />
<PageHeader title="Create a new App" />
<div className="main-content">
Developer documentation coming soon.
<div className="centered-content">
</div>
</div>
</div>
)
}
export default Developers
export default CreateApp

43
src/components/pages/developers.tsx

@ -0,0 +1,43 @@
import React, { FC, useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { Link } from 'react-router-dom'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faPlusCircle } from '@fortawesome/free-solid-svg-icons'
import { fetchSelfApps } from 'src/actions/apps'
import { getSelfApps } from 'src/selectors/apps'
import { setTitle } from 'src/utils'
import { AppState, App, AppThunkDispatch } from 'src/types'
import PageHeader from 'src/components/page-header'
const Developers: FC = () => {
const apps = useSelector<AppState, App[]>(getSelfApps)
const dispatch = useDispatch<AppThunkDispatch>()
useEffect(() => {
setTitle('Developers')
dispatch(fetchSelfApps())
}, [])
return (
<div>
<PageHeader title="Developers" />
<div className="main-content">
<div className="centered-content">
<p>This is where you manage apps you create.</p>
<Link className="button is-primary" to="/apps/new">
<span className="icon is-small">
<FontAwesomeIcon icon={faPlusCircle} />
</span>
<span>Create a new App</span>
</Link>
</div>
</div>
</div>
)
}
export default Developers

22
src/components/pages/directory/index.ts

@ -1,22 +0,0 @@
import { connect } from 'react-redux'
import { fetchGroups } from 'src/actions/directory'
import { getGroups } from 'src/selectors/directory'
import { AppState, AppThunkDispatch } from 'src/types'
import Directory from './directory'
const mapStateToProps = (state: AppState) => ({
groups: getGroups(state),
})
const mapDispatchToProps = (dispatch: AppThunkDispatch) => ({
fetchGroups: () => {
dispatch(fetchGroups())
},
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(Directory)

94
src/components/pages/group-admin/group-admin.tsx → src/components/pages/group-admin.tsx

@ -1,12 +1,25 @@
import React, { FC, useEffect } from 'react'
import { RouteComponentProps, Link } from 'react-router-dom'
import { useSelector, useDispatch } from 'react-redux'
import { Link, useParams, useHistory } from 'react-router-dom'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faCheckCircle } from '@fortawesome/free-solid-svg-icons'
import { handleApiError } from 'src/api/errors'
import { initForm, initField, setFieldValue } from 'src/actions/forms'
import { fetchGroup, updateGroup } from 'src/actions/groups'
import { getEntity } from 'src/selectors/entities'
import { getFieldValue } from 'src/selectors/forms'
import { useDeepCompareEffect } from 'src/hooks'
import { setTitle } from 'src/utils'
import { Group, GroupMembershipType, Tab } from 'src/types'
import {
AppState,
AppThunkDispatch,
Group,
GroupMembershipType,
Tab,
EntityType,
} from 'src/types'
import PageHeader from 'src/components/page-header'
import TextareaField from 'src/components/forms/textarea-field'
@ -20,41 +33,26 @@ interface Params {
tab: string
}
export interface Props extends RouteComponentProps<Params> {
group?: Group
about?: string
fetchGroup: () => void
initForm: (group: Group) => void
updateGroup: (about: string) => void
}
const tabs: Tab[] = [
{ id: '', label: 'General' },
{ id: 'members', label: 'Members' },
{ id: 'logs', label: 'Logs' },
]
const GroupAdmin: FC = () => {
const { id, tab = '' } = useParams<Params>()
const history = useHistory()
const group = useSelector<AppState, Group | undefined>(state => getEntity<Group>(state, EntityType.Group, id))
const about = useSelector<AppState, string>(state => getFieldValue<string>(state, 'about', ''))
const GroupAdmin: FC<Props> = ({
group,
about = '',
fetchGroup,
initForm,
updateGroup,
match,
history,
}) => {
const tab = match.params.tab || ''
const tabs: Tab[] = [
{
id: '',
label: 'General',
},
{
id: 'members',
label: 'Members',
},
{
id: 'logs',
label: 'Logs',
},
]
const dispatch = useDispatch<AppThunkDispatch>()
useEffect(() => {
fetchGroup()
try {
dispatch(fetchGroup(id))
} catch (err) {
handleApiError(err, dispatch, history)
}
}, [])
useDeepCompareEffect(() => {
@ -64,10 +62,24 @@ const GroupAdmin: FC<Props> = ({
return
}
dispatch(initForm())
dispatch(initField('about'))
dispatch(initField('expiration'))
dispatch(initField('limit'))
dispatch(setFieldValue('about', group.about as string))
setTitle(`${group.name} Administration`)
initForm(group)
}
}, [group, initForm])
}, [group])
const handleUpdateGroup = async () => {
try {
await dispatch(updateGroup(id, { about }))
await dispatch(fetchGroup(id))
} catch (err) {
handleApiError(err, dispatch, history)
}
}
if (!group) return <Loading />
@ -111,7 +123,7 @@ const GroupAdmin: FC<Props> = ({
<TextareaField name="about" label="About" placeholder="About this Community" />
<br />
<button className="button is-primary" onClick={e => updateGroup(about)}>
<button className="button is-primary" onClick={e => handleUpdateGroup()}>
<span className="icon is-small">
<FontAwesomeIcon icon={faCheckCircle} />
</span>
@ -122,15 +134,15 @@ const GroupAdmin: FC<Props> = ({
{tab === 'members' &&
<div>
<GroupInvitations group={match.params.id} />
<GroupInvitations group={id} />
<hr />
<h1 className="title is-size-4">Members</h1>
<MemberList group={match.params.id} />
<MemberList group={id} />
</div>
}
{tab === 'logs' && <GroupLogs group={match.params.id} />}
{tab === 'logs' && <GroupLogs group={id} />}
</div>
</div>
</div>

44
src/components/pages/group-admin/index.ts

@ -1,44 +0,0 @@
import { connect } from 'react-redux'
import { handleApiError } from 'src/api/errors'
import { fetchGroup, updateGroup } from 'src/actions/directory'
import { getEntity } from 'src/selectors/entities'
import { getFieldValue } from 'src/selectors/forms'
import { AppState, EntityType, Group, AppThunkDispatch } from 'src/types'
import GroupAdmin, { Props } from './group-admin'
import { initForm, initField, setFieldValue } from 'src/actions/forms'
const mapStateToProps = (state: AppState, ownProps: Props) => ({
group: getEntity<Group>(state, EntityType.Group, ownProps.match.params.id),
about: getFieldValue<string>(state, 'about'),
})
const mapDispatchToProps = (dispatch: AppThunkDispatch, ownProps: Props) => ({
fetchGroup: () => {
try {
dispatch(fetchGroup(ownProps.match.params.id))
} catch (err) {
handleApiError(err, dispatch, ownProps.history)
}
},
initForm: (group: Group) => {
dispatch(initForm())
dispatch(initField('about'))
dispatch(initField('expiration'))
dispatch(initField('limit'))
dispatch(setFieldValue('about', group.about as string))
},
updateGroup: (about: string) => {
try {
dispatch(updateGroup(ownProps.match.params.id, { about }))
dispatch(fetchGroup(ownProps.match.params.id))
} catch (err) {
handleApiError(err, dispatch, ownProps.history)
}
}
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(GroupAdmin)

36
src/components/pages/group/group.tsx → src/components/pages/group.tsx

@ -1,41 +1,43 @@
import React, { FC, useEffect } from 'react'
import { Link, RouteComponentProps } from 'react-router-dom'
import { useSelector, useDispatch } from 'react-redux'
import { Link, useParams, useHistory } from 'react-router-dom'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faEdit, faUserCheck } from '@fortawesome/free-solid-svg-icons'
import { handleApiError } from 'src/api/errors'
import { fetchGroup } from 'src/actions/groups'
import { getEntity } from 'src/selectors/entities'
import { setTitle } from 'src/utils'
import { Group, GroupMembershipType } from 'src/types'
import { AppState, EntityType, Group, GroupMembershipType, AppThunkDispatch } from 'src/types'
import PageHeader from 'src/components/page-header'
import GroupInfo from 'src/components/group-info'
import Loading from 'src/components/pages/loading'
interface Params {
id: string
}
export interface Props extends RouteComponentProps<Params> {
group?: Group
fetchGroup: () => void
}
const GroupPage: FC = () => {
const { id } = useParams<Params>()
const group = useSelector<AppState, Group | undefined>(state => getEntity<Group>(state, EntityType.Group, id))
const dispatch = useDispatch<AppThunkDispatch>()
const history = useHistory()
const GroupPage: FC<Props> = ({ group, fetchGroup }) => {
useEffect(() => {
fetchGroup()
try {
dispatch(fetchGroup(id))
} catch (err) {
handleApiError(err, dispatch, history)
}
}, [])
useEffect(() => {
if (group) setTitle(group.name)
}, [group])
if (!group) {
return (
<div>
<PageHeader title="Group" />
<div className="main-content"></div>
</div>
)
}
if (!group) return <Loading />
const isAdmin = group.membership === GroupMembershipType.Admin

26
src/components/pages/group/index.ts

@ -1,26 +0,0 @@
import { connect } from 'react-redux'
import { handleApiError } from 'src/api/errors'
import { fetchGroup } from 'src/actions/directory'
import { getEntity } from 'src/selectors/entities'
import { AppState, EntityType, Group, AppThunkDispatch } from 'src/types'
import GroupPage, { Props } from './group'
const mapStateToProps = (state: AppState, ownProps: Props) => ({
group: getEntity<Group>(state, EntityType.Group, ownProps.match.params.id),
})
const mapDispatchToProps = (dispatch: AppThunkDispatch, ownProps: Props) => ({
fetchGroup: () => {
try {
dispatch(fetchGroup(ownProps.match.params.id))
} catch (err) {
handleApiError(err, dispatch, ownProps.history)
}
}
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(GroupPage)

26
src/components/pages/directory/directory.tsx → src/components/pages/groups.tsx

@ -1,27 +1,25 @@
import React, { FC, useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { Link } from 'react-router-dom'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faPlusCircle } from '@fortawesome/free-solid-svg-icons'
import { fetchGroups } from 'src/actions/groups'
import { getGroups } from 'src/selectors/groups'
import { setTitle } from 'src/utils'
import { Group } from 'src/types'
import { AppState, AppThunkDispatch, Group } from 'src/types'
import PageHeader from 'src/components/page-header'
import GroupList from 'src/components/group-list'
interface Props {
groups: Group[]
fetchGroups: () => void
}
const Directory: FC<Props> = ({ groups, fetchGroups }) => {
useEffect(() => {
fetchGroups()
}, [])
const Groups: FC = () => {
const groups = useSelector<AppState, Group[]>(getGroups)
const dispatch = useDispatch<AppThunkDispatch>()
useEffect(() => {
dispatch(fetchGroups())
setTitle('Communities')
})
}, [])
return (
<div>
@ -29,9 +27,7 @@ const Directory: FC<Props> = ({ groups, fetchGroups }) => {
<div className="main-content">
<GroupList groups={groups} />
{groups.length === 0 && <p>No Communities</p>}
<br /><br />
<hr />
<p className="has-text-centered">
<Link className="button is-primary" to="/register">
@ -46,4 +42,4 @@ const Directory: FC<Props> = ({ groups, fetchGroups }) => {
)
}
export default Directory
export default Groups

0
src/components/pages/home/index.tsx → src/components/pages/home.tsx

6
src/components/pages/loading/index.tsx → src/components/pages/loading.tsx

@ -1,10 +1,14 @@
import React, { FC } from 'react'
import PageHeader from 'src/components/page-header'
import Spinner from 'src/components/spinner'
const Loading: FC = () => (
<div>
<PageHeader title="Loading..." />
<div className="main-content"></div>
<div className="main-content">
<Spinner />
</div>
</div>
)

101
src/components/pages/login.tsx

@ -0,0 +1,101 @@
import React, { FC, useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { useHistory } from 'react-router'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faIdCard, faKey } from '@fortawesome/free-solid-svg-icons'
import classNames from 'classnames'
import { handleApiError } from 'src/api/errors'
import { authenticate } from 'src/actions/authentication'
import { showNotification } from 'src/actions/notifications'
import { getChecked, getAuthenticated } from 'src/selectors/authentication'
import { getFieldValue } from 'src/selectors/forms'
import { getIsFetching } from 'src/selectors/requests'
import { initForm, initField, setFieldNotification } from 'src/actions/forms'
import PageHeader from 'src/components/page-header'
import TextField from 'src/components/forms/text-field'
import PasswordField from 'src/components/forms/password-field'
import { AppState, RequestKey, ClassDictionary, NotificationType } from 'src/types'
const Login: FC = () => {
const checked = useSelector<AppState, boolean>(getChecked)
const authenticated = useSelector<AppState, boolean>(getAuthenticated)
const name = useSelector<AppState, string>(state => getFieldValue(state, 'name', ''))
const password = useSelector<AppState, string>(state => getFieldValue(state, 'password', ''))
const authenticating = useSelector<AppState, boolean>(state => getIsFetching(state, RequestKey.Authenticate))
const dispatch = useDispatch()
const history = useHistory()
useEffect(() => {
if (checked && authenticated) history.push('/self')
}, [checked, authenticated])
useEffect(() => {
dispatch(initForm())
dispatch(initField('name', 'id'))
dispatch(initField('password'))
}, [])
const handleAuthenticate = async () => {
let invalid = false
if (!name) {
dispatch(setFieldNotification('name', NotificationType.Error, 'This is required'))
invalid = true
}
if (!password) {
dispatch(setFieldNotification('password', NotificationType.Error, 'This is required'))
invalid = true
}
if (invalid) return
try {
const id = await dispatch(authenticate(name, password))
dispatch(showNotification(NotificationType.Welcome, `Welcome back ${id}!`))
history.push('/')
} catch (err) {
handleApiError(err, dispatch, history)
}
}
const buttonClassDictionary: ClassDictionary = {
button: true,
'is-primary': true,
'is-loading': authenticating,
}
return (
<div>
<PageHeader title="Log In to Flexor" />
<div className="main-content">
<div className="centered-content">
<div className="centered-content-icon has-background-primary">
<span className="icon is-large has-text-white">
<FontAwesomeIcon icon={faKey} size="2x" />
</span>
</div>
<TextField icon={faIdCard} name="name" label="Username" placeholder="Your Username/ID" />
<br />
<PasswordField placeholder="Your password" showStrength={false} />
<br />
<button className={classNames(buttonClassDictionary)} onClick={() => handleAuthenticate()} disabled={authenticating}>
<span className="icon is-small">
<FontAwesomeIcon icon={faKey} />
</span>
<span>Log In</span>
</button>
</div>
</div>
</div>
)
}
export default Login

56
src/components/pages/login/index.ts

@ -1,56 +0,0 @@
import { connect } from 'react-redux'
import { handleApiError } from 'src/api/errors'
import { authenticate } from 'src/actions/authentication'
import { showNotification } from 'src/actions/notifications'
import { getChecked, getAuthenticated } from 'src/selectors/authentication'
import { getFieldValue } from 'src/selectors/forms'
import { getIsFetching } from 'src/selectors/requests'
import { initForm, initField, setFieldNotification } from 'src/actions/forms'
import { AppState, AppThunkDispatch, NotificationType, RequestKey } from 'src/types'
import Login, { Props } from './login'
const mapStateToProps = (state: AppState) => ({
checked: getChecked(state),
authenticated: getAuthenticated(state),
name: getFieldValue(state, 'name', ''),
password: getFieldValue(state, 'password', ''),
authenticating: getIsFetching(state, RequestKey.Authenticate),
})
const mapDispatchToProps = (dispatch: AppThunkDispatch, ownProps: Props) => ({
initForm: () => {
dispatch(initForm())
dispatch(initField('name', 'id'))
dispatch(initField('password'))
},
authenticate: async (name: string, password: string) => {
let invalid = false
if (!name) {
dispatch(setFieldNotification('name', NotificationType.Error, 'This is required'))
invalid = true
}
if (!password) {
dispatch(setFieldNotification('password', NotificationType.Error, 'This is required'))
invalid = true
}
if (invalid) return
try {
const id = await dispatch(authenticate(name, password))
dispatch(showNotification(NotificationType.Welcome, `Welcome back ${id}!`))
ownProps.history.push('/')
} catch (err) {
handleApiError(err, dispatch, ownProps.history)
}
},
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(Login)

76
src/components/pages/login/login.tsx

@ -1,76 +0,0 @@
import React, { FC, useEffect } from 'react'
import { RouteComponentProps } from 'react-router'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faIdCard, faKey } from '@fortawesome/free-solid-svg-icons'
import classNames from 'classnames'
import PageHeader from 'src/components/page-header'
import TextField from 'src/components/forms/text-field'
import PasswordField from 'src/components/forms/password-field'
import { ClassDictionary } from 'src/types'
export interface Props extends RouteComponentProps {
checked: boolean
authenticated: boolean
name?: string
password?: string
authenticating?: boolean
initForm: () => void
authenticate: (name: string, password: string) => void
}
const Login: FC<Props> = ({
checked,
authenticated,
name = '',
password = '',
authenticating = false,
initForm,
authenticate,
history
}) => {
useEffect(() => {
if (checked && authenticated) history.push('/self')
}, [checked, authenticated])
useEffect(() => {
initForm()
}, [])
const buttonClassDictionary: ClassDictionary = {
button: true,
'is-primary': true,
'is-loading': authenticating,
}
return (
<div>
<PageHeader title="Log In to Flexor" />
<div className="main-content">
<div className="centered-content">
<div className="centered-content-icon has-background-primary">
<span className="icon is-large has-text-white">
<FontAwesomeIcon icon={faKey} size="2x" />
</span>
</div>
<TextField icon={faIdCard} name="name" label="Username" placeholder="Your Username/ID" />
<br />
<PasswordField placeholder="Your password" showStrength={false} />
<br />
<button className={classNames(buttonClassDictionary)} onClick={() => authenticate(name, password)} disabled={authenticating}>
<span className="icon is-small">
<FontAwesomeIcon icon={faKey} />
</span>
<span>Log In</span>
</button>
</div>
</div>
</div>
)
}
export default Login

101
src/components/pages/register-group.tsx

@ -0,0 +1,101 @@
import React, { FC, useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { useParams, useHistory } from 'react-router'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faUserPlus } from '@fortawesome/free-solid-svg-icons'
import { handleApiError } from 'src/api/errors'
import { fetchGroup } from 'src/actions/groups'
import { initField } from 'src/actions/forms'
import { showNotification } from 'src/actions/notifications'
import { register } from 'src/actions/registration'
import { getEntity } from 'src/selectors/entities'
import { getForm } from 'src/selectors/forms'
import { setTitle, valueFromForm } from 'src/utils'
import { useDeepCompareEffect } from 'src/hooks'
import { AppState, AppThunkDispatch, Group, EntityType, NotificationType, Form } from 'src/types'
import PageHeader from 'src/components/page-header'
import Loading from 'src/components/pages/loading'
import CreateUserForm from 'src/components/create-user-form'
interface Params {
id: string
}
const RegisterGroup: FC = () => {
const { id } = useParams<Params>()
const group = useSelector<AppState, Group | undefined>(state => getEntity(state, EntityType.Group, id))
const form = useSelector<AppState, Form>(getForm)
const dispatch = useDispatch<AppThunkDispatch>()
const history = useHistory()
useEffect(() => {
try {
dispatch(fetchGroup(id))
} catch (err) {
handleApiError(err, dispatch, history)
}
dispatch(initField('user-id', 'id'))
dispatch(initField('user-name', 'name'))
dispatch(initField('user-email', 'email'))
dispatch(initField('password'))
dispatch(initField('user-agree'))
setTitle('Register')
}, [])
useDeepCompareEffect(() => {
if (group) setTitle(`Register with ${group.name}`)
}, [group])
const handleRegister = async () => {
if (!valueFromForm<boolean>(form, 'user-agree', false)) {
dispatch(showNotification(NotificationType.Error, 'You must agree to the terms and conditions.'))
return
}
try {
await dispatch(register({
id: valueFromForm<string>(form, 'user-id', ''),
email: valueFromForm<string>(form, 'user-email', ''),
password: valueFromForm<string>(form, 'password', ''),
name: valueFromForm<string>(form, 'user-name', ''),
group: id,
}))
dispatch(showNotification(NotificationType.Welcome, `Welcome to Flexor!`))
history.push('/self')
} catch (err) {
handleApiError(err, dispatch, history)
}
}
if (!group) return <Loading />
return (
<div>
<PageHeader title="Register" subtitle={group.name} />
<div className="main-content">
<div className="centered-content">
<CreateUserForm />
<br />
<button className="button is-primary" onClick={() => handleRegister()}>
<span className="icon is-small">
<FontAwesomeIcon icon={faUserPlus} />
</span>
<span>Create Your Account</span>
</button>
</div>
</div>
</div>
)
}
export default RegisterGroup

63
src/components/pages/register-group/index.ts

@ -1,63 +0,0 @@
import { connect } from 'react-redux'
import { handleApiError } from 'src/api/errors'
import { fetchGroup } from 'src/actions/directory'
import { initField } from 'src/actions/forms'
import { showNotification } from 'src/actions/notifications'
import { register } from 'src/actions/registration'
import { getEntity } from 'src/selectors/entities'
import { getForm } from 'src/selectors/forms'
import { valueFromForm } from 'src/utils'
import { AppState, AppThunkDispatch, EntityType, Form, NotificationType } from 'src/types'
import RegisterGroup, { Props } from './register-group'
const mapStateToProps = (state: AppState, ownProps: Props) => ({
group: getEntity(state, EntityType.Group, ownProps.match.params.id),
form: getForm(state),
})
const mapDispatchToProps = (dispatch: AppThunkDispatch, ownProps: Props) => ({
initForm: () => {
dispatch(initField('user-id', 'id'))
dispatch(initField('user-name', 'name'))
dispatch(initField('user-email', 'email'))
dispatch(initField('password'))
dispatch(initField('user-agree'))
},
fetchGroup: async () => {
try {
await dispatch(fetchGroup(ownProps.match.params.id))
} catch (err) {
handleApiError(err, dispatch, ownProps.history)
}
},
register: async (form: Form) => {
if (!valueFromForm<boolean>(form, 'user-agree', false)) {
dispatch(showNotification(NotificationType.Error, 'You must agree to the terms and conditions.'))
return
}
try {
await dispatch(register({
id: valueFromForm<string>(form, 'user-id', ''),
email: valueFromForm<string>(form, 'user-email', ''),
password: valueFromForm<string>(form, 'password', ''),
name: valueFromForm<string>(form, 'user-name', ''),
group: ownProps.match.params.id,
}))
dispatch(showNotification(NotificationType.Welcome, `Welcome to Flexor!`))
ownProps.history.push('/self')
} catch (err) {
handleApiError(err, dispatch, ownProps.history)
}
}
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(RegisterGroup)

61
src/components/pages/register-group/register-group.tsx

@ -1,61 +0,0 @@
import React, { FC, useEffect } from 'react'
import { RouteComponentProps } from 'react-router'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faUserPlus } from '@fortawesome/free-solid-svg-icons'
import { setTitle } from 'src/utils'
import { useDeepCompareEffect } from 'src/hooks'
import { Entity, Form } from 'src/types'
import PageHeader from 'src/components/page-header'
import Loading from 'src/components/pages/loading'
import CreateUserForm from 'src/components/create-user-form'
interface Params {
id: string
}
export interface Props extends RouteComponentProps<Params> {
group?: Entity
form: Form
fetchGroup: () => void
initForm: () => void
register: (form: Form) => void
}
const RegisterGroup: FC<Props> = ({ group, form, fetchGroup, initForm, register }) => {
useEffect(() => {
fetchGroup()
initForm()
setTitle('Register')
}, [])
useDeepCompareEffect(() => {
if (group) setTitle(`Register @ ${group.name}`)
}, [group])
if (!group) return <Loading />
return (
<div>
<PageHeader title="Register" subtitle={group ? group.name : ''} />
<div className="main-content">
<div className="centered-content">
<CreateUserForm />
<br />
<button className="button is-primary" onClick={() => register(form)}>
<span className="icon is-small">
<FontAwesomeIcon icon={faUserPlus} />
</span>
<span>Create Your Account</span>
</button>
</div>
</div>
</div>
)
}
export default RegisterGroup

89
src/components/pages/register/index.ts → src/components/pages/register.tsx

@ -1,4 +1,6 @@
import { connect } from 'react-redux'
import React, { FC, useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { useHistory } from 'react-router'
import { handleApiError } from 'src/api/errors'
import { getForm } from 'src/selectors/forms'
@ -7,31 +9,21 @@ import { initForm, initField, setFieldValue } from 'src/actions/forms'
import { showNotification } from 'src/actions/notifications'
import { createGroup, register } from 'src/actions/registration'
import { valueFromForm } from 'src/utils'
import PageHeader from 'src/components/page-header'
import CreateGroupStep from 'src/components/create-group-step'
import CreateUserStep from 'src/components/create-user-step'
import { setTitle, valueFromForm } from 'src/utils'
import { AppState, AppThunkDispatch, Form, NotificationType } from 'src/types'
import Register, { Props } from './register'
const Register: FC = () => {
const stepIndex = useSelector<AppState, number>(getStep)
const form = useSelector<AppState, Form>(getForm)
const mapStateToProps = (state: AppState) => ({
stepIndex: getStep(state),
form: getForm(state),
})
const dispatch = useDispatch<AppThunkDispatch>()
const history = useHistory()
const mapDispatchToProps = (dispatch: AppThunkDispatch, ownProps: Props) => ({
initForm: () => {
dispatch(initForm())
dispatch(initField('group-name', 'name'))
dispatch(initField('group-registration', 'registration'))
dispatch(initField('group-agree'))
dispatch(initField('user-id', 'id'))
dispatch(initField('user-name', 'name'))
dispatch(initField('user-email', 'email'))
dispatch(initField('password'))
dispatch(initField('user-agree'))
dispatch(setFieldValue('group-registration', 'open'))
},
register: async (form: Form) => {
const handleRegister = async () => {
const groupAgree = valueFromForm<boolean>(form, 'group-agree', false)
const userAgree = valueFromForm<boolean>(form, 'user-agree', false)
@ -55,14 +47,53 @@ const mapDispatchToProps = (dispatch: AppThunkDispatch, ownProps: Props) => ({
dispatch(showNotification(NotificationType.Welcome, `Welcome to Flexor!`))
ownProps.history.push('/self')
history.push('/self')
} catch (err) {
handleApiError(err, dispatch, ownProps.history)
handleApiError(err, dispatch, history)
}
}
const title = () => {
switch (stepIndex) {
case 0: return 'Create Your Account'
default: return 'Create a Community'
}
}
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(Register)
const component = () => {
switch (stepIndex) {
case 0: return <CreateUserStep />
default: return <CreateGroupStep register={() => handleRegister()} />
}
}
useEffect(() => {
dispatch(initForm())
dispatch(initField('group-name', 'name'))
dispatch(initField('group-registration', 'registration'))
dispatch(initField('group-agree'))
dispatch(initField('user-id', 'id'))
dispatch(initField('user-name', 'name'))
dispatch(initField('user-email', 'email'))
dispatch(initField('password'))
dispatch(initField('user-agree'))
dispatch(setFieldValue('group-registration', 'open'))
}, [])
useEffect(() => {
setTitle(title())
}, [stepIndex])
return (
<div>
<PageHeader title={title()} />
<div className="main-content">
{component()}
</div>
</div>
)
}
export default Register

52
src/components/pages/register/register.tsx

@ -1,52 +0,0 @@
import React, { FC, useEffect } from 'react'
import { RouteComponentProps } from 'react-router'
import PageHeader from 'src/components/page-header'
import CreateGroupStep from 'src/components/create-group-step'
import CreateUserStep from 'src/components/create-user-step'
import { setTitle } from 'src/utils'
import { Form } from 'src/types'
export interface Props extends RouteComponentProps {
stepIndex: number
form: Form
initForm: () => void
register: (form: Form) => void
}
const Register: FC<Props> = ({ stepIndex, form, initForm, register }) => {
const title = () => {
switch (stepIndex) {
case 0: return 'Create Your Account'
default: return 'Create a Community'
}
}
const component = () => {
switch (stepIndex) {
case 0: return <CreateUserStep />
default: return <CreateGroupStep register={() => register(form)} />
}
}
useEffect(() => {
initForm()
}, [])
useEffect(() => {
setTitle(title())
}, [stepIndex])
return (
<div>
<PageHeader title={title()} />
<div className="main-content">
{component()}
</div>
</div>
)
}
export default Register

60
src/components/pages/self/self.tsx → src/components/pages/self.tsx

@ -1,12 +1,17 @@
import React, { FC } from 'react'
import { Link, RouteComponentProps } from 'react-router-dom'
import { useSelector, useDispatch } from 'react-redux'
import { Link, useParams, useHistory } from 'react-router-dom'
import moment from 'moment'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faDoorOpen, faCheckCircle, faPlusCircle, faIdCard, faEnvelope } from '@fortawesome/free-solid-svg-icons'
import { unauthenticate } from 'src/actions/authentication'
import { initForm, initField, setFieldValue } from 'src/actions/forms'
import { getAuthenticated, getChecked, getAuthenticatedUser } from 'src/selectors/authentication'
import { useAuthenticationCheck, useDeepCompareEffect } from 'src/hooks'
import { setTitle } from 'src/utils'
import { User, Tab } from 'src/types'
import { AppState, User, Tab } from 'src/types'
import PageHeader from 'src/components/page-header'
import Loading from 'src/components/pages/loading'
@ -17,37 +22,38 @@ interface Params {
tab: string
}
export interface Props extends RouteComponentProps<Params> {
checked: boolean
authenticated: boolean
user?: User
initForm: (user: User) => void
logout: () => void
}
const tabs: Tab[] = [
{ id: '', label: 'Posts' },
{ id: 'settings', label: 'Settings' },
{ id: 'apps', label: 'Apps' },
]
const Self: FC = () => {
const { tab = '' } = useParams<Params>()
const dispatch = useDispatch()
const history = useHistory()
const checked = useSelector<AppState, boolean>(getChecked)
const authenticated = useSelector<AppState, boolean>(getAuthenticated)
const user = useSelector<AppState, User | undefined>(getAuthenticatedUser)
const Self: FC<Props> = ({ checked, authenticated, user, initForm, logout, history, match }) => {
useAuthenticationCheck(checked, authenticated, history)
const tab = match.params.tab || ''
const tabs: Tab[] = [
{
id: '',
label: 'Posts',
},
{
id: 'settings',
label: 'Settings',
},
{
id: 'apps',
label: 'Apps',
},
]
const handleLogout = () => {
localStorage.clear()
dispatch(unauthenticate())
window.location.href = '/'
}
useDeepCompareEffect(() => {
if (user) {
setTitle(`${user.name} (@${user.id})`)
initForm(user)
dispatch(initForm())
dispatch(initField('name'))
dispatch(initField('about'))
dispatch(setFieldValue('name', user.name as string))
dispatch(setFieldValue('about', user.about as string))
}
}, [user])
@ -124,7 +130,7 @@ const Self: FC<Props> = ({ checked, authenticated, user, initForm, logout, histo
<hr />
<button className="button is-danger" onClick={() => logout()}>
<button className="button is-danger" onClick={() => handleLogout()}>
<span className="icon is-small">
<FontAwesomeIcon icon={faDoorOpen} />
</span>

35
src/components/pages/self/index.ts

@ -1,35 +0,0 @@
import { Dispatch } from 'redux'
import { connect } from 'react-redux'
import { unauthenticate } from 'src/actions/authentication'
import { getAuthenticated, getChecked, getAuthenticatedUser } from 'src/selectors/authentication'
import { AppState, User } from 'src/types'
import Self from './self'
import { initForm, initField, setFieldValue } from 'src/actions/forms'
const mapStateToProps = (state: AppState) => ({
checked: getChecked(state),
authenticated: getAuthenticated(state),
user: getAuthenticatedUser(state),
})
const mapDispatchToProps = (dispatch: Dispatch) => ({
initForm: (user: User) => {
dispatch(initForm())
dispatch(initField('name'))
dispatch(initField('about'))
dispatch(setFieldValue('name', user.name as string))
dispatch(setFieldValue('about', user.about as string))
},
logout: async () => {
localStorage.clear()
dispatch(unauthenticate())
window.location.href = '/'
},
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(Self)

2
src/components/spinner/index.tsx → src/components/spinner.tsx

@ -1,7 +1,5 @@
import React, { FC } from 'react'
import './spinner.scss'
const Spinner: FC = () => (
<div className="sk-cube-grid">
<div className="sk-cube sk-cube1"></div>

25
src/components/user-apps.tsx

@ -0,0 +1,25 @@
import React, { FC, useEffect } from 'react'
import { Link } from 'react-router-dom'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faPlusCircle } from '@fortawesome/free-solid-svg-icons'
import { setTitle } from 'src/utils'
const UserApps: FC = () => {
useEffect(() => {
setTitle('Your Apps')
}, [])
return (
<div>
<Link className="button is-primary" to="/apps/new">
<span className="icon is-small">
<FontAwesomeIcon icon={faPlusCircle} />
</span>
<span>Create a new App</span>
</Link>
</div>
)
}
export default UserApps

16
src/components/user-info/user-info.tsx → src/components/user-info.tsx

@ -1,17 +1,15 @@
import React, { FC } from 'react'
import { useSelector } from 'react-redux'
import { Link } from 'react-router-dom'
import { User } from 'src/types'
import { getAuthenticatedUser } from 'src/selectors/authentication'
import { AppState, User } from 'src/types'
import './user-info.scss'
interface Props {
authenticated: boolean
user?: User
}
const UserInfo: FC = () => {
const user = useSelector<AppState, User | undefined>(getAuthenticatedUser)
const UserInfo: FC<Props> = ({ authenticated, user }) => {
const hasAvatar = authenticated && user && user.imageUrl
const hasAvatar = user && user.imageUrl
const imageUrl = hasAvatar ? user!.imageUrl : undefined
const name = () => {
@ -31,7 +29,7 @@ const UserInfo: FC<Props> = ({ authenticated, user }) => {
}
const content = () => {
if (authenticated && user) {
if (user) {
return (
<div className="media-content">
<div className="content">

15
src/components/user-info/index.ts

@ -1,15 +0,0 @@
import { connect } from 'react-redux'
import { getAuthenticated, getAuthenticatedUser } from 'src/selectors/authentication'
import { AppState } from 'src/types'
import UserInfo from './user-info'
const mapStateToProps = (state: AppState) => ({
authenticated: getAuthenticated(state),
user: getAuthenticatedUser(state),
})
export default connect(
mapStateToProps
)(UserInfo)

3
src/components/user-info/user-info.scss

@ -1,3 +0,0 @@
article#user-info {
padding: 20px;
}

14
src/reducers/directory.ts → src/reducers/groups.ts

@ -1,21 +1,21 @@
import { Reducer } from 'redux'
import { DirectoryActions } from '../actions/directory'
import { DirectoryState } from '../types'
import { GroupsActions } from '../actions/groups'
import { GroupsState } from '../types'
const initialState: DirectoryState = {
const initialState: GroupsState = {
groups: [],
continuation: undefined,
}
const reducer: Reducer<DirectoryState, DirectoryActions> = (state = initialState, action) => {
const reducer: Reducer<GroupsState, GroupsActions> = (state = initialState, action) => {
switch (action.type) {
case 'DIRECTORY_SET_GROUPS':
case 'GROUPS_SET_GROUPS':
return {
...state,
groups: action.payload,
}
case 'DIRECTORY_APPEND_GROUPS':
case 'GROUPS_APPEND_GROUPS':
return {
...state,
groups: [
@ -23,7 +23,7 @@ const reducer: Reducer<DirectoryState, DirectoryActions> = (state = initialState
...action.payload,
],
}
case 'DIRECTORY_SET_CONTINUATION':
case 'GROUPS_SET_CONTINUATION':
return {
...state,
continuation: action.payload,

18
src/selectors/apps.ts

@ -0,0 +1,18 @@
import { createSelector } from 'reselect'
import { denormalize } from 'src/utils/normalization'
import { getEntityStore } from './entities'
import { getAuthenticatedUserId } from './authentication'
import { EntityType, App } from 'src/types'
export const getSelfApps = createSelector(
[getEntityStore, getAuthenticatedUserId],
(entities, userId) => {
const apps = entities[EntityType.App]
if (!apps) return []
return denormalize(Object.values(apps).filter(app => app.userId === userId).map(user => user.id), EntityType.User, entities) as App[]
}
)

2
src/selectors/directory.ts → src/selectors/groups.ts

@ -4,7 +4,7 @@ import { getEntityStore } from './entities'
import { denormalize } from 'src/utils/normalization'
import { AppState, Group, User, EntityType, GroupLog, Invitation } from 'src/types'
export const getGroupIds = (state: AppState) => state.directory.groups
export const getGroupIds = (state: AppState) => state.groups.groups
export const getGroups = createSelector(
[getEntityStore, getGroupIds],

4
src/store/index.ts

@ -2,9 +2,9 @@ import { createStore, combineReducers, applyMiddleware } from 'redux'
import { AppState } from '../types'
import authentication from '../reducers/authentication'
import directory from '../reducers/directory'
import entities from '../reducers/entities'
import forms from '../reducers/forms'
import groups from '../reducers/groups'
import menu from '../reducers/menu'
import notifications from '../reducers/notifications'
import registration from '../reducers/registration'
@ -16,9 +16,9 @@ import thunk from 'redux-thunk'
const store = createStore(
combineReducers<AppState>({
authentication,
directory,
entities,
forms,
groups,
menu,
notifications,
registration,

43
src/components/app/app.scss → src/styles/app.scss

@ -19,21 +19,21 @@ $primary: $blue;
$body-background-color: $white-ter;
$body-size: 14px;
@import "../../../node_modules/bulma/sass/utilities/_all.sass";
@import "../../../node_modules/bulma/sass/base/_all.sass";
@import "../../../node_modules/bulma/sass/form/_all.sass";
@import "../../../node_modules/bulma/sass/grid/columns.sass";
@import "../../../node_modules/bulma/sass/elements/button.sass";
@import "../../../node_modules/bulma/sass/elements/icon.sass";
@import "../../../node_modules/bulma/sass/elements/notification.sass";
@import "../../../node_modules/bulma/sass/elements/other.sass";
@import "../../../node_modules/bulma/sass/elements/table.sass";
@import "../../../node_modules/bulma/sass/elements/tag.sass";
@import "../../../node_modules/bulma/sass/elements/title.sass";
@import "../../../node_modules/bulma/sass/layout/hero.sass";
@import "../../../node_modules/bulma/sass/components/level.sass";
@import "../../../node_modules/bulma/sass/components/media.sass";
@import "../../../node_modules/bulma/sass/components/tabs.sass";
@import "../../node_modules/bulma/sass/utilities/_all.sass";
@import "../../node_modules/bulma/sass/base/_all.sass";
@import "../../node_modules/bulma/sass/form/_all.sass";
@import "../../node_modules/bulma/sass/grid/columns.sass";
@import "../../node_modules/bulma/sass/elements/button.sass";
@import "../../node_modules/bulma/sass/elements/icon.sass";
@import "../../node_modules/bulma/sass/elements/notification.sass";
@import "../../node_modules/bulma/sass/elements/other.sass";
@import "../../node_modules/bulma/sass/elements/table.sass";
@import "../../node_modules/bulma/sass/elements/tag.sass";
@import "../../node_modules/bulma/sass/elements/title.sass";
@import "../../node_modules/bulma/sass/layout/hero.sass";
@import "../../node_modules/bulma/sass/components/level.sass";
@import "../../node_modules/bulma/sass/components/media.sass";
@import "../../node_modules/bulma/sass/components/tabs.sass";
div#main-menu {
background: linear-gradient(135deg, $primary, darken($primary, 20%));
@ -69,7 +69,7 @@ div.centered-content {
}
}
.centered-content-narrow {
div.centered-content-narrow {
@extend div.centered-content;
width: 75%;
}
@ -86,6 +86,13 @@ footer {
padding: $size-normal;
}
div#notification-container {
bottom: 10px;
position: absolute;
left: 10px;
width: 40%;
}
div.group-list-item {
background-color: $white;
border-radius: 15px;
@ -107,3 +114,7 @@ div.invitation-options {
div.invitation-options > div {
margin-right: 20px;
}
article#user-info {
padding: 20px;
}

0
src/components/spinner/spinner.scss → src/styles/spinner.scss

29
src/types/entities.ts

@ -3,6 +3,7 @@ export enum EntityType {
Group = 'groups',
Log = 'logs',
Invitation = 'invitations',
App = 'apps',
}
export enum GroupMembershipType {
@ -22,12 +23,19 @@ export type Group = Entity & {
membership?: GroupMembershipType
}
export type Installation = {
app: App
settings: object
created: number
}
export type User = Entity & {
name: string
group?: Group
about?: string
imageUrl?: string
coverImageUrl?: string
installations: Installation[]
}
export type GroupLog = Entity & {
@ -41,6 +49,27 @@ export type Invitation = Entity & {
expires: number
}
export type App = Entity & {
version: string
name: string
imageUrl?: string
coverImageUrl?: string
about?: string
websiteUrl?: string
companyName?: string
composerUrl?: string
composerSchema?: object
rendererUrl?: string
rendererSchema?: object
initCallbackUrl?: string
composeCallbackUrl?: string
rating: number
publicKey: string
active: boolean
updated: number
created: number
}
export interface EntityCollection {
[id: string]: Entity
}

6
src/types/store.ts

@ -21,6 +21,8 @@ export enum RequestKey {
FetchGroupLogs = 'fetch_group_logs',
CreateInvitation = 'create_invitation',
FetchInvitations = 'fetch_invitations',
FetchApps = 'fetch_apps',
FetchSelfApps = 'fetch_self_apps',
}
export type FormValue = string | number | boolean
@ -75,7 +77,7 @@ export interface FormsState {
notification?: FormNotification
}
export interface DirectoryState {
export interface GroupsState {
groups: string[]
continuation?: string
}
@ -90,9 +92,9 @@ export type EntitiesState = EntityStore
export interface AppState {
authentication: AuthenticationState
directory: DirectoryState
entities: EntitiesState
forms: FormsState
groups: GroupsState
menu: MenuState
notifications: NotificationsState
registration: RegistrationState

46
src/utils/normalization.ts

@ -5,6 +5,7 @@ import {
User,
Invitation,
GroupLog,
App,
} from '../types'
import compact from 'lodash/compact'
@ -14,6 +15,14 @@ export interface NormalizeResult {
entities: EntityStore
}
type NormalizedInstallation = Entity & {
app: string
}
type NormalizedUser = Entity & {
installations: NormalizedInstallation[]
}
function set(type: EntityType, store: EntityStore, entity?: Entity): string | undefined {
if (!entity) return
@ -48,6 +57,12 @@ export function normalize(entities: Entity[], type: EntityType): NormalizeResult
return set(type, newStore, {
...user,
group: set(EntityType.Group, newStore, user.group),
installations: user.installations.map(installation => {
return {
...installation,
app: set(EntityType.App, newStore, installation.app),
}
})
})
})
@ -76,6 +91,17 @@ export function normalize(entities: Entity[], type: EntityType): NormalizeResult
})
})
break
case EntityType.App:
keys = entities.map(entity => {
const app = entity as App
return set(type, newStore, {
...app,
user: set(EntityType.User, newStore, app.user)
})
})
break
}
@ -89,12 +115,18 @@ export function denormalize(keys: string[], type: EntityType, store: EntityStore
const entities = keys.map(key => {
switch (type) {
case EntityType.User:
const user = get(type, store, key)
const user = get(type, store, key) as NormalizedUser
if (!user) return
return {
...user,
group: get(EntityType.Group, store, user.group),
installations: user.installations.map(installation => {
return {
...installation,
app: get(EntityType.App, store, installation.app),
}
})
}
case EntityType.Group:
return get(type, store, key)
@ -104,7 +136,7 @@ export function denormalize(keys: string[], type: EntityType, store: EntityStore
return {
...invitation,
user: get(EntityType.User, store, invitation.user)
user: get(EntityType.User, store, invitation.user),
}
case EntityType.Log:
const log = get(type, store, key)
@ -112,7 +144,15 @@ export function denormalize(keys: string[], type: EntityType, store: EntityStore
return {
...log,
user: get(EntityType.User, store, log.user)
user: get(EntityType.User, store, log.user),
}
case EntityType.App:
const app = get(type, store, key)
if (!app) return
return {
...app,
user: get(EntityType.User, store, app.user),
}
}
})

Loading…
Cancel
Save