Browse Source

Initial commit from /verdaccio/src/webui

master
ポール ウェッブ 7 months ago
commit
de0be98d68
68 changed files with 2420 additions and 0 deletions
  1. 30
    0
      .eslintrc
  2. 165
    0
      app.js
  3. 1
    0
      components/Footer/earth.svg
  4. 2
    0
      components/Footer/flags/brazil-1f1e7-1f1f7.svg
  5. 2
    0
      components/Footer/flags/china-1f1e8-1f1f3.svg
  6. 2
    0
      components/Footer/flags/india-1f1ee-1f1f3.svg
  7. 2
    0
      components/Footer/flags/nicaragua-1f1f3-1f1ee.svg
  8. 2
    0
      components/Footer/flags/pakistan-1f1f5-1f1f0.svg
  9. 2
    0
      components/Footer/flags/spain-1f1ea-1f1f8.svg
  10. 82
    0
      components/Footer/footer.scss
  11. 63
    0
      components/Footer/index.js
  12. 1
    0
      components/Footer/logo.svg
  13. 49
    0
      components/Header/header.scss
  14. 68
    0
      components/Header/index.js
  15. BIN
      components/Header/logo.png
  16. 29
    0
      components/Help/help.scss
  17. 42
    0
      components/Help/index.js
  18. 155
    0
      components/Login/index.js
  19. 18
    0
      components/NoItems/index.js
  20. 3
    0
      components/NoItems/noItems.scss
  21. 15
    0
      components/NotFound/404.scss
  22. 23
    0
      components/NotFound/index.js
  23. 52
    0
      components/Package/index.js
  24. 88
    0
      components/Package/package.scss
  25. 27
    0
      components/PackageDetail/index.js
  26. 16
    0
      components/PackageDetail/packageDetail.scss
  27. 77
    0
      components/PackageList/index.js
  28. 23
    0
      components/PackageList/packageList.scss
  29. 25
    0
      components/PackageSidebar/Module/index.jsx
  30. 24
    0
      components/PackageSidebar/Module/style.scss
  31. 11
    0
      components/PackageSidebar/ModuleContentPlaceholder/index.jsx
  32. 8
    0
      components/PackageSidebar/ModuleContentPlaceholder/style.scss
  33. 97
    0
      components/PackageSidebar/index.jsx
  34. 50
    0
      components/PackageSidebar/modules/Dependencies/index.jsx
  35. 13
    0
      components/PackageSidebar/modules/Dependencies/style.scss
  36. 37
    0
      components/PackageSidebar/modules/Infos/index.jsx
  37. 21
    0
      components/PackageSidebar/modules/Infos/style.scss
  38. 45
    0
      components/PackageSidebar/modules/LastSync/index.jsx
  39. 13
    0
      components/PackageSidebar/modules/LastSync/style.scss
  40. 19
    0
      components/PackageSidebar/modules/Maintainers/MaintainerInfo/index.jsx
  41. 26
    0
      components/PackageSidebar/modules/Maintainers/MaintainerInfo/style.scss
  42. 122
    0
      components/PackageSidebar/modules/Maintainers/index.jsx
  43. 13
    0
      components/PackageSidebar/modules/Maintainers/style.scss
  44. 18
    0
      components/PackageSidebar/modules/PeerDependencies/index.jsx
  45. 15
    0
      components/Readme/index.js
  46. 6
    0
      components/Readme/readme.scss
  47. 35
    0
      components/Search/index.js
  48. 5
    0
      components/Search/search.scss
  49. 26
    0
      index.js
  50. 26
    0
      modules/detail/detail.scss
  51. 83
    0
      modules/detail/index.jsx
  52. 123
    0
      modules/home/index.js
  53. 46
    0
      router.js
  54. 22
    0
      styles/core.scss
  55. 37
    0
      styles/global.scss
  56. 2
    0
      styles/main.scss
  57. 48
    0
      styles/mixins.scss
  58. 64
    0
      styles/variables.scss
  59. BIN
      template/favicon.ico
  60. 16
    0
      template/index.html
  61. 3
    0
      utils/__setPublicPath__.js
  62. 59
    0
      utils/api.js
  63. 24
    0
      utils/asyncComponent.js
  64. 73
    0
      utils/login.js
  65. 10
    0
      utils/logo.js
  66. 92
    0
      utils/package.js
  67. 12
    0
      utils/storage.js
  68. 12
    0
      utils/url.js

+ 30
- 0
.eslintrc View File

@@ -0,0 +1,30 @@
1
+{
2
+  "env": {
3
+    "browser": true,
4
+    "node": true,
5
+    "jest": true,
6
+    "es6": true
7
+  },
8
+  "globals": {
9
+    "__DEBUG__": true
10
+  },
11
+  "rules": {
12
+    "require-jsdoc": 0,
13
+    "no-console": 2,
14
+    "no-unused-vars": [
15
+      2,
16
+      {
17
+        "vars": "all",
18
+        "args": "all"
19
+      }
20
+    ],
21
+    "comma-dangle": 0,
22
+    "semi": 1,
23
+    "react/no-danger-with-children": 1,
24
+    "react/no-string-refs": 1,
25
+    "react/prefer-es6-class": [
26
+      2,
27
+      "always"
28
+    ]
29
+  }
30
+}

+ 165
- 0
app.js View File

@@ -0,0 +1,165 @@
1
+import React, {Component} from 'react';
2
+import isNil from 'lodash/isNil';
3
+import 'element-theme-default';
4
+import {i18n} from 'element-react';
5
+import locale from 'element-react/src/locale/lang/en';
6
+
7
+import storage from './utils/storage';
8
+import logo from './utils/logo';
9
+import {makeLogin, isTokenExpire} from './utils/login';
10
+
11
+import Header from './components/Header';
12
+import Footer from './components/Footer';
13
+import LoginModal from './components/Login';
14
+
15
+i18n.use(locale);
16
+
17
+import Route from './router';
18
+
19
+import './styles/main.scss';
20
+import 'normalize.css';
21
+
22
+export default class App extends Component {
23
+  state = {
24
+    error: {},
25
+    logoUrl: '',
26
+    user: {},
27
+    scope: (window.VERDACCIO_SCOPE) ? `${window.VERDACCIO_SCOPE}:` : '',
28
+    showLoginModal: false,
29
+    isUserLoggedIn: false
30
+  };
31
+
32
+  constructor(props) {
33
+    super(props);
34
+    this.handleLogout = this.handleLogout.bind(this);
35
+    this.toggleLoginModal = this.toggleLoginModal.bind(this);
36
+    this.doLogin = this.doLogin.bind(this);
37
+    this.loadLogo = this.loadLogo.bind(this);
38
+    this.isUserAlreadyLoggedIn = this.isUserAlreadyLoggedIn.bind(this);
39
+  }
40
+
41
+  componentDidMount() {
42
+    this.loadLogo();
43
+    this.isUserAlreadyLoggedIn();
44
+  }
45
+
46
+  isUserAlreadyLoggedIn() {
47
+    // checks for token validity
48
+    const token = storage.getItem('token');
49
+    const username = storage.getItem('username');
50
+
51
+    if (isTokenExpire(token) || isNil(username)) {
52
+      this.handleLogout();
53
+    } else {
54
+      this.setState({
55
+        user: {username, token},
56
+        isUserLoggedIn: true
57
+      });
58
+    }
59
+  }
60
+
61
+  async loadLogo() {
62
+    const logoUrl = await logo();
63
+    this.setState({logoUrl});
64
+  }
65
+
66
+  /**
67
+   * Toggles the login modal
68
+   * Required by: <LoginModal /> <Header />
69
+   */
70
+  toggleLoginModal() {
71
+    this.setState((prevState) => ({
72
+      showLoginModal: !prevState.showLoginModal,
73
+      error: {}
74
+    }));
75
+  }
76
+
77
+  /**
78
+   * handles login
79
+   * Required by: <Header />
80
+   */
81
+  async doLogin(usernameValue, passwordValue) {
82
+    const {username, token, error} = await makeLogin(
83
+      usernameValue,
84
+      passwordValue
85
+    );
86
+
87
+    if (username && token) {
88
+      this.setState({
89
+        user: {
90
+          username,
91
+          token
92
+        }
93
+      });
94
+      storage.setItem('username', username);
95
+      storage.setItem('token', token);
96
+      // close login modal after successful login
97
+      // set isUserLoggedin to true
98
+      this.setState({
99
+        isUserLoggedIn: true,
100
+        showLoginModal: false
101
+      });
102
+    }
103
+
104
+    if (error) {
105
+      this.setState({
106
+        user: {},
107
+        error
108
+      });
109
+    }
110
+  }
111
+
112
+  /**
113
+   * Logouts user
114
+   * Required by: <Header />
115
+   */
116
+  handleLogout() {
117
+    storage.removeItem('username');
118
+    storage.removeItem('token');
119
+    this.setState({
120
+      user: {},
121
+      isUserLoggedIn: false
122
+    });
123
+  }
124
+
125
+  renderHeader() {
126
+    const {
127
+      logoUrl,
128
+      user,
129
+      scope,
130
+    } = this.state;
131
+    return <Header
132
+      logo={logoUrl}
133
+      username={user.username}
134
+      scope={scope}
135
+      toggleLoginModal={this.toggleLoginModal}
136
+      handleLogout={this.handleLogout}
137
+    />;
138
+  }
139
+
140
+  renderLoginModal() {
141
+    const {
142
+      error,
143
+      showLoginModal
144
+    } = this.state;
145
+    return <LoginModal
146
+      visibility={showLoginModal}
147
+      error={error}
148
+      onChange={this.setUsernameAndPassword}
149
+      onCancel={this.toggleLoginModal}
150
+      onSubmit={this.doLogin}
151
+    />;
152
+  }
153
+
154
+  render() {
155
+    const {isUserLoggedIn} = this.state;
156
+    return (
157
+      <div className="page-full-height">
158
+          {this.renderHeader()}
159
+          {this.renderLoginModal()}
160
+          <Route isUserLoggedIn={isUserLoggedIn} />
161
+        <Footer />
162
+      </div>
163
+    );
164
+  }
165
+}

+ 1
- 0
components/Footer/earth.svg View File

@@ -0,0 +1 @@
1
+<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 45" style="enable-background:new 0 0 45 45;" xml:space="preserve" version="1.1" id="svg2"><metadata id="metadata8"><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/></cc:Work></rdf:RDF></metadata><defs id="defs6"><clipPath id="clipPath16" clipPathUnits="userSpaceOnUse"><path id="path18" d="M 0,36 36,36 36,0 0,0 0,36 Z"/></clipPath><clipPath id="clipPath28" clipPathUnits="userSpaceOnUse"><path id="path30" d="M 18,36 C 8.059,36 0,27.941 0,18 l 0,0 C 0,8.059 8.059,0 18,0 l 0,0 c 9.941,0 18,8.059 18,18 l 0,0 c 0,9.941 -8.059,18 -18,18 z"/></clipPath></defs><g transform="matrix(1.25,0,0,-1.25,0,45)" id="g10"><g id="g12"><g clip-path="url(#clipPath16)" id="g14"><g transform="translate(36,18)" id="g20"><path id="path22" style="fill:#88c9f9;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,-9.941 -8.059,-18 -18,-18 -9.941,0 -18,8.059 -18,18 0,9.941 8.059,18 18,18 C -8.059,18 0,9.941 0,0"/></g></g></g><g id="g24"><g clip-path="url(#clipPath28)" id="g26"><g transform="translate(3.6274,28.9521)" id="g32"><path id="path34" style="fill:#5c913b;fill-opacity:1;fill-rule:nonzero;stroke:none" d="M 0,0 C -0.451,2.93 2.195,4.156 3.607,4.469 5.019,4.784 6.383,5.09 6.54,4.464 6.696,3.836 6.851,3.003 7.713,3.316 8.575,3.63 10.756,3.875 11.776,4.658 12.795,5.441 14.02,5.445 15.04,5.131 16.059,4.818 18.917,4.904 18.29,3.964 17.663,3.023 16.465,3.137 15.839,2.04 15.212,0.941 16.011,0.214 16.873,0.214 c 0.865,0 1.709,-0.135 2.259,0.727 0.549,0.863 -0.382,2.463 0.325,2.357 0.706,-0.106 1.477,-0.866 2.03,-2.043 0.547,-1.176 1.408,-0.47 1.723,-1.176 0.313,-0.705 2.04,-2.039 1.177,-1.804 -0.864,0.236 -1.726,0.392 -1.961,-0.47 -0.236,-0.863 0.389,-1.726 -0.236,-1.647 -0.627,0.079 -0.861,-0.089 -1.725,-0.004 -0.862,0.083 -1.333,0.631 -2.039,-0.545 -0.705,-1.175 -1.254,-1.961 -1.567,-2.509 -0.315,-0.549 -0.785,-0.861 -0.55,-1.96 0.235,-1.099 -0.628,-0.785 -0.628,0.156 0,0.94 -0.548,1.098 -1.253,0.942 -0.706,-0.157 -1.803,-0.313 -1.724,-1.098 0.077,-0.784 -0.315,-1.725 0.313,-2.352 0.627,-0.629 1.33,0.076 1.723,-0.158 0.393,-0.237 1.525,-0.023 1.133,-0.416 -0.393,-0.391 -1.76,-0.881 -0.976,-1.509 0.786,-0.626 1.578,-0.829 1.893,-0.907 0.313,-0.08 0.062,0.774 1.083,1.166 1.017,0.392 2.608,1.29 3,0.584 0.391,-0.705 0.338,-0.595 1.75,-0.75 1.41,-0.156 1.79,-0.585 2.417,-1.917 0.626,-1.333 0.446,-1.192 1.462,-1.581 1.021,-0.393 1.678,-0.222 0.737,-1.086 -0.941,-0.86 -1.651,-0.814 -2.199,-1.833 -0.55,-1.017 -0.153,-1.731 -1.25,-2.75 -1.098,-1.019 -2.242,-1.876 -3.417,-2.583 -0.618,-0.37 -2.162,-2.07 -3.083,-2.667 -0.834,-0.541 -1.083,0 -1.083,0 0,0 0.256,1.667 0.964,2.372 0.704,0.705 1.105,3.344 0.869,4.128 -0.234,0.783 -1.36,1.02 -1.75,1.333 -0.392,0.312 -1.417,1.548 -1.417,2.334 0,0.784 1.71,2.809 1.71,2.809 0.218,-1.088 -1.039,0.329 -1.627,0.524 -0.47,0.157 -1.542,1.656 -2.459,1.814 -0.916,0.159 -1.363,0.7 -2.068,1.25 -0.706,0.549 -2.431,1.332 -2.353,2.195 0.079,0.862 -1.725,1.568 -2.038,1.568 -0.314,0 -1.019,0 -1.647,1.098 -0.627,1.098 -1.725,2.196 -1.411,2.979 0.313,0.784 0.392,1.727 0.234,2.589 C 3.058,0.236 1.882,0.55 1.647,0.315 1.412,0.079 0.158,-1.02 0,0"/></g></g></g></g></svg>

+ 2
- 0
components/Footer/flags/brazil-1f1e7-1f1f7.svg View File

@@ -0,0 +1,2 @@
1
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 45" style="enable-background:new 0 0 45 45;" xml:space="preserve" version="1.1" id="svg2"><metadata id="metadata8"><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/></cc:Work></rdf:RDF></metadata><defs id="defs6"><clipPath id="clipPath16" clipPathUnits="userSpaceOnUse"><path id="path18" d="M 0,36 36,36 36,0 0,0 0,36 Z"/></clipPath></defs><g transform="matrix(1.25,0,0,-1.25,0,45)" id="g10"><g id="g12"><g clip-path="url(#clipPath16)" id="g14"><g transform="translate(36,9)" id="g20"><path id="path22" style="fill:#009b3a;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,-2.209 -1.791,-4 -4,-4 l -28,0 c -2.209,0 -4,1.791 -4,4 l 0,18 c 0,2.209 1.791,4 4,4 l 28,0 c 2.209,0 4,-1.791 4,-4 L 0,0 Z"/></g><g transform="translate(32.7275,18)" id="g24"><path id="path26" style="fill:#fedf01;fill-opacity:1;fill-rule:nonzero;stroke:none" d="M 0,0 -14.728,-11.124 -29.456,0 -14.728,11.125 0,0 Z"/></g><g transform="translate(24.4336,18.0762)" id="g28"><path id="path30" style="fill:#002776;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,3.567 -2.892,6.458 -6.458,6.458 -3.567,0 -6.458,-2.891 -6.458,-6.458 0,-3.566 2.891,-6.458 6.458,-6.458 C -2.892,-6.458 0,-3.566 0,0"/></g><g transform="translate(12.2769,21.1128)" id="g32"><path id="path34" style="fill:#cbe9d4;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c -0.332,-0.621 -0.558,-1.303 -0.672,-2.023 3.994,0.29 9.417,-1.892 11.744,-4.596 0.402,0.604 0.7,1.281 0.882,2.004 C 9.083,-1.806 4.038,0.016 0,0"/></g><path id="path36" style="fill:#88c9f9;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 13,16.767 -1,0 0,1 1,0 0,-1 z"/><path id="path38" style="fill:#88c9f9;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 14,14.767 -1,0 0,1 1,0 0,-1 z"/><path id="path40" style="fill:#55acee;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 16,16.767 -1,0 0,1 1,0 0,-1 z"/><path id="path42" style="fill:#55acee;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 18,15.767 -1,0 0,1 1,0 0,-1 z"/><path id="path44" style="fill:#55acee;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 22,13.767 -1,0 0,1 1,0 0,-1 z"/><path id="path46" style="fill:#55acee;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 19,12.767 -1,0 0,1 1,0 0,-1 z"/><path id="path48" style="fill:#55acee;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 22,18.767 -1,0 0,1 1,0 0,-1 z"/><path id="path50" style="fill:#3b88c3;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 20,14.767 -1,0 0,1 1,0 0,-1 z"/></g></g></g></svg>

+ 2
- 0
components/Footer/flags/china-1f1e8-1f1f3.svg View File

@@ -0,0 +1,2 @@
1
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 45" style="enable-background:new 0 0 45 45;" xml:space="preserve" version="1.1" id="svg2"><metadata id="metadata8"><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/></cc:Work></rdf:RDF></metadata><defs id="defs6"><clipPath id="clipPath16" clipPathUnits="userSpaceOnUse"><path id="path18" d="M 0,36 36,36 36,0 0,0 0,36 Z"/></clipPath></defs><g transform="matrix(1.25,0,0,-1.25,0,45)" id="g10"><g id="g12"><g clip-path="url(#clipPath16)" id="g14"><g transform="translate(36,9)" id="g20"><path id="path22" style="fill:#de2910;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,-2.209 -1.791,-4 -4,-4 l -28,0 c -2.209,0 -4,1.791 -4,4 l 0,18 c 0,2.209 1.791,4 4,4 l 28,0 c 2.209,0 4,-1.791 4,-4 L 0,0 Z"/></g><g transform="translate(7,25.0488)" id="g24"><path id="path26" style="fill:#ffde02;fill-opacity:1;fill-rule:nonzero;stroke:none" d="M 0,0 0.929,-2.67 3.755,-2.729 1.502,-4.436 2.321,-7.143 0,-5.528 -2.321,-7.143 -1.502,-4.436 -3.755,-2.729 -0.929,-2.67 0,0 Z"/></g><g transform="translate(13,28.4722)" id="g28"><path id="path30" style="fill:#ffde02;fill-opacity:1;fill-rule:nonzero;stroke:none" d="M 0,0 0.34,-0.688 1.099,-0.798 0.55,-1.334 0.679,-2.09 0,-1.733 -0.679,-2.09 l 0.13,0.756 -0.55,0.536 0.76,0.11 L 0,0 Z"/></g><g transform="translate(15,24.4722)" id="g32"><path id="path34" style="fill:#ffde02;fill-opacity:1;fill-rule:nonzero;stroke:none" d="M 0,0 0.34,-0.688 1.099,-0.798 0.55,-1.334 0.679,-2.09 0,-1.733 -0.679,-2.09 l 0.13,0.756 -0.55,0.536 0.76,0.11 L 0,0 Z"/></g><g transform="translate(15,20.4722)" id="g36"><path id="path38" style="fill:#ffde02;fill-opacity:1;fill-rule:nonzero;stroke:none" d="M 0,0 0.34,-0.688 1.099,-0.798 0.55,-1.334 0.679,-2.09 0,-1.733 -0.679,-2.09 l 0.13,0.756 -0.55,0.536 0.76,0.11 L 0,0 Z"/></g><g transform="translate(13,16.4727)" id="g40"><path id="path42" style="fill:#ffde02;fill-opacity:1;fill-rule:nonzero;stroke:none" d="M 0,0 0.34,-0.689 1.099,-0.799 0.55,-1.334 0.679,-2.091 0,-1.734 l -0.679,-0.357 0.13,0.757 -0.55,0.535 0.76,0.11 L 0,0 Z"/></g></g></g></g></svg>

+ 2
- 0
components/Footer/flags/india-1f1ee-1f1f3.svg View File

@@ -0,0 +1,2 @@
1
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 45" style="enable-background:new 0 0 45 45;" xml:space="preserve" version="1.1" id="svg2"><metadata id="metadata8"><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/></cc:Work></rdf:RDF></metadata><defs id="defs6"><clipPath id="clipPath16" clipPathUnits="userSpaceOnUse"><path id="path18" d="M 0,36 36,36 36,0 0,0 0,36 Z"/></clipPath><clipPath id="clipPath42" clipPathUnits="userSpaceOnUse"><path id="path44" d="m 15,21 6,0 0,-6 -6,0 0,6 z"/></clipPath></defs><g transform="matrix(1.25,0,0,-1.25,0,45)" id="g10"><g id="g12"><g clip-path="url(#clipPath16)" id="g14"><g transform="translate(0,9)" id="g20"><path id="path22" style="fill:#138808;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,-2.209 1.791,-4 4,-4 l 28,0 c 2.209,0 4,1.791 4,4 L 36,4 0,4 0,0 Z"/></g><path id="path24" style="fill:#eeeeee;fill-opacity:1;fill-rule:nonzero;stroke:none" d="M 0,13 36,13 36,23 0,23 0,13 Z"/><g transform="translate(36,23)" id="g26"><path id="path28" style="fill:#ff9933;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 0,4 c 0,2.209 -1.791,4 -4,4 l -28,0 c -2.209,0 -4,-1.791 -4,-4 l 0,-4 36,0 z"/></g><g transform="translate(22,18)" id="g30"><path id="path32" style="fill:#000080;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,2.21 -1.791,4.001 -4.001,4.001 -2.209,0 -4,-1.791 -4,-4.001 0,-2.209 1.791,-4 4,-4 C -1.791,-4 0,-2.209 0,0"/></g><g transform="translate(21,18)" id="g34"><path id="path36" style="fill:#eeeeee;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,1.657 -1.344,3.001 -3.001,3.001 -1.657,0 -3,-1.344 -3,-3.001 0,-1.657 1.343,-3 3,-3 C -1.344,-3 0,-1.657 0,0"/></g><g id="g38"><g id="g40"/><g id="g46"><g style="opacity:0.60000598" id="g48" clip-path="url(#clipPath42)"><g id="g50" transform="translate(18,21)"><path id="path52" style="fill:#000080;fill-opacity:1;fill-rule:nonzero;stroke:none" d="M 0,0 0.146,-2.264 1.148,-0.229 0.417,-2.376 2.121,-0.878 0.624,-2.583 2.771,-1.852 0.736,-2.854 3,-3 0.736,-3.146 2.771,-4.147 0.624,-3.417 2.121,-5.121 0.417,-3.624 1.148,-5.771 0.146,-3.736 0,-6 l -0.146,2.264 -1.002,-2.035 0.731,2.147 -1.705,-1.497 1.498,1.704 -2.147,-0.73 2.035,1.001 L -3,-3 l 2.264,0.146 -2.035,1.002 2.147,-0.731 -1.498,1.705 1.705,-1.498 -0.731,2.147 1.002,-2.035 L 0,0 Z"/></g></g></g></g><g transform="translate(17,18)" id="g54"><path id="path56" style="fill:#000080;fill-opacity:1;fill-rule:nonzero;stroke:none" d="M 0,0 C 0,0.552 0.448,1 1,1 1.552,1 2,0.552 2,0 2,-0.552 1.552,-1 1,-1 0.448,-1 0,-0.552 0,0"/></g></g></g></g></svg>

+ 2
- 0
components/Footer/flags/nicaragua-1f1f3-1f1ee.svg
File diff suppressed because it is too large
View File


+ 2
- 0
components/Footer/flags/pakistan-1f1f5-1f1f0.svg View File

@@ -0,0 +1,2 @@
1
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 45" style="enable-background:new 0 0 45 45;" xml:space="preserve" version="1.1" id="svg2"><metadata id="metadata8"><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/></cc:Work></rdf:RDF></metadata><defs id="defs6"><clipPath id="clipPath16" clipPathUnits="userSpaceOnUse"><path id="path18" d="M 0,36 36,36 36,0 0,0 0,36 Z"/></clipPath></defs><g transform="matrix(1.25,0,0,-1.25,0,45)" id="g10"><g id="g12"><g clip-path="url(#clipPath16)" id="g14"><g transform="translate(27.6102,20.9519)" id="g20"><path id="path22" style="fill:#004600;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 -1.213,-2.022 -0.208,2.349 -2.298,0.528 2.17,0.924 -0.207,2.349 1.548,-1.779 2.17,0.924 L 0.75,1.25 2.298,-0.529 0,0 Z m -5.11,-10.424 c -4.142,0 -7.5,3.358 -7.5,7.5 0,3.72 2.711,6.798 6.263,7.389 -2.221,-1.034 -3.763,-3.279 -3.763,-5.889 0,-3.59 2.91,-6.5 6.5,-6.5 2.61,0 4.855,1.542 5.889,3.763 -0.591,-3.552 -3.669,-6.263 -7.389,-6.263 m 9.5,20.472 -23,0 0,-26 23,0 c 2.209,0 4,1.791 4,4 l 0,18 c 0,2.209 -1.791,4 -4,4"/></g><g transform="translate(4,31)" id="g24"><path id="path26" style="fill:#eeeeee;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c -2.209,0 -4,-1.791 -4,-4 l 0,-18 c 0,-2.209 1.791,-4 4,-4 l 5,0 0,26 -5,0 z"/></g><g transform="translate(29.5723,24.2247)" id="g28"><path id="path30" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 -2.17,-0.924 -1.548,1.779 0.207,-2.349 -2.17,-0.923 2.298,-0.528 0.208,-2.35 1.213,2.022 2.298,-0.529 -1.548,1.779 L 0,0 Z"/></g><g transform="translate(24,13.0276)" id="g32"><path id="path34" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c -3.59,0 -6.5,2.91 -6.5,6.5 0,2.611 1.543,4.856 3.763,5.89 C -6.289,11.799 -9,8.72 -9,5 c 0,-4.142 3.358,-7.5 7.5,-7.5 3.72,0 6.799,2.711 7.39,6.263 C 4.856,1.543 2.611,0 0,0"/></g></g></g></g></svg>

+ 2
- 0
components/Footer/flags/spain-1f1ea-1f1f8.svg View File

@@ -0,0 +1,2 @@
1
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 45" style="enable-background:new 0 0 45 45;" xml:space="preserve" version="1.1" id="svg2"><metadata id="metadata8"><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/></cc:Work></rdf:RDF></metadata><defs id="defs6"><clipPath id="clipPath16" clipPathUnits="userSpaceOnUse"><path id="path18" d="M 0,36 36,36 36,0 0,0 0,36 Z"/></clipPath></defs><g transform="matrix(1.25,0,0,-1.25,0,45)" id="g10"><g id="g12"><g clip-path="url(#clipPath16)" id="g14"><g transform="translate(36,9)" id="g20"><path id="path22" style="fill:#c60a1d;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,-2.209 -1.791,-4 -4,-4 l -28,0 c -2.209,0 -4,1.791 -4,4 l 0,18 c 0,2.209 1.791,4 4,4 l 28,0 c 2.209,0 4,-1.791 4,-4 L 0,0 Z"/></g><path id="path24" style="fill:#ffc400;fill-opacity:1;fill-rule:nonzero;stroke:none" d="M 36,12 0,12 0,24 36,24 36,12 Z"/><g transform="translate(9,19)" id="g26"><path id="path28" style="fill:#ea596e;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 0,-3 c 0,-1.657 1.343,-3 3,-3 1.657,0 3,1.343 3,3 L 6,0 0,0 Z"/></g><path id="path30" style="fill:#f4a2b2;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 12,17 3,0 0,3 -3,0 0,-3 z"/><path id="path32" style="fill:#dd2e44;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 12,17 -3,0 0,3 3,0 0,-3 z"/><g transform="translate(15,21.5)" id="g34"><path id="path36" style="fill:#ea596e;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,-0.829 -1.343,-1.5 -3,-1.5 -1.657,0 -3,0.671 -3,1.5 0,0.829 1.343,1.5 3,1.5 1.657,0 3,-0.671 3,-1.5"/></g><g transform="translate(15,22.25)" id="g38"><path id="path40" style="fill:#ffac33;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,0.414 -1.343,0.75 -3,0.75 -1.657,0 -3,-0.336 -3,-0.75 0,-0.414 1.343,-0.75 3,-0.75 1.657,0 3,0.336 3,0.75"/></g><path id="path42" style="fill:#99aab5;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 7,13 1,0 0,7 -1,0 0,-7 z"/><path id="path44" style="fill:#99aab5;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 17,13 -1,0 0,7 1,0 0,-7 z"/><path id="path46" style="fill:#66757f;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 9,13 -3,0 0,1 3,0 0,-1 z"/><path id="path48" style="fill:#66757f;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 18,13 -3,0 0,1 3,0 0,-1 z"/><path id="path50" style="fill:#66757f;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 8,20 -1,0 0,1 1,0 0,-1 z"/><path id="path52" style="fill:#66757f;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 17,20 -1,0 0,1 1,0 0,-1 z"/></g></g></g></svg>

+ 82
- 0
components/Footer/footer.scss View File

@@ -0,0 +1,82 @@
1
+@import '../../styles/variables';
2
+@import '../../styles/mixins';
3
+
4
+.wrap {
5
+  margin-top: auto;
6
+  background: $snow;
7
+  @include border(top, 1px, solid, $greyGainsboro);
8
+}
9
+
10
+footer.footer {
11
+  display: flex;
12
+  margin-top: 0;
13
+  padding: 20px 0;
14
+  font-size: $font-size-sm;
15
+  height: 18px;
16
+  color: $nobel-01;
17
+
18
+  span {
19
+    display: inline-block;
20
+    height: 18px;
21
+    vertical-align: middle;
22
+  }
23
+
24
+  .tooltip {
25
+    align-items: center;
26
+    margin-left: 5px;
27
+    position: relative;
28
+    margin-top: -2px;
29
+    height: 20px;
30
+    background: $greyAthens;
31
+    padding: 1px 4px;
32
+    border-radius: 2px;
33
+    transform: scaleX(0);
34
+    transition: transform 0.3s cubic-bezier(.12,.76,.14,.99);
35
+    transform-origin: left;
36
+
37
+    &:before {
38
+      content: '';
39
+      position: absolute;
40
+      top: 3px;
41
+      left: -7px;
42
+      display: block;
43
+      width: 7px;
44
+      height: 15px;
45
+      background: $greyAthens;
46
+      background-image: linear-gradient(45deg, white, white 30%, transparent 30%), linear-gradient(-225deg, white, white 30%, transparent 30%);
47
+    }
48
+
49
+
50
+    img:not(:first-child) {
51
+      margin-left: 8px;
52
+    }
53
+  }
54
+
55
+  // Footer Hover
56
+  &.showAuthorsGeographic .tooltip,
57
+  &:hover .tooltip {
58
+    display: flex;
59
+    transform: scaleX(1);
60
+  }
61
+
62
+  .earth {
63
+    margin-left: 8px;
64
+    margin-right: 8px;
65
+  }
66
+
67
+  :global {
68
+    .emoji {
69
+      width: 18px;
70
+      height: 18px;
71
+    }
72
+  }
73
+
74
+  .right {
75
+    margin-left: auto;
76
+  }
77
+  .logo {
78
+    vertical-align: top;
79
+    height: 18px;
80
+    width: 20px;
81
+  }
82
+}

+ 63
- 0
components/Footer/index.js View File

@@ -0,0 +1,63 @@
1
+import React, {Component} from 'react';
2
+
3
+import classes from './footer.scss';
4
+import logo from './logo.svg';
5
+import earth from './earth.svg';
6
+
7
+// Vectors from Twitter Emoji (Open Source)
8
+import brazilFlag from './flags/brazil-1f1e7-1f1f7.svg';
9
+import chinaFlag from './flags/china-1f1e8-1f1f3.svg';
10
+import indiaFlag from './flags/india-1f1ee-1f1f3.svg';
11
+import nicaraguaFlag from './flags/nicaragua-1f1f3-1f1ee.svg';
12
+import pakistanFlag from './flags/pakistan-1f1f5-1f1f0.svg';
13
+import spainFlag from './flags/spain-1f1ea-1f1f8.svg';
14
+
15
+export default class Footer extends Component {
16
+  constructor(props) {
17
+    super(props);
18
+    this.handleEarthIconClick = this.handleEarthIconClick.bind(this);
19
+    this.state = {
20
+      showAuthorsGeographic: false
21
+    };
22
+  }
23
+
24
+  handleEarthIconClick() {
25
+    this.setState({
26
+      showAuthorsGeographic: true
27
+    });
28
+  }
29
+
30
+  render() {
31
+    return (
32
+      <div className={classes.wrap}>
33
+        <footer
34
+          className={`container ${classes.footer} ${this.state.showAuthorsGeographic && classes.showAuthorsGeographic}`}
35
+        >
36
+          <span>Made with&nbsp;</span>
37
+          <span>❤</span>
38
+          <span>&nbsp;on</span>
39
+          <img className={`${classes.earth} emoji`} src={earth} alt="Earth" onClick={this.handleEarthIconClick}/>
40
+          <div className={classes.tooltip}>
41
+            <img src={brazilFlag} alt="Brazil" title="Brazil" className="emoji"/>
42
+            <img src={chinaFlag} alt="China" title="China" className="emoji"/>
43
+            <img src={indiaFlag} alt="India" title="India" className="emoji"/>
44
+            <img src={nicaraguaFlag} alt="Nicaragua" title="Nicaragua" className="emoji"/>
45
+            <img src={pakistanFlag} alt="Pakistan" title="Pakistan" className="emoji"/>
46
+            <img src={spainFlag} alt="Spain" title="Spain" className="emoji"/>
47
+          </div>
48
+          {/* Countries are order by alphabets */}
49
+
50
+          <div className={classes.right}>
51
+            Powered by&nbsp;
52
+            { /* Can't switch to HTTPS due it hosted on GitHub Pages */ }
53
+            <a href="http://www.verdaccio.org/">
54
+              <img className={classes.logo} src={logo} alt="Verdaccio" title="Verdaccio"/>
55
+            </a>
56
+            &nbsp;/&nbsp;
57
+            {__APP_VERSION__}
58
+            </div>
59
+        </footer>
60
+      </div>
61
+    );
62
+  }
63
+}

+ 1
- 0
components/Footer/logo.svg View File

@@ -0,0 +1 @@
1
+<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path d="M48 17.6L32.8 48H24L.4.8h15.2l12.8 25.6 4.4-8.8H48z" id="a"/><filter id="b" filterUnits="objectBoundingBox" height="140.3%" width="139.9%" y="-11.7%" x="-20%"><feGaussianBlur stdDeviation="2.5"/></filter><path d="M50.8 12H35.6L41.2.8h15.2L50.8 12z" id="c"/><filter id="d" filterUnits="objectBoundingBox" height="269.6%" width="191.3%" y="-49.1%" x="-45.7%"><feGaussianBlur stdDeviation="2.5"/></filter><path d="M32.8 48H24L.4.8h15.2l20.377 40.89L32.8 48z" id="e"/></defs><path fill="none" d="M-1-1h582v402H-1z"/><g><g stroke="null" fill-rule="evenodd" fill="none"><use transform="matrix(1.71429 0 0 1.71429 -37.027 -40.362)" x="22.366" y="28.311" xlink:href="#a" filter="url(#b)" fill="#000"/><use transform="matrix(1.71429 0 0 1.71429 -37.027 -40.362)" x="22.366" y="28.311" xlink:href="#a" fill="#405236"/><path stroke="#405236" d="M80.27 40.4H58.816L50 58.028 26.785 11.6H5.33l38.4 76.8h12.542l24-48z" stroke-width="2.4"/><use transform="matrix(1.71429 0 0 1.71429 -37.027 -40.362)" x="22.366" y="28.311" xlink:href="#c" filter="url(#d)" fill="#000"/><use transform="matrix(1.71429 0 0 1.71429 -37.027 -40.362)" x="22.366" y="28.311" xlink:href="#c" fill="#CD4000"/><path stroke="#CD4000" d="M87.128 26.686L94.671 11.6H73.215l-7.543 15.086h21.456z" stroke-width="2.4"/><g><use transform="matrix(1.71429 0 0 1.71429 -37.027 -40.362)" x="22.366" y="28.311" xlink:href="#e" fill="#4A5E3F"/><path stroke="#405236" d="M56.274 88.4l4.415-8.763L26.783 11.6H5.33l38.4 76.8h12.547-.002z" stroke-width="2.4"/></g><path stroke="#CD4000" stroke-linecap="square" stroke-width="2.4" d="M65.771 11.6h26.094m-32.95 6.857h26.092m-32.95 8.229H78.15"/></g></g></svg>

+ 49
- 0
components/Header/header.scss View File

@@ -0,0 +1,49 @@
1
+@import '../../styles/variables';
2
+@import '../../styles/mixins';
3
+
4
+.header {
5
+  display: flex;
6
+  height: 70px;
7
+  width: 100%;
8
+  align-items: center;
9
+  background: $primary-color;
10
+  color: $white;
11
+
12
+  figure {
13
+    margin: 0 0 0 10px;
14
+    font-size: $font-size-sm;
15
+    line-height: $line-height-sm;
16
+    padding: 8px 0;
17
+  }
18
+
19
+  .headerWrap {
20
+    display: flex;
21
+    align-items: center;
22
+    @include container-size;
23
+  }
24
+
25
+  .headerRight {
26
+    margin-left: auto;
27
+  }
28
+
29
+  .logo {
30
+    height: 50px;
31
+  }
32
+
33
+  .headerButton {
34
+    color: inherit;
35
+    border-color: $white;
36
+    background-color: transparent;
37
+
38
+    &:hover, &:focus {
39
+      color: $primary-color;
40
+      border-color: $saltpan;
41
+      background-color: $saltpan;
42
+    }
43
+  }
44
+
45
+  .usernameField {
46
+    margin-right: 10px;
47
+  }
48
+}
49
+

+ 68
- 0
components/Header/index.js View File

@@ -0,0 +1,68 @@
1
+import React from 'react';
2
+import PropTypes from 'prop-types';
3
+import {Button} from 'element-react';
4
+import capitalize from 'lodash/capitalize';
5
+import {getRegistryURL} from '../../utils/url';
6
+import classes from './header.scss';
7
+import './logo.png';
8
+
9
+const Header = ({
10
+  logo = '',
11
+  scope = '',
12
+  username = '',
13
+  handleLogout = () => {},
14
+  toggleLoginModal = () => {}
15
+}) => {
16
+  const registryUrl = getRegistryURL();
17
+  return (
18
+    <header className={classes.header}>
19
+      <div className={classes.headerWrap}>
20
+        <a href={`${registryUrl}/#/`}>
21
+          <img src={logo} className={classes.logo} />
22
+        </a>
23
+        <figure>
24
+          npm set {scope}
25
+          registry {registryUrl}
26
+          <br />
27
+          npm adduser --registry {registryUrl}
28
+        </figure>
29
+
30
+        <div className={classes.headerRight}>
31
+          {username ? (
32
+            <div className="user-logged">
33
+              <span
34
+                className={`user-logged-greetings ${classes.usernameField}`}
35
+              >
36
+                Hi, {capitalize(username)}
37
+              </span>
38
+              <Button
39
+                className={`${classes.headerButton} header-button-logout`}
40
+                type="danger"
41
+                onClick={handleLogout}
42
+              >
43
+                Logout
44
+              </Button>
45
+            </div>
46
+          ) : (
47
+            <Button
48
+              className={`${classes.headerButton} header-button-login`}
49
+              onClick={toggleLoginModal}
50
+            >
51
+              Login
52
+            </Button>
53
+          )}
54
+        </div>
55
+      </div>
56
+    </header>
57
+  );
58
+};
59
+
60
+Header.propTypes = {
61
+  logo: PropTypes.string,
62
+  scope: PropTypes.string,
63
+  username: PropTypes.string,
64
+  handleLogout: PropTypes.func.isRequired,
65
+  toggleLoginModal: PropTypes.func.isRequired
66
+};
67
+
68
+export default Header;

BIN
components/Header/logo.png View File


+ 29
- 0
components/Help/help.scss View File

@@ -0,0 +1,29 @@
1
+@import '../../styles/variables';
2
+@import '../../styles/mixins';
3
+
4
+.help {
5
+  .noPkg {
6
+    display: flex;
7
+    flex-direction: column;
8
+    align-items: center;
9
+    padding: 30px 0;
10
+    font-size: $font-size-lg;
11
+    color: $nobel-02;
12
+
13
+    .noPkgTitle {
14
+      margin: 1em 0;
15
+      padding: 0;
16
+      font-size: $font-size-xl;
17
+    }
18
+
19
+    .noPkgIntro {
20
+      line-height: $line-height-xxs;
21
+      margin: 0 auto;
22
+      font-size: $font-size-sm;
23
+    }
24
+
25
+    code {
26
+      font-style: italic;
27
+    }
28
+  }
29
+}

+ 42
- 0
components/Help/index.js View File

@@ -0,0 +1,42 @@
1
+import React from 'react';
2
+import SyntaxHighlighter, {registerLanguage} from 'react-syntax-highlighter/dist/light';
3
+import sunburst from 'react-syntax-highlighter/src/styles/sunburst';
4
+import js from 'react-syntax-highlighter/dist/languages/javascript';
5
+
6
+import classes from './help.scss';
7
+import {getRegistryURL} from '../../utils/url';
8
+
9
+registerLanguage('javascript', js);
10
+
11
+const Help = () => {
12
+  const registryURL = getRegistryURL();
13
+
14
+    return (
15
+      <div className={classes.help}>
16
+        <li className={classes.noPkg}>
17
+          <h1 className={classes.noPkgTitle}>
18
+            No Package Published Yet
19
+          </h1>
20
+          <div className={classes.noPkgIntro}>
21
+            <div>
22
+              To publish your first package just:
23
+            </div>
24
+            <br/>
25
+            <strong>
26
+              1. Login
27
+            </strong>
28
+            <SyntaxHighlighter language='javascript' style={sunburst} id="adduser">
29
+              {`npm adduser --registry  ${registryURL}`}
30
+            </SyntaxHighlighter>
31
+            <strong>2. Publish</strong>
32
+            <SyntaxHighlighter language='javascript' style={sunburst} id="publish">
33
+              {`npm publish --registry ${registryURL}`}
34
+            </SyntaxHighlighter>
35
+            <strong>3. Refresh this page!</strong>
36
+          </div>
37
+        </li>
38
+      </div>
39
+    );
40
+};
41
+
42
+export default Help;

+ 155
- 0
components/Login/index.js View File

@@ -0,0 +1,155 @@
1
+import React, {Component, createRef} from 'react';
2
+import PropTypes from 'prop-types';
3
+import {Form, Button, Dialog, Input, Alert} from 'element-react';
4
+
5
+export default class LoginModal extends Component {
6
+  static propTypes = {
7
+    visibility: PropTypes.bool,
8
+    error: PropTypes.object,
9
+    onCancel: PropTypes.func,
10
+    onSubmit: PropTypes.func
11
+  };
12
+
13
+  static defaultProps = {
14
+    visibility: true,
15
+    error: {},
16
+    onCancel: () => {},
17
+    onSubmit: () => {}
18
+  };
19
+
20
+  state = {
21
+    form: {
22
+      username: '',
23
+      password: ''
24
+    },
25
+    rules: {
26
+      username: [
27
+        {
28
+          required: true,
29
+          message: 'Please input the username',
30
+          trigger: 'change'
31
+        }
32
+      ],
33
+      password: [
34
+        {
35
+          required: true,
36
+          message: 'Please input the password',
37
+          trigger: 'change'
38
+        }
39
+      ]
40
+    }
41
+  };
42
+
43
+  constructor(props) {
44
+    super(props);
45
+    this.formRef = createRef();
46
+    this.submitCredentials = this.submitCredentials.bind(this);
47
+    this.setCredentials = this.setCredentials.bind(this);
48
+  }
49
+
50
+  /**
51
+   * set login modal's username and password to current state
52
+   * Required by: <LoginModal />
53
+   */
54
+  setCredentials(key, value) {
55
+    this.setState(
56
+      (prevState) => ({
57
+        form: {...prevState.form, [key]: value}
58
+      })
59
+    );
60
+  }
61
+
62
+  /**
63
+   * Clears the username and password field.
64
+   */
65
+  handleReset() {
66
+    this.formRef.current.resetFields();
67
+  }
68
+
69
+  submitCredentials(event) {
70
+    // prevents default submit behavior
71
+    event.preventDefault();
72
+    this.formRef.current.validate((valid) => {
73
+      if (valid) {
74
+        const {username, password} = this.state.form;
75
+        this.props.onSubmit(username, password);
76
+        this.setState({
77
+          form: {username}
78
+        });
79
+      }
80
+      return false;
81
+    });
82
+  }
83
+
84
+  renderLoginError({type, title, description} = {}) {
85
+    return type ? (
86
+      <Alert
87
+        title={title}
88
+        type={type}
89
+        description={description}
90
+        showIcon={true}
91
+        closable={false}
92
+        style={{lineHeight: '10px'}}
93
+      />
94
+    ) : (
95
+      ''
96
+    );
97
+  }
98
+
99
+  render() {
100
+    const {visibility, onCancel, error} = this.props;
101
+    const {username, password} = this.state.form;
102
+    return (
103
+      <div className="login-dialog">
104
+        <Dialog
105
+          title="Login"
106
+          size="tiny"
107
+          visible={visibility}
108
+          onCancel={onCancel}
109
+        >
110
+          <Dialog.Body>
111
+            <Form
112
+              className="login-form"
113
+              ref={this.formRef}
114
+              model={this.state.form}
115
+              rules={this.state.rules}
116
+            >
117
+              <Form.Item>
118
+                {this.renderLoginError(error)}
119
+              </Form.Item>
120
+              <Form.Item prop="username" labelPosition="top">
121
+                <Input
122
+                  name="username"
123
+                  placeholder="Type your username"
124
+                  value={username}
125
+                  onChange={this.setCredentials.bind(this, 'username')}
126
+                />
127
+              </Form.Item>
128
+              <Form.Item prop="password">
129
+                <Input
130
+                  name="password"
131
+                  type="password"
132
+                  placeholder="Type your password"
133
+                  value={password}
134
+                  onChange={this.setCredentials.bind(this, 'password')}
135
+                />
136
+              </Form.Item>
137
+              <Form.Item style={{float: 'right'}}>
138
+                <Button onClick={onCancel} className="cancel-login-button">
139
+                  Cancel
140
+                </Button>
141
+                    <Button
142
+                      nativeType="submit"
143
+                      className="login-button"
144
+                      onClick={this.submitCredentials}
145
+                    >
146
+                      Login
147
+                </Button>
148
+              </Form.Item>
149
+            </Form>
150
+          </Dialog.Body>
151
+        </Dialog>
152
+      </div>
153
+    );
154
+  }
155
+}

+ 18
- 0
components/NoItems/index.js View File

@@ -0,0 +1,18 @@
1
+import React from 'react';
2
+import PropTypes from 'prop-types';
3
+
4
+import classes from './noItems.scss';
5
+
6
+const NoItems = (props) => {
7
+    return (
8
+      <div className={classes.noItems}>
9
+        <h2>{props.text}</h2>
10
+      </div>
11
+    );
12
+};
13
+
14
+NoItems.propTypes = {
15
+  text: PropTypes.string.isRequired
16
+};
17
+
18
+export default NoItems;

+ 3
- 0
components/NoItems/noItems.scss View File

@@ -0,0 +1,3 @@
1
+.noItems {
2
+  margin: 5em 0;
3
+}

+ 15
- 0
components/NotFound/404.scss View File

@@ -0,0 +1,15 @@
1
+@import '../../styles/variables';
2
+@import '../../styles/mixins';
3
+
4
+.notFound {
5
+  width: 100%;
6
+  font-size: $font-size-md;
7
+  line-height: $line-height-xl;
8
+  border: none;
9
+  outline: none;
10
+  @include border-bottom-default($grey-light);
11
+
12
+  &:focus {
13
+    @include border-bottom-default($grey);
14
+  }
15
+}

+ 23
- 0
components/NotFound/index.js View File

@@ -0,0 +1,23 @@
1
+
2
+import React from 'react';
3
+import PropTypes from 'prop-types';
4
+
5
+import classes from './404.scss';
6
+
7
+const NotFound = (props) => {
8
+    return (
9
+      <div className={classes.notFound}>
10
+        <h1>Error 404 - {props.pkg}</h1>
11
+        <hr/>
12
+        <p>
13
+          Oops, The package you are trying to access does not exist.
14
+        </p>
15
+      </div>
16
+    );
17
+};
18
+
19
+NotFound.propTypes = {
20
+  pkg: PropTypes.string.isRequired
21
+};
22
+
23
+export default NotFound;

+ 52
- 0
components/Package/index.js View File

@@ -0,0 +1,52 @@
1
+import React from 'react';
2
+import PropTypes from 'prop-types';
3
+import {Tag} from 'element-react';
4
+import {Link} from 'react-router-dom';
5
+
6
+import {formatDateDistance} from '../../utils/package';
7
+
8
+import classes from './package.scss';
9
+
10
+const Package = ({name, version, author, description, license, time}) => {
11
+  return (<section className={classes.package}>
12
+    <Link to={`detail/${name}`}>
13
+      <div className={classes.header}>
14
+        <div className={classes.title}>
15
+          <h1>
16
+            {name} <Tag type="gray">v{version}</Tag>
17
+          </h1>
18
+        </div>
19
+        <div role="author" className={classes.author}>
20
+        { author ? `By: ${author}`: ''}
21
+        </div>
22
+      </div>
23
+      <div className={classes.footer}>
24
+        <p className={classes.description}>
25
+          {description}
26
+        </p>
27
+      </div>
28
+      <div className={classes.details}>
29
+        <div className={classes.homepage}>
30
+          {time ? `Published ${formatDateDistance(time)} ago` : ''}
31
+        </div>
32
+        <div className={classes.license}>
33
+          {license}
34
+        </div>
35
+      </div>
36
+    </Link>
37
+  </section>);
38
+};
39
+
40
+Package.propTypes = {
41
+  name: PropTypes.string,
42
+  version: PropTypes.string,
43
+  author: PropTypes.string,
44
+  description: PropTypes.string,
45
+  license: PropTypes.string,
46
+  time: PropTypes.oneOfType([
47
+    PropTypes.string,
48
+    PropTypes.instanceOf(Date)
49
+  ])
50
+};
51
+
52
+export default Package;

+ 88
- 0
components/Package/package.scss View File

@@ -0,0 +1,88 @@
1
+@import '../../styles/variables';
2
+@import '../../styles/mixins';
3
+
4
+.package {
5
+  .header {
6
+    display: flex;
7
+    align-items: center;
8
+    margin: 10px 0 0;
9
+  }
10
+
11
+  .footer {
12
+    display: flex;
13
+    p.description {
14
+      width: 100%;
15
+      margin-bottom: 0;
16
+      font-size: $font-size-sm;
17
+      color: $grey-dark;
18
+      word-wrap: break-word;
19
+    }
20
+  }
21
+
22
+  .details {
23
+    display: flex;
24
+    font-size: 80%;
25
+    color: $grey-light;
26
+    padding-top: 5px;
27
+    .license {
28
+      width: 20%;
29
+      text-align: right;
30
+    }
31
+
32
+    .homepage {
33
+      width: 80%;
34
+    }
35
+  }
36
+
37
+  > a {
38
+    display: block;
39
+    position: relative;
40
+    color: inherit;
41
+    margin: 0;
42
+    padding: 10px 0;
43
+    cursor: pointer;
44
+    text-decoration: none;
45
+
46
+    .title {
47
+      width: 100%;
48
+
49
+      h1 {
50
+        font-size: $font-size-md;
51
+        margin: 0;
52
+
53
+        :global {
54
+          .el-tag {
55
+            margin-left: 5px;
56
+          }
57
+        }
58
+      }
59
+    }
60
+
61
+    .author {
62
+      color: $grey-light;
63
+      font-size: inherit;
64
+      width: 30%;
65
+      text-align: right;
66
+    }
67
+
68
+    &:hover {
69
+      &::before {
70
+        content: '';
71
+        display: block;
72
+        background: rgba(255, 255, 255, 0.7);
73
+        @include fullSize;
74
+      }
75
+
76
+      &::after {
77
+        display: block;
78
+        position: absolute;
79
+        top: 50%;
80
+        right: 0;
81
+        left: 0;
82
+        text-align: center;
83
+        transform: translateY(-50%);
84
+        font-size: $font-size-md;
85
+      }
86
+    }
87
+  }
88
+}

+ 27
- 0
components/PackageDetail/index.js View File

@@ -0,0 +1,27 @@
1
+import React from 'react';
2
+import PropTypes from 'prop-types';
3
+import isNil from 'lodash/isNil';
4
+
5
+import Readme from '../Readme';
6
+
7
+import classes from './packageDetail.scss';
8
+
9
+const displayState = (readMe) => {
10
+  return !isNil(readMe) ? <Readme readMe={readMe} /> : '';
11
+};
12
+
13
+const PackageDetail = ({packageName, readMe}) => {
14
+  return (
15
+    <div className={classes.pkgDetail}>
16
+      <h1 className={classes.title}>{packageName}</h1>
17
+      <div className={classes.readme}>{displayState(readMe)}</div>
18
+    </div>
19
+  );
20
+};
21
+
22
+PackageDetail.propTypes = {
23
+  readMe: PropTypes.string,
24
+  packageName: PropTypes.string.isRequired
25
+};
26
+
27
+export default PackageDetail;

+ 16
- 0
components/PackageDetail/packageDetail.scss View File

@@ -0,0 +1,16 @@
1
+@import '../../styles/variables';
2
+@import '../../styles/mixins';
3
+
4
+.pkgDetail {
5
+  .title {
6
+    font-size: $font-size-xxl;
7
+    font-weight: $font-weight-semibold;
8
+    margin: 0 0 40px;
9
+    padding-bottom: 5px;
10
+    @include border-bottom-default($greyGainsboro);
11
+  }
12
+
13
+  .readme {
14
+    margin-bottom: 5em;
15
+  }
16
+}

+ 77
- 0
components/PackageList/index.js View File

@@ -0,0 +1,77 @@
1
+import React from 'react';
2
+import PropTypes from 'prop-types';
3
+import isEmpty from 'lodash/isEmpty';
4
+
5
+import Package from '../Package';
6
+import Help from '../Help';
7
+import NoItems from '../NoItems';
8
+import {formatAuthor, formatLicense} from '../../utils/package';
9
+
10
+import classes from './packageList.scss';
11
+
12
+export default class PackageList extends React.Component {
13
+  static propTypes = {
14
+    packages: PropTypes.array,
15
+    help: PropTypes.bool
16
+  };
17
+
18
+  render() {
19
+    return (
20
+      <div className="package-list-items">
21
+        <div className={classes.pkgContainer}>
22
+          {this.renderTitle()}
23
+          {this.isTherePackages() ? this.renderList() : this.renderOptions()}
24
+        </div>
25
+      </div>
26
+    );
27
+  }
28
+
29
+  renderTitle() {
30
+    if (this.isTherePackages() === false) {
31
+      return;
32
+    }
33
+
34
+    return <h1 className={classes.listTitle}>Available Packages</h1>;
35
+  }
36
+
37
+  renderList() {
38
+    return this.props.packages.map((pkg, i) => {
39
+      const {name, version, description, time} = pkg;
40
+      const author = formatAuthor(pkg.author);
41
+      const license = formatLicense(pkg.license);
42
+      return (
43
+        <li key={i}>
44
+          <Package {...{name, version, author, description, license, time}} />
45
+        </li>
46
+      );
47
+    });
48
+  }
49
+
50
+  renderOptions() {
51
+    if (this.isTherePackages() === false && this.props.help) {
52
+      return this.renderHelp();
53
+    } else {
54
+      return this.renderNoItems();
55
+    }
56
+  }
57
+
58
+  renderNoItems() {
59
+    return (
60
+      <NoItems
61
+        className="package-no-items"
62
+        text={'No items were found with that query'}
63
+      />
64
+    );
65
+  }
66
+
67
+  renderHelp() {
68
+    if (this.props.help === false) {
69
+      return;
70
+    }
71
+    return <Help />;
72
+  }
73
+
74
+  isTherePackages() {
75
+    return isEmpty(this.props.packages) === false;
76
+  }
77
+}

+ 23
- 0
components/PackageList/packageList.scss View File

@@ -0,0 +1,23 @@
1
+@import '../../styles/variables';
2
+@import '../../styles/mixins';
3
+
4
+.pkgContainer {
5
+  margin: 0;
6
+  padding: 0;
7
+
8
+  li {
9
+    @include border-bottom-default($paleNavy);
10
+    list-style: none;
11
+
12
+    &:last-child {
13
+      border-bottom: none;
14
+    }
15
+  }
16
+
17
+  .listTitle {
18
+    font-weight: $font-weight-regular;
19
+    font-size: $font-size-xl;
20
+    margin-top: 30px;
21
+    margin-bottom: 0;
22
+  }
23
+}

+ 25
- 0
components/PackageSidebar/Module/index.jsx View File

@@ -0,0 +1,25 @@
1
+import React from 'react';
2
+import PropTypes from 'prop-types';
3
+
4
+import classes from './style.scss';
5
+
6
+export default function Module({title, description, children, className}) {
7
+  return (
8
+    <div className={`${classes.module} ${className}`}>
9
+      <h2 className={classes.moduleTitle}>
10
+        {title}
11
+        {description && <span>{description}</span>}
12
+      </h2>
13
+      <div>
14
+        {children}
15
+      </div>
16
+    </div>
17
+  );
18
+}
19
+
20
+Module.propTypes = {
21
+  title: PropTypes.string.isRequired,
22
+  description: PropTypes.string,
23
+  children: PropTypes.any.isRequired,
24
+  className: PropTypes.string
25
+};

+ 24
- 0
components/PackageSidebar/Module/style.scss View File

@@ -0,0 +1,24 @@
1
+@import '../../../styles/variables';
2
+@import '../../../styles/mixins';
3
+
4
+.module {
5
+
6
+  margin-bottom: 10px;
7
+
8
+  .moduleTitle {
9
+    display: flex;
10
+    align-items: flex-end;
11
+    font-size: $font-size-lg;
12
+    margin: 0 0 10px;
13
+    padding: 5px 0;
14
+    font-weight: $font-weight-semibold;
15
+    @include border-bottom-default($greyGainsboro);
16
+
17
+    span { // description
18
+      font-size: $font-size-sm;
19
+      color: $greyChateau;
20
+      margin-left: auto;
21
+      font-weight: $font-weight-light;
22
+    }
23
+  }
24
+}

+ 11
- 0
components/PackageSidebar/ModuleContentPlaceholder/index.jsx View File

@@ -0,0 +1,11 @@
1
+import React from 'react';
2
+import PropTypes from 'prop-types';
3
+
4
+import classes from './style.scss';
5
+
6
+export default function ModuleContentPlaceholder({text}) {
7
+  return <p className={classes.emptyPlaceholder}>{text}</p>;
8
+}
9
+ModuleContentPlaceholder.propTypes = {
10
+  text: PropTypes.string.isRequired
11
+};

+ 8
- 0
components/PackageSidebar/ModuleContentPlaceholder/style.scss View File

@@ -0,0 +1,8 @@
1
+@import '../../../styles/variables';
2
+
3
+.emptyPlaceholder {
4
+  text-align: center;
5
+  margin: 20px 0;
6
+  font-size: $font-size-base;
7
+  color: $greyChateau;
8
+}

+ 97
- 0
components/PackageSidebar/index.jsx View File

@@ -0,0 +1,97 @@
1
+import React from 'react';
2
+import PropTypes from 'prop-types';
3
+import get from 'lodash/get';
4
+import LastSync from './modules/LastSync';
5
+import Maintainers from './modules/Maintainers';
6
+import Dependencies from './modules/Dependencies';
7
+import PeerDependencies from './modules/PeerDependencies';
8
+import Infos from './modules/Infos';
9
+
10
+import {
11
+  formatLicense,
12
+  formatRepository,
13
+  getLastUpdatedPackageTime,
14
+  getRecentReleases
15
+} from '../../utils/package';
16
+import API from '../../utils/api';
17
+
18
+export default class PackageSidebar extends React.Component {
19
+  state = {};
20
+
21
+  static propTypes = {
22
+    packageName: PropTypes.string.isRequired
23
+  };
24
+
25
+  constructor(props) {
26
+    super(props);
27
+    this.loadPackageData = this.loadPackageData.bind(this);
28
+  }
29
+
30
+  async componentDidMount() {
31
+    await this.loadPackageData(this.props.packageName);
32
+  }
33
+
34
+  async loadPackageData(packageName) {
35
+    let packageMeta;
36
+
37
+    try {
38
+      packageMeta = await API.request(`sidebar/${packageName}`, 'GET');
39
+    } catch (err) {
40
+      this.setState({
41
+        failed: true
42
+      });
43
+    }
44
+
45
+    this.setState({
46
+      packageMeta
47
+    });
48
+  }
49
+
50
+  render() {
51
+    let {packageMeta} = this.state;
52
+
53
+    if (packageMeta) {
54
+      const {time, _uplinks} = packageMeta;
55
+
56
+      // Infos component
57
+      const license = formatLicense(get(packageMeta, 'latest.license', null));
58
+      const repository = formatRepository(
59
+        get(packageMeta, 'latest.repository', null)
60
+      );
61
+      const homepage = get(packageMeta, 'latest.homepage', null);
62
+
63
+      // Lastsync component
64
+      const recentReleases = getRecentReleases(time);
65
+      const lastUpdated = getLastUpdatedPackageTime(_uplinks);
66
+
67
+      // Dependencies component
68
+      const dependencies = get(packageMeta, 'latest.dependencies', {});
69
+      const peerDependencies = get(packageMeta, 'latest.peerDependencies', {});
70
+
71
+      // Maintainers component
72
+      return (
73
+        <aside className="sidebar-info">
74
+          {time && (
75
+            <LastSync
76
+              recentReleases={recentReleases}
77
+              lastUpdated={lastUpdated}
78
+            />
79
+          )}
80
+          <Infos
81
+            homepage={homepage}
82
+            repository={repository}
83
+            license={license}
84
+          />
85
+          {/* TODO: Refacor later, when we decide to show only maintainers/authors */}
86
+          <Maintainers packageMeta={packageMeta} />
87
+          <Dependencies dependencies={dependencies} />
88
+          <PeerDependencies dependencies={peerDependencies} />
89
+          {/* Package management module? Help us implement it! */}
90
+        </aside>
91
+      );
92
+    }
93
+    return (
94
+      <aside className="sidebar-loading">Loading package information...</aside>
95
+    );
96
+  }
97
+}

+ 50
- 0
components/PackageSidebar/modules/Dependencies/index.jsx View File

@@ -0,0 +1,50 @@
1
+import React from 'react';
2
+import PropTypes from 'prop-types';
3
+import Module from '../../Module';
4
+
5
+import {getDetailPageURL} from '../../../../utils/url';
6
+import ModuleContentPlaceholder from '../../ModuleContentPlaceholder';
7
+
8
+import classes from './style.scss';
9
+
10
+export const NO_DEPENDENCIES = 'Zero Dependencies!';
11
+export const DEP_ITEM_CLASS = 'dependency-item';
12
+
13
+const renderDependenciesList = (dependencies, dependenciesList) => {
14
+  return (
15
+    <ul>
16
+      {dependenciesList.map((dependenceName, index) => {
17
+        return (
18
+          <li
19
+            className={DEP_ITEM_CLASS}
20
+            key={index}
21
+            title={`Depend on version: ${dependencies[dependenceName]}`}
22
+          >
23
+            <a href={getDetailPageURL(dependenceName)}>{dependenceName}</a>
24
+            {index < dependenciesList.length - 1 && <span>,&nbsp;</span>}
25
+          </li>
26
+        );
27
+      })}
28
+    </ul>
29
+  );
30
+};
31
+
32
+const Dependencies = ({dependencies = {}, title = 'Dependencies'}) => {
33
+  const dependenciesList = Object.keys(dependencies);
34
+  return (
35
+    <Module title={title} className={classes.dependenciesModule}>
36
+      {dependenciesList.length > 0 ? (
37
+        renderDependenciesList(dependencies, dependenciesList)
38
+      ) : (
39
+        <ModuleContentPlaceholder text={NO_DEPENDENCIES} />
40
+      )}
41
+    </Module>
42
+  );
43
+};
44
+
45
+Dependencies.propTypes = {
46
+  dependencies: PropTypes.object,
47
+  title: PropTypes.string
48
+};
49
+
50
+export default Dependencies;

+ 13
- 0
components/PackageSidebar/modules/Dependencies/style.scss View File

@@ -0,0 +1,13 @@
1
+@import '../../../../styles/variables';
2
+
3
+.dependenciesModule {
4
+  li {
5
+    display: inline-block;
6
+    font-size: $font-size-sm;
7
+    line-height: $line-height-xxs;
8
+
9
+    a {
10
+      color: inherit;
11
+    }
12
+  }
13
+}

+ 37
- 0
components/PackageSidebar/modules/Infos/index.jsx View File

@@ -0,0 +1,37 @@
1
+import React from 'react';
2
+import PropTypes from 'prop-types';
3
+import Module from '../../Module';
4
+import ModuleContentPlaceholder from '../../ModuleContentPlaceholder';
5
+
6
+import classes from './style.scss';
7
+
8
+const renderSection = (title, url) => (
9
+  <li>
10
+    <span>{title}</span>
11
+    <a href={url} target="_blank">
12
+      {url}
13
+    </a>
14
+  </li>
15
+);
16
+
17
+const Infos = ({homepage, repository, license}) => {
18
+  const showInfo = homepage || repository || license;
19
+  return <Module title="Infos" className={classes.infosModule}>
20
+      {showInfo ? <ul>
21
+          {homepage && renderSection('Homepage', homepage)}
22
+          {repository && renderSection('Repository', repository)}
23
+          {license && <li>
24
+              <span>License</span>
25
+              <span>{license}</span>
26
+            </li>}
27
+        </ul> : <ModuleContentPlaceholder text="Not Available!" />}
28
+    </Module>;
29
+};
30
+
31
+Infos.propTypes = {
32
+  homepage: PropTypes.string,
33
+  repository: PropTypes.string,
34
+  license: PropTypes.string
35
+};
36
+
37
+export default Infos;

+ 21
- 0
components/PackageSidebar/modules/Infos/style.scss View File

@@ -0,0 +1,21 @@
1
+@import '../../../../styles/variables';
2
+@import '../../../../styles/mixins';
3
+
4
+.infosModule {
5
+  li {
6
+    display: flex;
7
+    font-size: $font-size-sm;
8
+    line-height: $line-height-xs;
9
+
10
+    a {
11
+      color: inherit;
12
+      max-width: 150px;
13
+      @include ellipsis;
14
+    }
15
+    
16
+    a:last-child,
17
+    span:last-child {
18
+      margin-left: auto;
19
+    }
20
+  }
21
+}

+ 45
- 0
components/PackageSidebar/modules/LastSync/index.jsx View File

@@ -0,0 +1,45 @@
1
+import React from 'react';
2
+import propTypes from 'prop-types';
3
+import Module from '../../Module';
4
+import ModuleContentPlaceholder from '../../ModuleContentPlaceholder';
5
+
6
+import classes from './style.scss';
7
+
8
+const renderRecentReleases = (recentReleases) => {
9
+  return (
10
+    <ul>
11
+      {recentReleases.map((versionInfo) => {
12
+        const {version, time} = versionInfo;
13
+        return (
14
+          <li className="last-sync-item" key={version}>
15
+            <span>{version}</span>
16
+            <span>{time}</span>
17
+          </li>
18
+        );
19
+      })}
20
+    </ul>
21
+  );
22
+};
23
+
24
+const LastSync = ({recentReleases = [], lastUpdated = ''}) => {
25
+  return (
26
+    <Module
27
+      title="Last Sync"
28
+      description={lastUpdated}
29
+      className={classes.releasesModule}
30
+    >
31
+      {recentReleases.length ? (
32
+        renderRecentReleases(recentReleases)
33
+      ) : (
34
+        <ModuleContentPlaceholder text="Not Available!" />
35
+      )}
36
+    </Module>
37
+  );
38
+};
39
+
40
+LastSync.propTypes = {
41
+  recentReleases: propTypes.array,
42
+  lastUpdated: propTypes.string
43
+};
44
+
45
+export default LastSync;

+ 13
- 0
components/PackageSidebar/modules/LastSync/style.scss View File

@@ -0,0 +1,13 @@
1
+@import '../../../../styles/variables';
2
+
3
+.releasesModule {
4
+  li {
5
+    display: flex;
6
+    font-size: $font-size-sm;
7
+    line-height: $line-height-xs;
8
+
9
+    span:last-child {
10
+      margin-left: auto;
11
+    }
12
+  }
13
+}

+ 19
- 0
components/PackageSidebar/modules/Maintainers/MaintainerInfo/index.jsx View File

@@ -0,0 +1,19 @@
1
+import React from 'react';
2
+import PropTypes from 'prop-types';
3
+
4
+import classes from './style.scss';
5
+
6
+export default function MaintainerInfo({title, name, avatar}) {
7
+  let avatarDescription = `${title} ${name}'s avatar`;
8
+  return (
9
+    <div className={classes.maintainer} title={name}>
10
+      <img src={avatar} alt={avatarDescription} title={avatarDescription}/>
11
+      <span className="maintainer-name">{name}</span>
12
+    </div>
13
+  );
14
+}
15
+MaintainerInfo.propTypes = {
16
+  title: PropTypes.string.isRequired,
17
+  name: PropTypes.string.isRequired,
18
+  avatar: PropTypes.string.isRequired
19
+};

+ 26
- 0
components/PackageSidebar/modules/Maintainers/MaintainerInfo/style.scss View File

@@ -0,0 +1,26 @@
1
+@import '../../../../../styles/variables';
2
+@import '../../../../../styles/mixins';
3
+
4
+.maintainer {
5
+  display: flex;
6
+  line-height: $line-height-xl;
7
+  cursor: default;
8
+
9
+  &:not(:last-child) {
10
+    margin-bottom: 10px;
11
+  }
12
+
13
+  img {
14
+    width: 30px;
15
+    height: 30px;
16
+    margin-right: 10px;
17
+    border-radius: 100%;
18
+    flex-shrink: 0;
19
+  }
20
+
21
+  span {
22
+    font-size: $font-size-sm;
23
+    flex-shrink: 1;
24
+    @include ellipsis;
25
+  }
26
+}

+ 122
- 0
components/PackageSidebar/modules/Maintainers/index.jsx View File

@@ -0,0 +1,122 @@
1
+import React from 'react';
2
+import PropTypes from 'prop-types';
3
+import get from 'lodash/get';
4
+import filter from 'lodash/filter';
5
+import size from 'lodash/size';
6
+import has from 'lodash/has';
7
+import uniqBy from 'lodash/uniqBy';
8
+
9
+import Module from '../../Module';
10
+import MaintainerInfo from './MaintainerInfo';
11
+import ModuleContentPlaceholder from '../../ModuleContentPlaceholder';
12
+
13
+import classes from './style.scss';
14
+
15
+const CONTRIBUTORS_TO_SHOW = 5;
16
+
17
+export default class Maintainers extends React.Component {
18
+  static propTypes = {
19
+    packageMeta: PropTypes.object.isRequired
20
+  };
21
+
22
+  state = {};
23
+
24
+  constructor(props) {
25
+    super(props);
26
+    this.handleShowAllContributors = this.handleShowAllContributors.bind(this);
27
+  }
28
+
29
+  get author() {
30
+    return get(this, 'props.packageMeta.latest.author');
31
+  }
32
+
33
+  get contributors() {
34
+    let contributors = get(this, 'props.packageMeta.latest.contributors', {});
35
+    return filter(contributors, (contributor) => {
36
+      return (
37
+        contributor.name !== get(this, 'author.name') &&
38
+        contributor.email !== get(this, 'author.email')
39
+      );
40
+    });
41
+  }
42
+
43
+  get showAllContributors() {
44
+    return this.state.showAllContributors || size(this.contributors) <= 5;
45
+  }
46
+
47
+  get uniqueContributors() {
48
+    if (!this.contributors) {
49
+      return [];
50
+    }
51
+
52
+    return uniqBy(this.contributors, (contributor) => contributor.name).slice(
53
+      0,
54
+      CONTRIBUTORS_TO_SHOW
55
+    );
56
+  }
57
+
58
+  handleShowAllContributors() {
59
+    this.setState({
60
+      showAllContributors: true
61
+    });
62
+  }
63
+
64
+  renderContributors() {
65
+    if (!this.contributors) return null;
66
+
67
+    return (this.showAllContributors
68
+      ? this.contributors
69
+      : this.uniqueContributors
70
+    ).map((contributor, index) => {
71
+      return (
72
+        <MaintainerInfo
73
+          key={index}
74
+          title="Contributors"
75
+          name={contributor.name}
76
+          avatar={contributor.avatar}
77
+        />
78
+      );
79
+    });
80
+  }
81
+
82
+  renderAuthorAndContributors(author) {
83
+    return (
84
+      <div>
85
+        <ul className="maintainer-author">
86
+          {author &&
87
+            author.name && (
88
+              <MaintainerInfo
89
+                title="Author"
90
+                name={author.name}
91
+                avatar={author.avatar}
92
+              />
93
+            )}
94
+          {this.renderContributors()}
95
+        </ul>
96
+        {!this.showAllContributors && (
97
+          <button
98
+            onClick={this.handleShowAllContributors}
99
+            className={classes.showAllContributors}
100
+            title="Current list only show the author and first 5 contributors unique by name"
101
+          >
102
+            Show all contributor
103
+          </button>
104
+        )}
105
+      </div>
106
+    );
107
+  }
108
+
109
+  render() {
110
+    let author = this.author;
111
+    const contributors = this.renderContributors();
112
+    return (
113
+      <Module title="Maintainers" className={classes.maintainersModule}>
114
+        {contributors.length || has(author, 'name') ? (
115
+          this.renderAuthorAndContributors(author)
116
+        ) : (
117
+          <ModuleContentPlaceholder text="Not Available!" />
118
+        )}
119
+      </Module>
120
+    );
121
+  }
122
+}

+ 13
- 0
components/PackageSidebar/modules/Maintainers/style.scss View File

@@ -0,0 +1,13 @@
1
+@import '../../../../styles/variables';
2
+
3
+.maintainersModule {
4
+  .showAllContributors {
5
+    cursor: pointer;
6
+    width: 100%;
7
+    background: none;
8
+    border: none;
9
+    font-size: $font-size-sm;
10
+    text-align: center;
11
+    padding: 10px 0;
12
+  }
13
+}

+ 18
- 0
components/PackageSidebar/modules/PeerDependencies/index.jsx View File

@@ -0,0 +1,18 @@
1
+import React from 'react';
2
+import PropTypes from 'prop-types';
3
+import Dependencies from '../Dependencies';
4
+
5
+export const TITLE = 'Peer Dependencies'
6
+
7
+const PeerDependencies = ({dependencies = {}, title = TITLE}) => {
8
+  return (
9
+    <Dependencies title={title} dependencies={dependencies} />
10
+  );
11
+};
12
+
13
+PeerDependencies.propTypes = {
14
+  dependencies: PropTypes.object,
15
+  title: PropTypes.string
16
+};
17
+
18
+export default PeerDependencies;

+ 15
- 0
components/Readme/index.js View File

@@ -0,0 +1,15 @@
1
+
2
+import React from 'react';
3
+import PropTypes from 'prop-types';
4
+
5
+import 'github-markdown-css';
6
+
7
+const Readme = (props) => {
8
+  return <div className="markdown-body" dangerouslySetInnerHTML={{__html: props.readMe}}/>;
9
+};
10
+
11
+Readme.propTypes = {
12
+  readMe: PropTypes.string.isRequired
13
+};
14
+
15
+export default Readme;

+ 6
- 0
components/Readme/readme.scss View File

@@ -0,0 +1,6 @@
1
+@import '../../styles/mixins';
2
+
3
+.searchBox {
4
+  @include searchBox;
5
+}
6
+

+ 35
- 0
components/Search/index.js View File

@@ -0,0 +1,35 @@
1
+
2
+import React from 'react';
3
+import PropTypes from 'prop-types';
4
+
5
+import classes from './search.scss';
6
+
7
+const noSubmit = (e) => {
8
+  e.preventDefault();
9
+};
10
+
11
+const Search = (props) => {
12
+    return (
13
+      <form autoComplete="off" onSubmit={noSubmit}>
14
+        <input
15
+          name="search-box"
16
+          type="text"
17
+          placeholder={props.placeHolder}
18
+          className={classes.searchBox}
19
+          onChange={props.handleSearchInput}
20
+          autoComplete="off"
21
+        />
22
+      </form>
23
+    );
24
+};
25
+
26
+Search.defaultProps = {
27
+  placeHolder: 'Type to search...'
28
+};
29
+
30
+Search.propTypes = {
31
+  handleSearchInput: PropTypes.func.isRequired,
32
+  placeHolder: PropTypes.string,
33
+};
34
+
35
+export default Search;

+ 5
- 0
components/Search/search.scss View File

@@ -0,0 +1,5 @@
1
+@import '../../styles/mixins';
2
+
3
+.searchBox {
4
+  @include searchBox;
5
+}

+ 26
- 0
index.js View File

@@ -0,0 +1,26 @@
1
+import './utils/__setPublicPath__';
2
+
3
+import React from 'react';
4
+import ReactDOM from 'react-dom';
5
+import {AppContainer} from 'react-hot-loader';
6
+
7
+import App from './app';
8
+
9
+let rootNode = document.getElementById('root');
10
+
11
+let renderApp = (Component) => {
12
+  ReactDOM.render(
13
+    <AppContainer>
14
+      <Component/>
15
+    </AppContainer>,
16
+    rootNode
17
+  );
18
+};
19
+
20
+renderApp(App);
21
+
22
+if (module.hot) {
23
+  module.hot.accept('./app', () => {
24
+    renderApp(App);
25
+  });
26
+}

+ 26
- 0
modules/detail/detail.scss View File

@@ -0,0 +1,26 @@
1
+@import '../../styles/variables';
2
+@import '../../styles/mixins';
3
+
4
+.twoColumn {
5
+  @include container-size;
6
+  margin: auto 10px;
7
+  display: flex;
8
+
9
+  > div {
10
+    &:first-child {
11
+      flex-shrink: 1;
12
+      min-width: 300px;
13
+      width: 100%;
14
+    }
15
+  }
16
+
17
+  > aside {
18
+    &:last-child {
19
+      margin-left: auto;
20
+
21
+      padding-left: 15px;
22
+      flex-shrink: 0;
23
+      width: 285px;
24
+    }
25
+  }
26
+}

+ 83
- 0
modules/detail/index.jsx View File

@@ -0,0 +1,83 @@
1
+import React, {Component} from 'react';
2
+import PropTypes from 'prop-types';
3
+import {Loading} from 'element-react';
4
+import isEmpty from 'lodash/isEmpty';
5
+
6
+import PackageDetail from '../../components/PackageDetail';
7
+import NotFound from '../../components/NotFound';
8
+import API from '../../utils/api';
9
+
10
+import classes from './detail.scss';
11
+import PackageSidebar from '../../components/PackageSidebar/index';
12
+
13
+const loadingMessage = 'Loading...';
14
+
15
+export default class Detail extends Component {
16
+  static propTypes = {
17
+    match: PropTypes.object,
18
+    isUserLoggedIn: PropTypes.bool
19
+  };
20
+
21
+  state = {
22
+    readMe: '',
23
+    notFound: false
24
+  };
25
+
26
+  getPackageName(props = this.props) {
27
+    const params = props.match.params;
28
+    return `${(params.scope && '@' + params.scope + '/') || ''}${
29
+      params.package
30
+    }`;
31
+  }
32
+  get packageName() {
33
+    return this.getPackageName();
34
+  }
35
+
36
+  async componentDidMount() {
37
+    await this.loadPackageInfo(this.packageName);
38
+  }
39
+
40
+  componentDidUpdate(prevProps) {
41
+    const condition1 = prevProps.isUserLoggedIn !== this.props.isUserLoggedIn;
42
+    const condition2 =
43
+      prevProps.match.params.package !== this.props.match.params.package;
44
+    if (condition1 || condition2) {
45
+      const packageName = this.getPackageName(this.props);
46
+      this.loadPackageInfo(packageName);
47
+    }
48
+  }
49
+
50
+  async loadPackageInfo(packageName) {
51
+    this.setState({
52
+      readMe: ''
53
+    });
54
+
55
+    try {
56
+      const resp = await API.request(`package/readme/${packageName}`, 'GET');
57
+      this.setState({
58
+        readMe: resp,
59
+        notFound: false
60
+      });
61
+    } catch (err) {
62
+      this.setState({
63
+        notFound: true
64
+      });
65
+    }
66
+  }
67
+
68
+  render() {
69
+    const {notFound, readMe} = this.state;
70
+
71
+    if (notFound) {
72
+      return <NotFound pkg={this.packageName} />;
73
+    } else if (isEmpty(readMe)) {
74
+      return <Loading text={loadingMessage} />;
75
+    }
76
+    return (
77
+      <div className={classes.twoColumn}>
78
+        <PackageDetail readMe={readMe} packageName={this.packageName} />
79
+        <PackageSidebar packageName={this.packageName} />
80
+      </div>
81
+    );
82
+  }
83
+}

+ 123
- 0
modules/home/index.js View File

@@ -0,0 +1,123 @@
1
+import React, {Component, Fragment} from 'react';
2
+import PropTypes from 'prop-types';
3
+import {Loading, MessageBox} from 'element-react';
4
+import isEmpty from 'lodash/isEmpty';
5
+import debounce from 'lodash/debounce';
6
+
7
+import API from '../../utils/api';
8
+
9
+import PackageList from '../../components/PackageList';
10
+import Search from '../../components/Search';
11
+
12
+export default class Home extends Component {
13
+  static propTypes = {
14
+    children: PropTypes.element,
15
+    isUserLoggedIn: PropTypes.bool
16
+  };
17
+
18
+  state = {
19
+    loading: true,
20
+    fistTime: true,
21
+    query: ''
22
+  };
23
+
24
+  constructor(props) {
25
+    super(props);
26
+    this.handleSearchInput = this.handleSearchInput.bind(this);
27
+    this.searchPackage = debounce(this.searchPackage, 800);
28
+  }
29
+
30
+  componentDidMount() {
31
+    this.loadPackages();
32
+  }
33
+
34
+  componentDidUpdate(prevProps, prevState) {
35
+    if (prevState.query !== this.state.query) {
36
+      if (this.req && this.req.abort) this.req.abort();
37
+      this.setState({
38
+        loading: true
39
+      });
40
+
41
+      if (prevState.query !== '' && this.state.query === '') {
42
+        this.loadPackages();
43
+      } else {
44
+        this.searchPackage(this.state.query);
45
+      }
46
+    }
47
+
48
+    if (prevProps.isUserLoggedIn !== this.props.isUserLoggedIn) {
49
+      this.loadPackages();
50
+    }
51
+  }
52
+
53
+  async loadPackages() {
54
+    try {
55
+      this.req = await API.request('packages', 'GET');
56
+
57
+      if (this.state.query === '') {
58
+        this.setState({
59
+          packages: this.req,
60
+          loading: false
61
+        });
62
+      }
63
+    } catch (error) {
64
+      MessageBox.msgbox({
65
+        type: 'error',
66
+        title: 'Warning',
67
+        message: `Unable to load package list: ${error.message}`
68
+      });
69
+    }
70
+  }
71
+
72
+  async searchPackage(query) {
73
+    try {
74
+      this.req = await API.request(`/search/${query}`, 'GET');
75
+
76
+      // Implement cancel feature later
77
+      if (this.state.query === query) {
78
+        this.setState({
79
+          packages: this.req,
80
+          fistTime: false,
81
+          loading: false
82
+        });
83
+      }
84
+    } catch (err) {
85
+      MessageBox.msgbox({
86
+        type: 'error',
87
+        title: 'Warning',
88
+        message: 'Unable to get search result, please try again later.'
89
+      });
90
+    }
91
+  }
92
+
93
+  handleSearchInput(e) {
94
+    this.setState({
95
+      query: e.target.value.trim()
96
+    });
97
+  }
98
+
99
+  isTherePackages() {
100
+    return isEmpty(this.state.packages);
101
+  }
102
+
103
+  render() {
104
+    const {packages, loading} = this.state;
105
+    return (
106
+      <Fragment>
107
+        {this.renderSearchBar()}
108
+        {loading ? (
109
+          <Loading text="Loading..." />
110
+        ) : (
111
+          <PackageList help={isEmpty(packages) === true} packages={packages} />
112
+        )}
113
+      </Fragment>
114
+    );
115
+  }
116
+
117
+  renderSearchBar() {
118
+    if (this.isTherePackages() && this.state.fistTime) {
119
+      return;
120
+    }
121
+    return <Search handleSearchInput={this.handleSearchInput} />;
122
+  }
123
+}

+ 46
- 0
router.js View File

@@ -0,0 +1,46 @@
1
+import React, {Component} from 'react';
2
+import PropTypes from 'prop-types';
3
+import {HashRouter as Router, Route, Switch} from 'react-router-dom';
4
+
5
+import {asyncComponent} from './utils/asyncComponent';
6
+
7
+const DetailPackage = asyncComponent(() => import('./modules/detail'));
8
+const HomePage = asyncComponent(() => import('./modules/home'));
9
+
10
+class RouterApp extends Component {
11
+  static propTypes = {
12
+    isUserLoggedIn: PropTypes.bool
13
+  };
14
+  render() {
15
+    const {isUserLoggedIn} = this.props;
16
+    return (
17
+      <Router>
18
+        <div className="container">
19
+          <Switch>
20
+            <Route
21
+              exact
22
+              path="/(search/:keyword)?"
23
+              render={() => <HomePage isUserLoggedIn={isUserLoggedIn} />}
24
+            />
25
+            <Route
26
+              exact
27
+              path="/detail/@:scope/:package"
28
+              render={(props) => (
29
+                <DetailPackage {...props} isUserLoggedIn={isUserLoggedIn} />
30
+              )}
31
+            />
32
+            <Route
33
+              exact
34
+              path="/detail/:package"
35
+              render={(props) => (
36
+                <DetailPackage {...props} isUserLoggedIn={isUserLoggedIn} />
37
+              )}
38
+            />
39
+          </Switch>
40
+        </div>
41
+      </Router>
42
+    );
43
+  }
44
+}
45
+
46
+export default RouterApp;

+ 22
- 0
styles/core.scss View File

@@ -0,0 +1,22 @@
1
+@import "variables";
2
+
3
+html,
4
+body {
5
+    height: 100%;
6
+}
7
+
8
+body {
9
+    font-family: $font-family-base;
10
+    font-size: $font-size-base;
11
+    color: $text-color;
12
+}
13
+
14
+ul {
15
+    margin: 0;
16
+    padding: 0;
17
+    list-style: none;
18
+}
19
+
20
+strong {
21
+    font-weight: $font-weight-semibold;
22
+}

+ 37
- 0
styles/global.scss View File

@@ -0,0 +1,37 @@
1
+@import "variables";
2
+@import "mixins";
3
+
4
+:global {
5
+  .container {
6
+    margin-top: $space-lg;
7
+
8
+    @include container-size;
9
+
10
+    .el-loading-spinner {
11
+      margin-top: 0 !important;
12
+    }
13
+  }
14
+
15
+  .page-full-height {
16
+    display: flex;
17
+    flex-direction: column;
18
+    min-height: 100vh;
19
+  }
20
+
21
+  .el-button {
22
+    &:hover, &:focus {
23
+      color: $primary-color;
24
+      border-color: $primary-color;
25
+    }
26
+  }
27
+
28
+  .el-input__inner {
29
+    &:hover, &:focus {
30
+      border-color: $primary-color;
31
+    }
32
+  }
33
+
34
+  .el-dialog__headerbtn:hover .el-dialog__close {
35
+    color: $eclipse;
36
+  }
37
+}

+ 2
- 0
styles/main.scss View File

@@ -0,0 +1,2 @@
1
+@import "core";
2
+@import "global";

+ 48
- 0
styles/mixins.scss View File

@@ -0,0 +1,48 @@
1
+@import "variables";
2
+
3
+@mixin border($direction, $width, $style, $color) {
4
+    border-#{$direction}: $width $style $color;
5
+}
6
+
7
+@mixin border-bottom-default($color) {
8
+    border-bottom: 1px solid $color;
9
+}
10
+
11
+@mixin searchBox {
12
+    width: 100%;
13
+    font-size: $font-size-md;
14
+    line-height: $line-height-xl;
15
+    border: none;
16
+    @include border-bottom-default($grey-light);
17
+    outline: none;
18
+
19
+    &:focus {
20
+        @include border-bottom-default($grey);
21
+    }
22
+}
23
+
24
+@mixin ellipsis {
25
+    display: inline-block;
26
+    overflow: hidden;
27
+    text-overflow: ellipsis;
28
+    white-space: nowrap;
29
+}
30
+
31
+@mixin fullSize {
32
+    position: absolute;
33
+    top: 0;
34
+    left: 0;
35
+    right: 0;
36
+    bottom: 0;
37
+}
38
+
39
+@mixin container-size {
40
+    margin-left: auto;
41
+    margin-right: auto;
42
+    width: 100%;
43
+    min-width: 400px;
44
+    max-width: $break-sm;
45
+    @media screen and (min-width: $break-lg) {
46
+        max-width: $break-lg;
47
+    }
48
+}

+ 64
- 0
styles/variables.scss View File

@@ -0,0 +1,64 @@
1
+// Verdaccio
2
+// -------------------------
3
+
4
+$black:         #000;
5
+$white:         #fff;
6
+$grey:          #808080;
7
+$grey-light:    #d3d3d3;
8
+$grey-dark:     #a9a9a9;
9
+
10
+$greyChateau:   #95989a;
11
+$greyGainsboro: #e3e3e3;
12
+$greyAthens:    #d3dddd;
13
+
14
+$eclipse:       #3c3c3c;
15
+$paleNavy:      #e4e8f1;
16
+$saltpan:       #f7f8f6;
17
+$snow:          #f9f9f9;
18
+
19
+$nobel-01:      #999999;
20
+$nobel-02:      #9f9f9f;
21
+
22
+// Main colors
23
+// -------------------------
24
+
25
+$primary-color: #4b5e40;
26
+$seconday-color:#20232a;
27
+
28
+// Scaffolding
29
+// -------------------------
30
+
31
+$body-bg:     $white; 
32
+$text-color:  $eclipse;
33
+
34
+// Typography
35
+// -------------------------
36
+
37
+ // Font Family from Bootstrap v4 Reboot.css
38
+$font-family-reboot: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
39
+$font-family-base: $font-family-reboot;
40
+
41
+$font-size-xxl:   26px;
42
+$font-size-xl:    24px;
43
+$font-size-lg:    21px;
44
+$font-size-md:    18px;
45
+$font-size-base:  16px;
46
+$font-size-sm:    14px;
47
+
48
+$line-height-xl:  30px;
49
+$line-height-sm:  18px;
50
+$line-height-xs:  2;
51
+$line-height-xxs: 1.5;
52
+
53
+$font-weight-light:     400;
54
+$font-weight-regular:   400;
55
+$font-weight-semibold:  600;
56
+$font-weight-bold:      700;
57
+
58
+$break-sm: 800px;
59
+$break-lg: 1240px;
60
+
61
+// Spacing
62
+// -------------------------
63
+
64
+$space-lg: 30px;

BIN
template/favicon.ico View File


+ 16
- 0
template/index.html View File

@@ -0,0 +1,16 @@
1
+<!DOCTYPE html>
2
+<html lang="en-us">
3
+<head>
4
+    <meta charset="utf-8">
5
+    <title><%= htmlWebpackPlugin.options.title %></title>
6
+    <link rel="icon" type="image/png" href="<%= htmlWebpackPlugin.options.verdaccioURL %>/-/static/favicon.ico"/>
7
+    <meta name="viewport" content="width=device-width, initial-scale=1">
8
+    <script>
9
+        window.VERDACCIO_API_URL = '<%= htmlWebpackPlugin.options.verdaccioURL %>/-/verdaccio/';
10
+        window.VERDACCIO_SCOPE = '<%= htmlWebpackPlugin.options.scope %>';
11
+    </script>
12
+</head>
13
+<body class="body">
14
+    <div id="root"></div>
15
+</body>
16
+</html>

+ 3
- 0
utils/__setPublicPath__.js View File

@@ -0,0 +1,3 @@
1
+if (!__DEBUG__) {
2
+  __webpack_public_path__ = window.VERDACCIO_API_URL.replace(/\/verdaccio\/$/, '/static/') // eslint-disable-line
3
+}

+ 59
- 0
utils/api.js View File

@@ -0,0 +1,59 @@
1
+import storage from './storage';
2
+
3
+class API {
4
+  request(url, method = 'GET', options = {}) {
5
+      if (!window.VERDACCIO_API_URL) {
6
+        throw new Error('VERDACCIO_API_URL is not defined!');
7
+      }
8
+
9
+      const token = storage.getItem('token');
10
+      if (token) {
11
+        if (!options.headers) options.headers = {};
12
+
13
+        options.headers.authorization = token;
14
+      }
15
+
16
+      if (!['http://', 'https://', '//'].some((prefix) => url.startsWith(prefix))) {
17
+        url = window.VERDACCIO_API_URL + url;
18
+      }
19
+
20
+      /**
21
+       * Handles response according to content type
22
+       * @param {object} response
23
+       * @returns {promise}
24
+       */
25
+      function handleResponseType(response) {
26
+        if (response.headers) {
27
+          const contentType = response.headers.get('Content-Type');
28
+          if (contentType.includes('application/pdf')) {
29
+            return Promise.all([response.ok, response.blob()]);
30
+          }
31
+          if (contentType.includes('application/json')) {
32
+            return Promise.all([response.ok, response.json()]);
33
+          }
34
+          // it includes all text types
35
+          if (contentType.includes('text/')) {
36
+            return Promise.all([response.ok, response.text()]);
37
+          }
38
+        }
39
+      }
40
+
41
+      return new Promise((resolve, reject) => {
42
+        fetch(url, {
43
+          method,
44
+          credentials: 'same-origin',
45
+          ...options
46
+        })
47
+        .then(handleResponseType)
48
+        .then(([responseOk, body]) => {
49
+          if (responseOk) {
50
+            resolve(body);
51
+          } else {
52
+            reject(body);
53
+          }
54
+        });
55
+      });
56
+    }
57
+}
58
+
59
+export default new API();

+ 24
- 0
utils/asyncComponent.js View File

@@ -0,0 +1,24 @@
1
+import React from 'react';
2
+
3
+export function asyncComponent(getComponent) {
4
+  return class AsyncComponent extends React.Component {
5
+    static Component = null;
6
+    state = {Component: AsyncComponent.Component};
7
+
8
+    componentDidMount() {
9
+      if (!this.state.Component) {
10
+        getComponent().then(({default: Component}) => {
11
+          AsyncComponent.Component = Component;
12
+          this.setState({Component});
13
+        });
14
+      }
15
+    }
16
+    render() {
17
+      const {Component} = this.state;
18
+      if (Component) {
19
+        return <Component {...this.props} />;
20
+      }
21
+      return null;
22
+    }
23
+  };
24
+}

+ 73
- 0
utils/login.js View File

@@ -0,0 +1,73 @@
1
+import isString from 'lodash/isString';
2
+import isNumber from 'lodash/isNumber';
3
+import isEmpty from 'lodash/isEmpty';
4
+import {Base64} from 'js-base64';
5
+import API from './api';
6
+import {HEADERS} from '../../lib/constants';
7
+
8
+export function isTokenExpire(token) {
9
+    if (!isString(token)) {
10
+        return true;
11
+    }
12
+
13
+    let [
14
+        ,
15
+        payload
16
+    ] = token.split('.');
17
+
18
+    if (!payload) {
19
+        return true;
20
+    }
21
+
22
+    try {
23
+        payload = JSON.parse(Base64.decode(payload));
24
+    } catch (error) {
25
+        // eslint-disable-next-line
26
+        console.error('Invalid token:', error, token);
27
+        return true;
28
+    }
29
+
30
+    if (!payload.exp || !isNumber(payload.exp)) {
31
+        return true;
32
+    }
33
+    // Report as expire before (real expire time - 30s)
34
+    const jsTimestamp = (payload.exp * 1000) - 30000;
35
+    const expired = Date.now() >= jsTimestamp;
36
+
37
+    return expired;
38
+}
39
+
40
+
41
+export async function makeLogin(username, password) {
42
+    // checks isEmpty
43
+    if (isEmpty(username) || isEmpty(password)) {
44
+        const error = {
45
+            title: 'Unable to login',
46
+            type: 'error',
47
+            description: 'Username or password can\'t be empty!'
48
+        };
49
+        return {error};
50
+    }
51
+
52
+    try {
53
+        const response = await API.request('login', 'POST', {
54
+            body: JSON.stringify({username, password}),
55
+            headers: {
56
+                Accept: HEADERS.JSON,
57
+                'Content-Type': HEADERS.JSON
58
+            }
59
+        });
60
+        const result = {
61
+            username: response.username,
62
+            token: response.token
63
+        };
64
+        return result;
65
+    } catch (e) {
66
+        const error = {
67
+            title: 'Unable to login',
68
+            type: 'error',
69
+            description: e.error
70
+        };
71
+        return {error};
72
+    }
73
+}

+ 10
- 0
utils/logo.js View File

@@ -0,0 +1,10 @@
1
+import API from './api';
2
+
3
+export default async function logo() {
4
+    try {
5
+        const logo = await API.request('logo');
6
+        return logo;
7
+    } catch (error) {
8
+        throw new Error(error);
9
+    }
10
+}

+ 92
- 0
utils/package.js View File

@@ -0,0 +1,92 @@
1
+import isString from 'lodash/isString';
2
+import isObject from 'lodash/isObject';
3
+export const TIMEFORMAT = 'YYYY/MM/DD, HH:mm:ss';
4
+import format from 'date-fns/format';
5
+import distanceInWordsToNow from 'date-fns/distance_in_words_to_now';
6
+
7
+/**
8
+ * Formats license field for webui.
9
+ * @see https://docs.npmjs.com/files/package.json#license
10
+ */
11
+export function formatLicense(license) {
12
+  if (isString(license)) {
13
+    return license;
14
+  }
15
+
16
+  if (isObject(license) && license.type) {
17
+    return license.type;
18
+  }
19
+
20
+  return null;
21
+}
22
+
23
+/**
24
+ * Formats repository field for webui.
25
+ * @see https://docs.npmjs.com/files/package.json#repository
26
+ */
27
+export function formatRepository(repository) {
28
+  if (isString(repository)) {
29
+    return repository;
30
+  }
31
+
32
+  if (isObject(repository) && repository.url) {
33
+    return repository.url;
34
+  }
35
+
36
+  return null;
37
+}
38
+
39
+
40
+/**
41
+ * Formats author field for webui.
42
+ * @see https://docs.npmjs.com/files/package.json#author
43
+ */
44
+export function formatAuthor(author) {
45
+    if (isString(author)) {
46
+        return author;
47
+    }
48
+
49
+    if (isObject(author) && author.name) {
50
+        return author.name;
51
+    }
52
+
53
+    return null;
54
+}
55
+
56
+/**
57
+ * For <LastSync /> component
58
+ * @param {array} uplinks
59
+ */
60
+export function getLastUpdatedPackageTime(uplinks = {}) {
61
+  let lastUpdate = 0;
62
+  Object.keys(uplinks).forEach((upLinkName) => {
63
+    const status = uplinks[upLinkName];
64
+    if (status.fetched > lastUpdate) {
65
+      lastUpdate = status.fetched;
66
+    }
67
+  });
68
+
69
+  return lastUpdate ? formatDate(lastUpdate) : '';
70
+}
71
+
72
+/**
73
+ * For <LastSync /> component
74
+ * @param {Object} time
75
+ * @returns {Array} last 3 releases
76
+ */
77
+export function getRecentReleases(time = {}) {
78
+  const recent = Object.keys(time).map((version) => ({
79
+    version,
80
+    time: formatDate(time[version])
81
+  }));
82
+  return recent.slice(recent.length - 3, recent.length).reverse();