1
0
mirror of https://github.com/SomboChea/ui synced 2024-12-24 05:04:26 +07:00

initial commit

This commit is contained in:
Priscila Oliveira 2019-02-03 11:23:33 +01:00
commit e2d478d65b
163 changed files with 19925 additions and 0 deletions

3
.babelrc Normal file
View File

@ -0,0 +1,3 @@
{
"presets": [["@verdaccio", { "flow": true }]]
}

12
.editorconfig Normal file
View File

@ -0,0 +1,12 @@
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
# 2 space indentation
[{.,}*.{js,jsx,yml,yaml}]
indent_style = space
indent_size = 2

7
.eslintignore Normal file
View File

@ -0,0 +1,7 @@
node_modules
coverage/
wiki/
static/
flow-typed/
website/
build/

128
.eslintrc Normal file
View File

@ -0,0 +1,128 @@
{
"plugins": [
"babel",
"react",
"flowtype",
"jest",
"verdaccio",
"jsx-a11y"
],
"extends": [
"eslint:recommended",
"google",
"plugin:flowtype/recommended",
"plugin:jest/recommended",
"plugin:prettier/recommended",
"plugin:react/recommended",
"plugin:verdaccio/recommended",
"plugin:jsx-a11y/recommended"
],
"settings": {
"react": {
"pragma": "React",
"version": "16.4.2",
"flowVersion": "0.81.0"
}
},
"parser": "babel-eslint",
"parserOptions": {
"sourceType": "module",
"ecmaVersion": 7,
"ecmaFeatures": {
"impliedStrict": true,
"jsx": true
}
},
"env": {
"browser": true,
"node": true,
"es6": true,
"jest": true
},
"globals": {
"__APP_VERSION__": true
},
"rules": {
"babel/no-invalid-this": 1,
"prettier/prettier": ["error", null, "@prettier"],
"react/no-deprecated": 1,
"react/jsx-no-target-blank": 1,
"react/destructuring-assignment": ["error", "always"],
"react/forbid-component-props": ["warn", { "forbid": ["style"] }],
"react/no-this-in-sfc": ["warn"],
"react/no-unsafe": ["warn"],
"react/sort-comp": ["warn", {
"order": [
"static-methods",
"lifecycle",
"render",
"everything-else",
"/^on.+$/",
"/^render.+$/"
]
}],
"react/void-dom-elements-no-children": ["warn"],
"react/no-did-mount-set-state": ["error", "disallow-in-func"],
"react/jsx-wrap-multilines": ["error",{
"declaration": "parens",
"assignment": "parens",
"return": "parens",
"arrow": "parens",
"condition": "parens",
"logical": "parens",
"prop": "parens"
}],
"react/jsx-boolean-value": ["error", "always"],
"react/jsx-closing-tag-location": ["error"],
"react/jsx-curly-spacing": ["error", "never"],
"react/jsx-equals-spacing": ["error", "never"],
"react/jsx-first-prop-new-line": ["error", "multiline-multiprop"],
"react/jsx-handler-names": ["warn"],
"react/jsx-indent": ["error", 2],
"react/jsx-indent-props": ["error", 2],
"react/jsx-key": ["error"],
"react/jsx-max-depth": ["error", { "max": 2}],
"react/jsx-max-props-per-line": ["error", {"maximum": 3, "when": "multiline" }],
"react/jsx-no-bind": ["error"],
"react/jsx-no-comment-textnodes": ["error"],
"react/jsx-no-duplicate-props": ["error"],
"react/jsx-no-literals": ["error"],
"react/jsx-no-undef": ["error"],
"react/jsx-one-expression-per-line": ["error", {"allow": "single-child"}],
"react/jsx-curly-brace-presence": ["error", { "props": "always", "children": "ignore" }],
"react/jsx-pascal-case": ["error"],
"react/jsx-props-no-multi-spaces": ["error"],
"react/jsx-sort-default-props": ["error"],
"react/jsx-sort-props": ["error"],
"react/no-string-refs": ["error"],
"react/no-danger-with-children": ["error"],
"react/jsx-tag-spacing": ["error", {
"closingSlash": "never",
"beforeSelfClosing": "always",
"afterOpening": "allow-multiline",
"beforeClosing": "allow"
}],
"react/prefer-es6-class": [
2,
"always"
],
"semi": ["error"],
"comma-dangle": ["error"],
"camelcase": 0,
"no-useless-escape": ["error"],
"no-invalid-this": 0,
"handle-callback-err": ["error"],
"no-fallthrough": ["error"],
"no-new-require": ["error"],
"max-len": ["error", 160],
"require-jsdoc": 0,
"valid-jsdoc": 0,
"prefer-spread": 1,
"prefer-rest-params": 1,
"linebreak-style": 0,
"quote-props":["error", "as-needed"],
"verdaccio/jsx-no-style": ["warn"],
"verdaccio/jsx-spread": ["warn"],
"jest/expect-expect": 0
}
}

25
.flowconfig Normal file
View File

@ -0,0 +1,25 @@
[ignore]
.*/node_modules/.*
.*/test/**/*.json
.*/static/.*
.*/test/unit/partials/.*
.*/.nyc_output/.*
.*/coverage/.*
.*/.vscode/.*
.*/build/.*
.*/docs/.*
.*/scripts/.*
.*/assets/.*
.*/bin/.*
.*/systemd/.*
.*/website/.*
.*/wiki/.*
.*/docs/.*
.*/tools/.*
[libs]
node_modules/@verdaccio/types/lib/
[options]
suppress_comment= \\(.\\|\n\\)*\\$FlowFixMe
module.ignore_non_literal_requires=true

24
.gitignore vendored Executable file
View File

@ -0,0 +1,24 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# Compiled script
/static
/dist
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

12
.prettierrc Normal file
View File

@ -0,0 +1,12 @@
{
"useTabs": false,
"printWidth": 160,
"tabWidth": 2,
"singleQuote": true,
"requirePragma": true,
"bracketSpacing": true,
"jsxBracketSameLine": true,
"trailingComma": "es5",
"semi": true,
"parser": "flow"
}

40
.stylelintrc Normal file
View File

@ -0,0 +1,40 @@
{
"processors": ["stylelint-processor-styled-components"],
"extends": [
"stylelint-config-recommended"
],
"rules": {
"at-rule-no-unknown": true,
"block-no-empty": true,
"color-named": "always-where-possible",
"comment-no-empty": true,
"declaration-block-no-duplicate-properties": [
true,
{
ignore: ["consecutive-duplicates-with-different-values"]
}
],
"declaration-block-no-shorthand-property-overrides": true,
"font-family-no-duplicate-names": true,
"color-no-invalid-hex": true,
"font-family-no-missing-generic-family-keyword": true,
"function-calc-no-unspaced-operator": true,
"function-linear-gradient-no-nonstandard-direction": true,
"keyframe-declaration-no-important": true,
"property-no-vendor-prefix": true,
"media-feature-name-no-unknown": true,
"no-descending-specificity": [true, { "severity": "warning" }],
"no-duplicate-at-import-rules": true,
"no-duplicate-selectors": true,
"no-empty-source": true,
"no-extra-semicolons": true,
"no-invalid-double-slash-comments": true,
"property-no-unknown": true,
"selector-pseudo-class-no-unknown": true,
"selector-pseudo-element-no-unknown": true,
"selector-type-no-unknown": [true, { "severity": "warning" }],
"string-no-newline": true,
"unit-no-unknown": true
}
}

1
README.md Executable file
View File

@ -0,0 +1 @@
under construction...

197
package.json Normal file
View File

@ -0,0 +1,197 @@
{
"name": "ui",
"version": "1.0.0",
"description": "Verdaccio User Interface",
"author": {
"name": "Verdaccio Core Team"
},
"repository": {
"type": "git",
"url": "git://github.com/verdaccio/io"
},
"main": "build/index.js",
"dependencies": {
"@verdaccio/file-locking": "0.0.7",
"@verdaccio/local-storage": "2.0.0-beta.1",
"@verdaccio/streams": "2.0.0-beta.0",
"JSONStream": "1.3.4",
"async": "2.6.1",
"body-parser": "1.18.3",
"bunyan": "1.8.12",
"chalk": "2.4.1",
"commander": "2.18.0",
"compression": "1.7.3",
"cookies": "0.7.2",
"cors": "2.8.4",
"date-fns": "1.29.0",
"express": "4.16.3",
"global": "4.3.2",
"handlebars": "4.0.12",
"http-errors": "1.7.1",
"js-base64": "2.4.9",
"js-string-escape": "1.0.1",
"js-yaml": "3.12.0",
"jsonwebtoken": "8.3.0",
"lockfile": "1.0.4",
"lodash": "4.17.11",
"lunr-mutable-indexes": "2.3.1",
"marked": "0.5.1",
"mime": "2.3.1",
"minimatch": "3.0.4",
"mkdirp": "0.5.1",
"mv": "2.1.1",
"pkginfo": "0.4.1",
"request": "2.88.0",
"semver": "5.5.1",
"verdaccio-audit": "1.1.0",
"verdaccio-htpasswd": "1.0.1",
"verror": "1.10.0"
},
"devDependencies": {
"@commitlint/cli": "7.2.1",
"@commitlint/config-conventional": "7.1.2",
"@material-ui/core": "3.1.0",
"@material-ui/icons": "3.0.1",
"@verdaccio/babel-preset": "0.0.3",
"@verdaccio/types": "4.1.4",
"autosuggest-highlight": "3.1.1",
"bundlesize": "0.17.0",
"codecov": "3.1.0",
"cross-env": "5.2.0",
"css-loader": "0.28.10",
"emotion": "9.2.12",
"enzyme": "3.6.0",
"enzyme-adapter-react-16": "1.5.0",
"eslint": "5.10.0",
"eslint-config-google": "0.11.0",
"eslint-config-prettier": "3.3.0",
"eslint-loader": "2.1.1",
"eslint-plugin-babel": "5.3.0",
"eslint-plugin-flowtype": "3.2.0",
"eslint-plugin-import": "2.14.0",
"eslint-plugin-jest": "22.1.2",
"eslint-plugin-jsx-a11y": "6.1.2",
"eslint-plugin-prettier": "3.0.0",
"eslint-plugin-react": "7.11.1",
"eslint-plugin-verdaccio": "0.0.5",
"file-loader": "2.0.0",
"flow-bin": "0.81.0",
"flow-runtime": "0.17.0",
"friendly-errors-webpack-plugin": "1.7.0",
"github-markdown-css": "2.10.0",
"html-webpack-plugin": "3.2.0",
"husky": "0.15.0-rc.8",
"identity-obj-proxy": "3.0.0",
"in-publish": "2.0.0",
"jest": "23.6.0",
"jest-environment-jsdom": "23.4.0",
"jest-environment-jsdom-global": "1.1.0",
"jest-environment-node": "23.4.0",
"lint-staged": "7.3.0",
"localstorage-memory": "1.0.2",
"mini-css-extract-plugin": "0.4.3",
"node-mocks-http": "1.7.0",
"node-sass": "4.9.3",
"normalize.css": "8.0.0",
"optimize-css-assets-webpack-plugin": "5.0.1",
"ora": "1.4.0",
"prettier": "1.14.3",
"prop-types": "15.6.2",
"puppeteer": "1.8.0",
"react": "16.4.2",
"react-autosuggest": "9.4.2",
"react-dom": "16.4.2",
"react-emotion": "9.2.12",
"react-hot-loader": "4.2.0",
"react-router": "4.3.1",
"react-router-dom": "4.2.2",
"resolve-url-loader": "3.0.0",
"rimraf": "2.6.2",
"sass-loader": "7.1.0",
"source-map-loader": "0.2.4",
"standard-version": "4.4.0",
"style-loader": "0.23.0",
"stylelint": "9.9.0",
"stylelint-config-recommended": "2.1.0",
"stylelint-config-recommended-scss": "3.2.0",
"stylelint-config-styled-components": "0.1.1",
"stylelint-processor-styled-components": "1.5.1",
"stylelint-scss": "3.3.1",
"stylelint-webpack-plugin": "0.10.5",
"supertest": "3.3.0",
"typeface-roboto": "0.0.54",
"url-loader": "1.1.1",
"verdaccio-auth-memory": "0.0.4",
"verdaccio-memory": "2.0.0-beta.0",
"webpack": "4.20.2",
"webpack-bundle-analyzer": "3.0.2",
"webpack-cli": "3.1.1",
"webpack-dev-server": "3.1.14",
"webpack-merge": "4.1.4",
"whatwg-fetch": "3.0.0"
},
"keywords": [
"verdaccio",
"verdaccio-ui"
],
"scripts": {
"flow": "flow check",
"pretest": "npm run code:build",
"test": "npm run test:unit",
"test:clean": "npx jest --clearCache",
"test:unit": "cross-env NODE_ENV=test BABEL_ENV=test TZ=UTC FORCE_COLOR=1 jest --config ./jest.config.js --maxWorkers 2 --passWithNoTests",
"test:functional": "cross-env NODE_ENV=test jest --config ./test/jest.config.functional.js --testPathPattern ./test/functional/index* --passWithNoTests",
"test:e2e": "cross-env BABEL_ENV=test jest --config ./test/jest.config.e2e.js",
"test:size": "bundlesize",
"test:all": "npm run build:webui && npm run test && npm run test:functional && npm run test:e2e && npm run test:size",
"pre:ci": "npm run lint && npm run build:webui",
"coverage:publish": "codecov",
"lint": "npm run flow && npm run lint:js && npm run lint:css",
"lint:js": "eslint .",
"lint:css": "stylelint 'src/webui/**/styles.js'",
"dev:start": "cross-env BABEL_ENV=registry babel-node src/lib/cli",
"code:build": "cross-env BABEL_ENV=registry babel src/ --out-dir build/ --ignore src/webui/ --copy-files",
"code:docker-build": "cross-env BABEL_ENV=registry-docker babel src/ --out-dir build/ --ignore src/webui/ --copy-files",
"pre:webpack": "rimraf static/*",
"dev:webui": "cross-env BABEL_ENV=ui babel-node tools/dev.server.js",
"build:webui": "npm run pre:webpack && cross-env BABEL_ENV=ui webpack --config tools/webpack.prod.config.babel.js",
"build:docker": "docker build -t verdaccio . --no-cache",
"build:docker:rpi": "docker build -f Dockerfile.rpi -t verdaccio:rpi ."
},
"engines": {
"node": ">=6.12.0",
"npm": ">=3"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged && commitlint -e $GIT_PARAMS"
}
},
"lint-staged": {
"linters": {
"*.yaml": [
"prettier --parser yaml --no-config --single-quote --write",
"git add"
],
"*": [
"eslint .",
"prettier --write",
"git add"
]
},
"ignore": [
"*.json"
]
},
"license": "MIT",
"commitlint": {
"extends": [
"@commitlint/config-conventional"
]
},
"collective": {
"type": "opencollective",
"url": "https://opencollective.com/verdaccio",
"logo": "https://opencollective.com/verdaccio/logo.txt"
}
}

30
src/.eslintrc Normal file
View File

@ -0,0 +1,30 @@
{
"env": {
"browser": true,
"node": true,
"jest": true,
"es6": true
},
"globals": {
"__DEBUG__": true
},
"rules": {
"require-jsdoc": 0,
"camelcase": ["error"],
"no-console": [
1,
{
"allow": [
"log"
]
}
],
"no-unused-vars": [
2,
{
"vars": "all",
"args": "all"
}
]
}
}

186
src/app.js Normal file
View File

@ -0,0 +1,186 @@
import React, {Component, Fragment} from 'react';
import isNil from 'lodash/isNil';
import storage from './utils/storage';
import {makeLogin, isTokenExpire} from './utils/login';
import Loading from './components/Loading';
import LoginModal from './components/Login';
import Header from './components/Header';
import {Container, Content} from './components/Layout';
import RouterApp from './router';
import API from './utils/api';
import './styles/typeface-roboto.scss';
import './styles/main.scss';
import 'normalize.css';
import Footer from './components/Footer';
export const AppContext = React.createContext();
export const AppContextProvider = AppContext.Provider;
export const AppContextConsumer = AppContext.Consumer;
export default class App extends Component {
state = {
error: {},
logoUrl: window.VERDACCIO_LOGO,
user: {},
scope: window.VERDACCIO_SCOPE ? `${window.VERDACCIO_SCOPE}:` : '',
showLoginModal: false,
isUserLoggedIn: false,
packages: [],
isLoading: true,
};
componentDidMount() {
this.isUserAlreadyLoggedIn();
this.loadOnHandler();
}
// eslint-disable-next-line no-unused-vars
componentDidUpdate(_, prevState) {
const {isUserLoggedIn} = this.state;
if (prevState.isUserLoggedIn !== isUserLoggedIn) {
this.loadOnHandler();
}
}
isUserAlreadyLoggedIn = () => {
// checks for token validity
const token = storage.getItem('token');
const username = storage.getItem('username');
if (isTokenExpire(token) || isNil(username)) {
this.handleLogout();
} else {
this.setState({
user: {username, token},
isUserLoggedIn: true,
});
}
};
loadOnHandler = async () => {
try {
this.req = await API.request('packages', 'GET');
this.setState({
packages: this.req,
isLoading: false,
});
} catch (error) {
// FIXME: add dialog
console.error({
title: 'Warning',
message: `Unable to load package list: ${error.message}`,
});
this.setLoading(false);
}
};
setLoading = (isLoading) =>
this.setState({
isLoading,
});
/**
* Toggles the login modal
* Required by: <LoginModal /> <Header />
*/
handleToggleLoginModal = () => {
this.setState((prevState) => ({
showLoginModal: !prevState.showLoginModal,
error: {},
}));
};
/**
* handles login
* Required by: <Header />
*/
handleDoLogin = async (usernameValue, passwordValue) => {
const {username, token, error} = await makeLogin(usernameValue, passwordValue);
if (username && token) {
this.setLoggedUser(username, token);
storage.setItem('username', username);
storage.setItem('token', token);
}
if (error) {
this.setState({
user: {},
error,
});
}
};
setLoggedUser = (username, token) => {
this.setState({
user: {
username,
token,
},
isUserLoggedIn: true, // close login modal after successful login
showLoginModal: false, // set isUserLoggedIn to true
});
};
/**
* Logouts user
* Required by: <Header />
*/
handleLogout = () => {
storage.removeItem('username');
storage.removeItem('token');
this.setState({
user: {},
isUserLoggedIn: false,
});
};
render() {
const {isLoading, isUserLoggedIn, packages, logoUrl, user, scope} = this.state;
return (
<Container isLoading={isLoading}>
{isLoading ? (
<Loading />
) : (
<Fragment>
<AppContextProvider value={{isUserLoggedIn, packages, logoUrl, user, scope}}>{this.renderContent()}</AppContextProvider>
</Fragment>
)}
{this.renderLoginModal()}
</Container>
);
}
renderLoginModal = () => {
const {error, showLoginModal} = this.state;
return (
<LoginModal
error={error}
onCancel={this.handleToggleLoginModal}
onChange={this.handleSetUsernameAndPassword}
onSubmit={this.handleDoLogin}
visibility={showLoginModal}
/>
);
};
renderContent = () => {
return (
<Fragment>
<Content>
<RouterApp onLogout={this.handleLogout} onToggleLoginModal={this.handleToggleLoginModal}>
{this.renderHeader()}
</RouterApp>
</Content>
<Footer />
</Fragment>
);
};
renderHeader = () => {
const {logoUrl, user, scope} = this.state;
return <Header logo={logoUrl} onLogout={this.handleLogout} onToggleLoginModal={this.handleToggleLoginModal} scope={scope} username={user.username} />;
};
}

16
src/app.scss Normal file
View File

@ -0,0 +1,16 @@
@import "./styles/variables";
.alertError {
background-color: $red !important;
min-width: inherit !important;
}
.alertErrorMsg {
display: flex;
align-items: center;
}
.alertIcon {
opacity: 0.9;
margin-right: 8px;
}

View File

@ -0,0 +1,51 @@
/* eslint no-unused-vars: 0 */
import React, {Component, Fragment} from 'react';
import {DetailContextConsumer} from '../../pages/version/index';
import Card from '@material-ui/core/Card/index';
import CardContent from '@material-ui/core/CardContent/index';
import CopyToClipBoard from '../CopyToClipBoard';
import CardHeader from '@material-ui/core/CardHeader/index';
import Avatar from '@material-ui/core/Avatar';
import CardActions from '@material-ui/core/CardActions';
import Typography from '@material-ui/core/Typography/index';
class Authors extends Component<any, any> {
render() {
return (
<DetailContextConsumer>
{(context) => {
return this.renderAuthor(context);
}}
</DetailContextConsumer>
);
}
renderAuthor = ({packageMeta}) => {
const {author} = packageMeta.latest;
if (!author) {
return null;
}
return (
<Card>
<CardContent>{this.renderAvatar(author)}</CardContent>
</Card>
);
};
renderAvatar = ({name, email, url, avatar}) => {
return (
<Fragment>
<Avatar aria-label={name} src={avatar} />
<Typography color={'textPrimary'} gutterBottom={true} variant={'caption'}>
{name}
</Typography>
</Fragment>
);
};
}
export default Authors;

View File

@ -0,0 +1,129 @@
/**
* @prettier
* @flow
*/
import React from 'react';
import type { Node } from 'react';
import Autosuggest from 'react-autosuggest';
import match from 'autosuggest-highlight/match';
import parse from 'autosuggest-highlight/parse';
import Paper from '@material-ui/core/Paper';
import MenuItem from '@material-ui/core/MenuItem';
import { fontWeight } from '../../utils/styles/sizes';
import { Wrapper, InputField } from './styles';
import { IProps } from './types';
const renderInputComponent = (inputProps): Node => {
const { ref, startAdornment, disableUnderline, onKeyDown, ...others } = inputProps;
return (
<InputField
InputProps={{
inputRef: node => {
ref(node);
},
startAdornment,
disableUnderline,
onKeyDown,
}}
fullWidth={true}
{...others}
/>
);
};
const getSuggestionValue = (suggestion): string => suggestion.name;
const renderSuggestion = (suggestion, { query, isHighlighted }): Node => {
const matches = match(suggestion.name, query);
const parts = parse(suggestion.name, matches);
return (
<MenuItem component={'div'} selected={isHighlighted}>
<div>
{parts.map((part, index) => {
return part.highlight ? (
<span href={suggestion.link} key={String(index)} style={{ fontWeight: fontWeight.semiBold }}>
{part.text}
</span>
) : (
<span href={suggestion.link} key={String(index)} style={{ fontWeight: fontWeight.light }}>
{part.text}
</span>
);
})}
</div>
</MenuItem>
);
};
const renderMessage = (message): Node => {
return (
<MenuItem component={'div'} selected={false}>
<div>{message}</div>
</MenuItem>
);
};
const SUGGESTIONS_RESPONSE = {
LOADING: 'Loading...',
FAILURE: 'Something went wrong.',
NO_RESULT: 'No results found.',
};
const AutoComplete = ({
suggestions,
startAdornment,
onChange,
onSuggestionsFetch,
onCleanSuggestions,
value = '',
placeholder = '',
disableUnderline = false,
color,
onClick,
onKeyDown,
onBlur,
suggestionsLoading = false,
suggestionsLoaded = false,
suggestionsError = false,
}: IProps): Node => {
const autosuggestProps = {
renderInputComponent,
suggestions,
getSuggestionValue,
renderSuggestion,
onSuggestionsFetchRequested: onSuggestionsFetch,
onSuggestionsClearRequested: onCleanSuggestions,
};
const inputProps = {
value,
onChange,
placeholder,
startAdornment,
disableUnderline,
color,
onKeyDown,
onBlur,
};
// this format avoid arrow function eslint rule
function renderSuggestionsContainer({ containerProps, children, query }) {
return (
<Paper {...containerProps} square={true}>
{suggestionsLoaded && children === null && query && renderMessage(SUGGESTIONS_RESPONSE.NO_RESULT)}
{suggestionsLoading && query && renderMessage(SUGGESTIONS_RESPONSE.LOADING)}
{suggestionsError && renderMessage(SUGGESTIONS_RESPONSE.FAILURE)}
{children}
</Paper>
);
}
return (
<Wrapper>
<Autosuggest {...autosuggestProps} inputProps={inputProps} onSuggestionSelected={onClick} renderSuggestionsContainer={renderSuggestionsContainer} />
</Wrapper>
);
};
export default AutoComplete;

View File

@ -0,0 +1,52 @@
/**
* @prettier
* @flow
*/
import React from 'react';
import styled, { css } from 'react-emotion';
import TextField from '../TextField';
import { IInputField } from './types';
export const Wrapper = styled.div`
&& {
width: 100%;
height: 32px;
position: relative;
z-index: 1;
}
`;
export const InputField = ({ color, ...others }: IInputField) => (
<TextField
{...others}
classes={{
input: css`
&& {
${color &&
css`
color: ${color};
`};
}
`,
root: css`
&& {
&:before {
content: '';
border: none;
}
&:after {
${color &&
css`
border-color: ${color};
`};
}
&:hover:before {
content: none;
}
}
`,
}}
/>
);

View File

@ -0,0 +1,29 @@
/**
* @prettier
* @flow
*/
import { InputAdornmentProps } from '@material-ui/core/InputAdornment';
export interface IProps {
suggestions: any[];
suggestionsLoading?: boolean;
suggestionsLoaded?: boolean;
suggestionsError?: boolean;
apiLoading?: boolean;
color?: string;
value?: string;
placeholder?: string;
startAdornment?: React.ComponentType<InputAdornmentProps>;
disableUnderline?: boolean;
onChange?: (event: SyntheticKeyboardEvent<HTMLInputElement>, { newValue: string, method: string }) => void;
onSuggestionsFetch?: ({ value: string }) => Promise<void>;
onCleanSuggestions?: () => void;
onClick?: (event: SyntheticKeyboardEvent<HTMLInputElement>, { suggestionValue: any[], method: string }) => void;
onKeyDown?: (event: SyntheticKeyboardEvent<HTMLInputElement>) => void;
onBlur?: (event: SyntheticKeyboardEvent<HTMLInputElement>) => void;
}
export interface IInputField {
color: string;
}

View File

@ -0,0 +1,33 @@
/**
* @prettier
* @flow
*/
import React from 'react';
import FileCopy from '@material-ui/icons/FileCopy';
import Tooltip from '@material-ui/core/Tooltip/index';
import type {Node} from 'react';
import {IProps} from './types';
import {ClipBoardCopy, ClipBoardCopyText, CopyIcon} from './styles';
import {copyToClipBoardUtility} from '../../utils/cli-utils';
import {TEXT} from '../../utils/constants';
const CopyToClipBoard = ({text}: IProps): Node => {
const renderToolTipFileCopy = () => (
<Tooltip disableFocusListener={true} title={TEXT.CLIPBOARD_COPY}>
<CopyIcon onClick={copyToClipBoardUtility(text)}>
<FileCopy />
</CopyIcon>
</Tooltip>
);
return (
<ClipBoardCopy>
<ClipBoardCopyText>{text}</ClipBoardCopyText>
{renderToolTipFileCopy()}
</ClipBoardCopy>
);
};
export default CopyToClipBoard;

View File

@ -0,0 +1,26 @@
import styled from 'react-emotion';
import IconButton from '@material-ui/core/IconButton/index';
export const ClipBoardCopy = styled.div`
&& {
display: flex;
align-items: center;
justify-content: space-between;
}
`;
export const ClipBoardCopyText = styled.span`
&& {
display: inline-block;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
height: 21px;
}
`;
export const CopyIcon = styled(IconButton)`
&& {
margin: 0 0 0 10px;
}
`;

View File

@ -0,0 +1,8 @@
/**
* @prettier
* @flow
*/
export interface IProps {
text: string;
}

View File

@ -0,0 +1,108 @@
/**
* @prettier
* @flow
*/
/* eslint react/jsx-max-depth: 0 */
import React, { Component, Fragment } from 'react';
import { withRouter } from 'react-router-dom';
import CardContent from '@material-ui/core/CardContent/index';
import { DetailContextConsumer } from '../../pages/version';
import { Content, CardWrap, Heading, Tags, Tag } from './styles';
class DepDetail extends Component<any, any> {
constructor(props: any) {
super(props);
const { name, version } = this.props;
this.state = {
name,
version,
};
}
render() {
const { name, version } = this.state;
const tagText = `${name}@${version}`;
return <Tag clickable={true} component={'div'} label={tagText} onClick={this.handleOnClick} />;
}
handleOnClick = () => {
const { name } = this.state;
const { onLoading, history } = this.props;
onLoading();
history.push(`/-/web/version/${name}`);
};
}
const WrappDepDetail = withRouter(DepDetail);
class DependencyBlock extends Component<any, any> {
renderTags = (deps: any, enableLoading: any) =>
deps.map(dep => {
const [name, version] = dep;
return <WrappDepDetail key={name} name={name} onLoading={enableLoading} version={version} />;
});
render() {
const { dependencies, title } = this.props;
const deps = Object.entries(dependencies);
return (
// $FlowFixMe
<DetailContextConsumer>
{({ enableLoading }) => {
return (
<CardWrap>
<CardContent>
<Heading variant={'subheading'}>{title}</Heading>
<Tags>{this.renderTags(deps, enableLoading)}</Tags>
</CardContent>
</CardWrap>
);
}}
</DetailContextConsumer>
);
}
}
class Dependencies extends Component<any, any> {
state = {
tabPosition: 0,
};
render() {
return (
<DetailContextConsumer>
{packageMeta => {
return this.renderDependencies(packageMeta);
}}
</DetailContextConsumer>
);
}
// $FlowFixMe
renderDependencies({ packageMeta }) {
const { latest } = packageMeta;
// console.log('renderDependencies', latest);
const { dependencies, devDependencies, peerDependencies } = latest;
// console.log('dependencies', dependencies);
// console.log('devDependencies', devDependencies);
return (
<Content>
<Fragment>
{dependencies && <DependencyBlock dependencies={dependencies} title={'Dependencies'} />}
{devDependencies && <DependencyBlock dependencies={devDependencies} title={'DevDependencies'} />}
{peerDependencies && <DependencyBlock dependencies={peerDependencies} title={'PeerDependencies'} />}
</Fragment>
</Content>
);
}
}
export default Dependencies;

View File

@ -0,0 +1,42 @@
/**
* @prettier
* @flow
*/
import styled from 'react-emotion';
import Card from '@material-ui/core/Card/index';
import Typography from '@material-ui/core/Typography/index';
import Chip from '@material-ui/core/Chip/index';
export const Content = styled.div`
&& {
padding: 20px;
}
`;
export const CardWrap = styled(Card)`
&& {
margin: 0 0 25px;
}
`;
export const Heading = styled(Typography)`
&& {
font-weight: 700;
}
`;
export const Tags = styled('div')`
&& {
display: flex;
justify-content: start;
flex-wrap: wrap;
margin: 0 -5px;
}
`;
export const Tag = styled(Chip)`
&& {
margin: 5px;
}
`;

View File

@ -0,0 +1,12 @@
/**
* @prettier
* @flow
*/
import type { Node } from 'react';
export interface IProps {
children: Node;
open: boolean;
onClose: () => void;
}

View File

@ -0,0 +1,67 @@
/**
* @prettier
* @flow
*/
import React, { Component } from 'react';
import { DetailContextConsumer } from '../../pages/version/index';
import Readme from '../Readme';
import Versions from '../Versions';
import { preventXSS } from '../../utils/sec-utils';
import Tabs from '@material-ui/core/Tabs/index';
import Tab from '@material-ui/core/Tab/index';
import { Content } from './styles';
import Dependencies from '../Dependencies';
import UpLinks from '../UpLinks';
class DetailContainer extends Component<any, any> {
state = {
tabPosition: 0,
};
render() {
return (
<DetailContextConsumer>
{context => {
return this.renderTabs(context);
}}
</DetailContextConsumer>
);
}
// $FlowFixMe
renderTabs = ({ readMe }) => {
const { tabPosition } = this.state;
return (
<React.Fragment>
<Tabs indicatorColor={'primary'} onChange={this.handleChange} textColor={'primary'} value={tabPosition} variant={'fullWidth'}>
<Tab label={'Readme'} />
<Tab label={'Dependencies'} />
<Tab label={'Versions'} />
<Tab label={'Uplinks'} />
</Tabs>
<Content>
{tabPosition === 0 && this.renderReadme(readMe)}
{tabPosition === 1 && <Dependencies />}
{tabPosition === 2 && <Versions />}
{tabPosition === 3 && <UpLinks />}
</Content>
</React.Fragment>
);
};
renderReadme = (readMe: string) => {
const encodedReadme = preventXSS(readMe);
return <Readme description={encodedReadme} />;
};
handleChange = (event: any, tabPosition: number) => {
event.preventDefault();
this.setState({ tabPosition });
};
}
export default DetailContainer;

View File

@ -0,0 +1,12 @@
/**
* @prettier
* @flow
*/
import styled from 'react-emotion';
export const Content = styled.div`
&& {
padding: 15px;
}
`;

View File

@ -0,0 +1,12 @@
/**
* @prettier
* @flow
*/
import type { Node } from 'react';
export interface IProps {
children: Node;
open: boolean;
onClose: () => void;
}

View File

@ -0,0 +1,102 @@
import React, {Component} from 'react';
import { DetailContextConsumer } from '../../pages/version/index';
import Typography from '@material-ui/core/Typography/index';
import Grid from '@material-ui/core/Grid/index';
import Install from '../Install';
import { Content } from './styles';
import Authors from '../Author';
import License from '../License';
import Repository from '../Repository';
import Developers from '../Developers';
class DetailSidebar extends Component<any, any> {
render() {
return (
<DetailContextConsumer>
{(context) => this.renderSideBar(context)}
</DetailContextConsumer>
);
};
renderSideBar = ({packageMeta, packageName}) => {
return (
<Content className={'sidebar-info'}>
<Grid container={true} spacing={24}>
<Grid item={true} xs={12}>
{this.renderTitle(packageName, packageMeta)}
</Grid>
<Grid className={'detail-info'} item={true} xs={12}>
{this.renderCopyCLI()}
</Grid>
<Grid item={true} xs={12}>
{this.renderSecondLevel(8)}
</Grid>
<Grid item={true} xs={12}>
{this.renderMaintainers()}
</Grid>
<Grid item={true} xs={12}>
{this.renderContributors()}
</Grid>
<Grid item={true} xs={12}>
{this.renderRepository()}
</Grid>
</Grid>
</Content>
);
}
renderTitle = (packageName, packageMeta) => {
return (
<>
<Typography color={"textPrimary"} gutterBottom={true} variant={'title'}>
{packageName}
</Typography>
<Typography color={"textSecondary"} gutterBottom={true} variant={'body2'}>
{packageMeta.latest.description}
</Typography>
</>
);
}
renderCopyCLI = () => {
return <Install />;
}
renderMaintainers = () => {
return <Developers type={'maintainers'} />;
}
renderContributors = () => {
return <Developers type={'contributors'} />;
}
renderSecondLevel = (spacing = 24) => {
return (
<Grid container={true} spacing={spacing}>
{this.renderAuthor()}
</Grid>
);
}
renderRepository = () => {
return <Repository />;
}
renderAuthor = () => {
return (
<>
<Grid item={true} xs={6}>
<Authors />
</Grid>
<Grid item={true} xs={6}>
<License />
</Grid>
</>
);
}
}
export default DetailSidebar;

View File

@ -0,0 +1,14 @@
/**
* @prettier
* @flow
*/
import styled from 'react-emotion';
import colors from '../../utils/styles/colors';
export const Content = styled.div`
&& {
padding: 10px;
background-color: ${colors.white};
}
`;

View File

@ -0,0 +1,12 @@
/**
* @prettier
* @flow
*/
import type { Node } from 'react';
export interface IProps {
children: Node;
open: boolean;
onClose: () => void;
}

View File

@ -0,0 +1,67 @@
import React, {Component} from 'react';
import { DetailContextConsumer } from '../../pages/version';
import Card from '@material-ui/core/Card';
import Avatar from '@material-ui/core/Avatar';
import Tooltip from '@material-ui/core/Tooltip';
import Add from '@material-ui/icons/Add';
import { Details, Heading, Content, CardContent, Fab } from './styles';
interface Props {
type: 'contributors' | 'maintainers'
}
class Developers extends Component<Props, any> {
state = {
visibleDevs: 6,
};
render() {
return (
<DetailContextConsumer>
{({ packageMeta }) => {
const { type } = this.props;
const developerType = packageMeta.latest[type];
if (!developerType || developerType.length === 0) return null;
return this.renderDevelopers(developerType);
}}
</DetailContextConsumer>
);
};
renderDevelopers = (developers) => {
const { type } = this.props;
const { visibleDevs } = this.state;
return (
<Card>
<CardContent>
<Heading variant={'subheading'}>{type}</Heading>
<Content>
{developers.slice(0, visibleDevs).map(developer => (
<Details key={developer.email}>{this.renderDeveloperDetails(developer)}</Details>
))}
{visibleDevs < developers.length &&
<Fab onClick={this.handleLoadMore} size={'small'}><Add /></Fab>
}
</Content>
</CardContent>
</Card>
);
}
renderDeveloperDetails= ({name, avatar }) => {
return (
<Tooltip title={name}>
<Avatar aria-label={name} src={avatar} />
</Tooltip>
);
}
handleLoadMore = () => {
this.setState((prev) => ({ visibleDevs: prev.visibleDevs + 6 }));
}
}
export default Developers;

View File

@ -0,0 +1,46 @@
/**
* @prettier
*/
import styled from 'react-emotion';
import Typography from '@material-ui/core/Typography';
import { default as MuiFab } from '@material-ui/core/Fab';
import colors from '../../utils/styles/colors';
import { default as MuiCardContent } from '@material-ui/core/CardContent/index';
export const Details = styled('span')`
display: flex;
flex-direction: column;
align-items: center;
`;
export const Content = styled('div')`
margin: -5px;
display: flex;
flex-wrap: wrap;
> * {
margin: 5px;
}
`;
export const CardContent = styled(MuiCardContent)`
&& {
padding-bottom: 20px;
}
`;
export const Heading = styled(Typography)`
&& {
font-weight: 700;
margin-bottom: 10px;
text-transform: capitalize;
}
`;
export const Fab = styled(MuiFab)`
&& {
background-color: ${colors.primary};
color: ${colors.white};
}
`;

View File

@ -0,0 +1,75 @@
/**
* @prettier
* @flow
*/
import React from "react";
import type { Element } from "react";
import { version } from "../../../package.json";
import {
Wrapper,
Left,
Right,
Earth,
Flags,
Love,
Flag,
Logo,
Inner,
ToolTip
} from "./styles";
import { goToVerdaccioWebsite } from "../../utils/windows.js";
const renderTooltip = () => (
<ToolTip>
<Earth name={"earth"} size={"md"} />
<Flags>
<Flag name={"spain"} size={"md"} />
<Flag name={"nicaragua"} size={"md"} />
<Flag name={"india"} size={"md"} />
<Flag name={"brazil"} size={"md"} />
<Flag name={"pakistan"} size={"md"} />
<Flag name={"china"} size={"md"} />
<Flag name={"austria"} size={"md"} />
</Flags>
</ToolTip>
);
const POWERED_LABEL = "Powered by";
const MADEWITH_LABEL = " Made with";
const ON_LABEL = "on";
const HEARTH_EMOJI = "♥";
const renderRight = () => (
<Right>
{POWERED_LABEL}
<Logo
img={true}
name={"verdaccio"}
onClick={goToVerdaccioWebsite}
pointer={true}
size={"md"}
/>
{`/ ${version}`}
</Right>
);
const renderLeft = () => (
<Left>
{MADEWITH_LABEL}
<Love>{HEARTH_EMOJI}</Love>
{ON_LABEL}
{renderTooltip()}
</Left>
);
const Footer = (): Element<Wrapper> => (
<Wrapper>
<Inner>
{renderLeft()}
{renderRight()}
</Inner>
</Wrapper>
);
export default Footer;

View File

@ -0,0 +1,107 @@
/**
* @prettier
* @flow
*/
import styled, { css } from 'react-emotion';
import mq from '../../utils/styles/media';
import Icon from '../Icon';
export const Wrapper = styled.div`
&& {
background: #f9f9f9;
border-top: 1px solid #e3e3e3;
color: #999999;
font-size: 14px;
padding: 20px;
}
`;
export const Inner = styled.div`
&& {
display: flex;
align-items: center;
justify-content: flex-end;
width: 100%;
${mq.medium(css`
min-width: 400px;
max-width: 800px;
margin: auto;
justify-content: space-between;
`)};
${mq.large(css`
max-width: 1240px;
`)};
}
`;
export const Left = styled.div`
&& {
align-items: center;
display: none;
${mq.medium(css`
display: flex;
`)};
}
`;
export const Right = styled(Left)`
&& {
display: flex;
}
`;
export const ToolTip = styled.span`
&& {
position: relative;
height: 18px;
}
`;
export const Earth = styled(Icon)`
&& {
padding: 0 10px;
}
`;
export const Flags = styled.span`
&& {
position: absolute;
background: #d3dddd;
padding: 1px 4px;
border-radius: 3px;
height: 20px;
display: inline-flex;
align-items: center;
visibility: hidden;
top: -2px;
:before {
content: '';
position: absolute;
top: 29%;
left: -4px;
margin-left: -5px;
border: 5px solid;
border-color: #d3dddd transparent transparent transparent;
transform: rotate(90deg);
}
${ToolTip}:hover & {
visibility: visible;
}
}
`;
export const Love = styled.span`
&& {
color: #e25555;
padding: 0 5px;
}
`;
export const Flag = styled(Icon)`
&& {
padding: 0 5px;
}
`;
export const Logo = Flag;

View File

@ -0,0 +1,269 @@
/**
* @prettier
* @flow
*/
import React, { Component } from 'react';
import type { Node } from 'react';
import { Link } from 'react-router-dom';
import Button from '@material-ui/core/Button/index';
import IconButton from '@material-ui/core/IconButton/index';
import MenuItem from '@material-ui/core/MenuItem/index';
import Menu from '@material-ui/core/Menu/index';
import Info from '@material-ui/icons/Info';
import Help from '@material-ui/icons/Help';
import Tooltip from '@material-ui/core/Tooltip/index';
import AccountCircle from '@material-ui/icons/AccountCircle';
import { default as IconSearch } from '@material-ui/icons/Search';
import { getRegistryURL } from '../../utils/url';
import ExternalLink from '../Link';
import Logo from '../Logo';
import RegistryInfoDialog from '../RegistryInfoDialog';
import Label from '../Label';
import Search from '../Search';
import RegistryInfoContent from '../RegistryInfoContent';
import { IProps, IState } from './types';
import type { ToolTipType } from './types';
import { Greetings, NavBar, InnerNavBar, MobileNavBar, InnerMobileNavBar, LeftSide, RightSide, IconSearchButton, SearchWrapper } from './styles';
class Header extends Component<IProps, IState> {
handleLoggedInMenu: Function;
handleLoggedInMenuClose: Function;
handleOpenRegistryInfoDialog: Function;
handleCloseRegistryInfoDialog: Function;
handleToggleLogin: Function;
renderInfoDialog: Function;
constructor(props: IProps) {
super(props);
this.state = {
openInfoDialog: false,
registryUrl: getRegistryURL(),
showMobileNavBar: false,
};
}
/**
* opens popover menu for logged in user.
*/
handleLoggedInMenu = (event: SyntheticEvent<HTMLElement>) => {
this.setState({
anchorEl: event.currentTarget,
});
};
/**
* closes popover menu for logged in user
*/
handleLoggedInMenuClose = () => {
this.setState({
anchorEl: null,
});
};
/**
* opens registry information dialog.
*/
handleOpenRegistryInfoDialog = () => {
this.setState({
openInfoDialog: true,
});
};
/**
* closes registry information dialog.
*/
handleCloseRegistryInfoDialog = () => {
this.setState({
openInfoDialog: false,
});
};
/**
* close/open popover menu for logged in users.
*/
handleToggleLogin = () => {
const { onToggleLoginModal } = this.props;
this.setState(
{
anchorEl: null,
},
onToggleLoginModal
);
};
handleToggleMNav = () => {
const { showMobileNavBar } = this.state;
this.setState({
showMobileNavBar: !showMobileNavBar,
});
};
handleDismissMNav = () => {
this.setState({
showMobileNavBar: false,
});
};
renderLeftSide = (): Node => {
const { withoutSearch = false } = this.props;
return (
<LeftSide>
<Link style={{ marginRight: '1em' }} to={'/'}>
{this.renderLogo()}
</Link>
{!withoutSearch && (
<SearchWrapper>
<Search />
</SearchWrapper>
)}
</LeftSide>
);
};
renderLogo = (): Node => {
const { logo } = this.props;
if (logo) {
return <img alt={'logo'} height={'40px'} src={logo} />;
} else {
return <Logo />;
}
};
renderToolTipIcon = (title: string, type: ToolTipType) => {
let content;
switch (type) {
case 'help':
content = (
<IconButton blank={true} color={'inherit'} component={ExternalLink} to={'https://verdaccio.org/docs/en/installation'}>
<Help />
</IconButton>
);
break;
case 'info':
content = (
<IconButton color={'inherit'} id={'header--button-registryInfo'} onClick={this.handleOpenRegistryInfoDialog}>
<Info />
</IconButton>
);
break;
case 'search':
content = (
<IconSearchButton color={'inherit'} onClick={this.handleToggleMNav}>
<IconSearch />
</IconSearchButton>
);
break;
}
return (
<Tooltip disableFocusListener={true} title={title}>
{content}
</Tooltip>
);
};
renderRightSide = (): Node => {
const { username = '', withoutSearch = false } = this.props;
return (
<RightSide>
{!withoutSearch && this.renderToolTipIcon('Search packages', 'search')}
{this.renderToolTipIcon('Documentation', 'help')}
{this.renderToolTipIcon('Registry Information', 'info')}
{username ? (
this.renderMenu()
) : (
<Button color={'inherit'} id={'header--button-login'} onClick={this.handleToggleLogin}>
{'Login'}
</Button>
)}
</RightSide>
);
};
renderGreetings = () => {
const { username = '' } = this.props;
return (
<>
<Greetings>{`Hi,`}</Greetings>
<Label capitalize={true} limit={140} text={username} weight={'bold'} />
</>
);
};
/**
* render popover menu
*/
renderMenu = (): Node => {
const { onLogout } = this.props;
const { anchorEl } = this.state;
const open = Boolean(anchorEl);
return (
<React.Fragment>
<IconButton color={'inherit'} id={'header--button-account'} onClick={this.handleLoggedInMenu}>
<AccountCircle />
</IconButton>
<Menu
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'top',
horizontal: 'right',
}}
id={'sidebar-menu'}
onClose={this.handleLoggedInMenuClose}
open={open}
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}>
<MenuItem disabled={true}>{this.renderGreetings()}</MenuItem>
<MenuItem id={'header--button-logout'} onClick={onLogout}>
{'Logout'}
</MenuItem>
</Menu>
</React.Fragment>
);
};
renderInfoDialog = (): Node => {
const { scope } = this.props;
const { openInfoDialog, registryUrl } = this.state;
return (
<RegistryInfoDialog onClose={this.handleCloseRegistryInfoDialog} open={openInfoDialog}>
<RegistryInfoContent registryUrl={registryUrl} scope={scope} />
</RegistryInfoDialog>
);
};
render() {
const { showMobileNavBar } = this.state;
const { withoutSearch = false } = this.props;
return (
<div>
<NavBar position={'static'}>
<InnerNavBar>
{this.renderLeftSide()}
{this.renderRightSide()}
</InnerNavBar>
{this.renderInfoDialog()}
</NavBar>
{showMobileNavBar &&
!withoutSearch && (
<MobileNavBar>
<InnerMobileNavBar>
<Search />
</InnerMobileNavBar>
<Button color={'inherit'} onClick={this.handleDismissMNav}>
{'Cancel'}
</Button>
</MobileNavBar>
)}
</div>
);
}
}
export default Header;

View File

@ -0,0 +1,106 @@
/**
* @prettier
* @flow
*/
import styled, { css } from 'react-emotion';
import AppBar from '@material-ui/core/AppBar/index';
import Toolbar from '@material-ui/core/Toolbar/index';
import IconButton from '@material-ui/core/IconButton/index';
import colors from '../../utils/styles/colors';
import mq from '../../utils/styles/media';
export const InnerNavBar = styled(Toolbar)`
&& {
justify-content: space-between;
align-items: center;
padding: 0 15px;
}
`;
export const Greetings = styled.span`
&& {
margin: 0 5px 0 0;
}
`;
export const RightSide = styled(Toolbar)`
&& {
display: flex;
padding: 0;
}
`;
export const LeftSide = styled(RightSide)`
&& {
flex: 1;
}
`;
export const MobileNavBar = styled.div`
&& {
align-items: center;
display: flex;
border-bottom: 1px solid ${colors.greyLight};
padding: 8px;
position: relative;
}
`;
export const InnerMobileNavBar = styled.div`
&& {
border-radius: 4px;
background-color: ${colors.greyLight};
color: ${colors.white};
width: 100%;
padding: 0px 5px;
margin: 0 10px 0 0;
}
`;
export const IconSearchButton = styled(IconButton)`
&& {
display: block;
}
`;
export const SearchWrapper = styled.div`
&& {
display: none;
max-width: 393px;
width: 100%;
}
`;
export const NavBar = styled(AppBar)`
&& {
background-color: ${colors.primary};
min-height: 60px;
display: flex;
justify-content: center;
${mq.medium(css`
${SearchWrapper} {
display: flex;
}
${IconSearchButton} {
display: none;
}
${MobileNavBar} {
display: none;
}
`)};
${mq.large(css`
${InnerNavBar} {
padding: 0 20px;
}
`)};
${mq.xlarge(css`
${InnerNavBar} {
max-width: 1240px;
width: 100%;
margin: 0 auto;
}
`)};
}
`;

View File

@ -0,0 +1,22 @@
/**
* @prettier
* @flow
*/
export interface IProps {
logo: string;
username?: string;
onLogout?: Function;
onToggleLoginModal: Function;
scope: string;
withoutSearch?: boolean;
}
export interface IState {
anchorEl?: any;
openInfoDialog: boolean;
registryUrl: string;
showMobileNavBar: boolean;
}
export type ToolTipType = 'search' | 'help' | 'info';

View File

@ -0,0 +1,51 @@
/**
* @prettier
* @flow
*/
import React, { Fragment } from 'react';
import type { Node } from 'react';
import CardActions from '@material-ui/core/CardActions/index';
import CardContent from '@material-ui/core/CardContent/index';
import Button from '@material-ui/core/Button/index';
import Typography from '@material-ui/core/Typography/index';
import CopyToClipBoard from '../CopyToClipBoard/index';
import { getRegistryURL } from '../../utils/url';
import { CardStyled as Card, HelpTitle } from './styles';
function renderHeadingClipboardSegments(title: string, text: string): Node {
return (
<Fragment>
<Typography variant={'body2'}>{title}</Typography>
<CopyToClipBoard text={text} />
</Fragment>
);
}
const Help = (): Node => {
const registryUrl = getRegistryURL();
return (
<Card id={'help-card'}>
<CardContent>
<Typography component={'h2'} gutterBottom={true} id={'help-card__title'} variant={'headline'}>
{'No Package Published Yet.'}
</Typography>
<HelpTitle color={'textSecondary'} gutterBottom={true}>
{'To publish your first package just:'}
</HelpTitle>
{renderHeadingClipboardSegments('1. Login', `$ npm adduser --registry ${registryUrl}`)}
{renderHeadingClipboardSegments('2. Publish', `$ npm publish --registry ${registryUrl}`)}
<Typography variant={'body2'}>{'3. Refresh this page.'}</Typography>
</CardContent>
<CardActions>
<Button color={'primary'} href={'https://verdaccio.org/docs/en/installation'} size={'small'} target={'_blank'}>
{'Learn More'}
</Button>
</CardActions>
</Card>
);
};
export default Help;

View File

@ -0,0 +1,21 @@
/**
* @prettier
* @flow
*/
import styled from 'react-emotion';
import Card from '@material-ui/core/Card/index';
import Typography from '@material-ui/core/Typography/index';
export const CardStyled = styled(Card)`
&& {
width: 600px;
margin: auto;
}
`;
export const HelpTitle = styled(Typography)`
&& {
margin-bottom: 20px;
}
`;

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" id="austria"><path d="M473.655 88.276H38.345C17.167 88.276 0 105.443 0 126.621v73.471h512v-73.471c0-21.178-17.167-38.345-38.345-38.345zM0 385.379c0 21.177 17.167 38.345 38.345 38.345h435.31c21.177 0 38.345-17.167 38.345-38.345v-73.471H0v73.471z" fill="#ff4b55"/><path fill="#f5f5f5" d="M0 200.09h512V311.9H0z"/></svg>

After

Width:  |  Height:  |  Size: 380 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 45" id="brazil"><defs><clipPath id="a"><path d="M0 36h36V0H0v36z"/></clipPath></defs><g clip-path="url(#a)" transform="matrix(1.25 0 0 -1.25 0 45)"><path d="M36 9a4 4 0 0 0-4-4H4a4 4 0 0 0-4 4v18a4 4 0 0 0 4 4h28a4 4 0 0 0 4-4V9z" fill="#009b3a"/><path d="M32.727 18L18 6.876 3.27 18 18 29.125 32.727 18z" fill="#fedf01"/><path d="M24.434 18.076a6.458 6.458 0 1 1-12.917 0 6.458 6.458 0 0 1 12.917 0" fill="#002776"/><path d="M12.277 21.113a6.406 6.406 0 0 1-.672-2.023c3.994.29 9.417-1.892 11.744-4.596.402.604.7 1.28.882 2.004-2.871 2.809-7.916 4.63-11.954 4.615" fill="#cbe9d4"/><path d="M13 16.767h-1v1h1v-1zM14 14.767h-1v1h1v-1z" fill="#88c9f9"/><path d="M16 16.767h-1v1h1v-1zM18 15.767h-1v1h1v-1zM22 13.767h-1v1h1v-1zM19 12.767h-1v1h1v-1zM22 18.767h-1v1h1v-1z" fill="#55acee"/><path d="M20 14.767h-1v1h1v-1z" fill="#3b88c3"/></g></svg>

After

Width:  |  Height:  |  Size: 898 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 45" id="china"><defs><clipPath id="a"><path d="M0 36h36V0H0v36z"/></clipPath></defs><g clip-path="url(#a)" transform="matrix(1.25 0 0 -1.25 0 45)"><path d="M36 9a4 4 0 0 0-4-4H4a4 4 0 0 0-4 4v18a4 4 0 0 0 4 4h28a4 4 0 0 0 4-4V9z" fill="#de2910"/><path d="M7 25.049l.929-2.67 2.826-.06-2.253-1.706.819-2.707L7 19.52l-2.321-1.615.819 2.707-2.253 1.707 2.826.059.929 2.67zM13 28.472l.34-.688.759-.11-.549-.536.129-.756-.679.357-.679-.357.13.756-.55.536.76.11.339.688zM15 24.472l.34-.688.759-.11-.549-.536.129-.756-.679.357-.679-.357.13.756-.55.536.76.11.339.688zM15 20.472l.34-.688.759-.11-.549-.536.129-.756-.679.357-.679-.357.13.756-.55.536.76.11.339.688zM13 16.473l.34-.69.759-.11-.549-.534.129-.757-.679.357-.679-.357.13.757-.55.535.76.11.339.689z" fill="#ffde02"/></g></svg>

After

Width:  |  Height:  |  Size: 833 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 45" id="earth"><Title>Earth</Title><defs><clipPath id="a"><path d="M0 36h36V0H0v36z"/></clipPath><clipPath id="b"><path d="M18 36C8.059 36 0 27.941 0 18S8.059 0 18 0s18 8.059 18 18-8.059 18-18 18z"/></clipPath></defs><g clip-path="url(#a)" transform="matrix(1.25 0 0 -1.25 0 45)"><path d="M36 18c0-9.941-8.059-18-18-18S0 8.059 0 18s8.059 18 18 18 18-8.059 18-18" fill="#88c9f9"/></g><g clip-path="url(#b)" transform="matrix(1.25 0 0 -1.25 0 45)"><path d="M3.627 28.952c-.45 2.93 2.195 4.156 3.607 4.47 1.412.314 2.776.62 2.933-.006.156-.628.311-1.46 1.173-1.148.862.314 3.043.56 4.063 1.342 1.02.783 2.244.787 3.264.473 1.02-.313 3.877-.227 3.25-1.167-.627-.94-1.825-.827-2.45-1.924-.628-1.099.171-1.826 1.033-1.826.865 0 1.71-.135 2.26.727.548.863-.383 2.463.324 2.357.706-.106 1.477-.866 2.03-2.043.547-1.176 1.408-.47 1.723-1.176.313-.705 2.04-2.039 1.177-1.804-.864.236-1.726.392-1.96-.47-.237-.863.388-1.726-.237-1.647-.627.08-.86-.089-1.725-.004-.862.083-1.333.631-2.039-.545-.705-1.175-1.254-1.96-1.567-2.509-.315-.549-.785-.86-.55-1.96.235-1.099-.628-.785-.628.156 0 .94-.548 1.098-1.253.942-.706-.157-1.803-.313-1.724-1.098.077-.784-.315-1.725.313-2.352.627-.629 1.33.076 1.723-.158.393-.237 1.525-.023 1.133-.416-.393-.39-1.76-.88-.976-1.509a4.831 4.831 0 0 1 1.893-.907c.313-.08.062.774 1.083 1.166 1.017.392 2.608 1.29 3 .584.391-.705.338-.595 1.75-.75 1.41-.156 1.79-.585 2.417-1.917.626-1.333.446-1.192 1.462-1.58 1.021-.394 1.678-.223.737-1.087-.94-.86-1.65-.814-2.199-1.833-.55-1.017-.153-1.73-1.25-2.75A20.755 20.755 0 0 0 24 4c-.618-.37-2.162-2.07-3.083-2.667-.834-.54-1.083 0-1.083 0s.256 1.667.964 2.372c.704.705 1.105 3.344.87 4.128-.235.783-1.36 1.02-1.75 1.333-.393.312-1.418 1.548-1.418 2.334 0 .784 1.71 2.81 1.71 2.81.218-1.089-1.039.328-1.627.523-.47.157-1.542 1.656-2.459 1.814-.916.16-1.363.7-2.068 1.25-.706.55-2.43 1.332-2.353 2.195.08.862-1.725 1.568-2.038 1.568-.314 0-1.019 0-1.647 1.098-.627 1.098-1.725 2.196-1.41 2.98.312.783.391 1.726.233 2.588-.156.862-1.332 1.176-1.567.941-.235-.236-1.489-1.335-1.647-.315" fill="#5c913b"/></g></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" id="india"><Title>India</Title><path d="M0 384c0 31.418 25.473 56.889 56.889 56.889H455.11C486.53 440.889 512 415.416 512 384v-56.889H0V384z" fill="#138808"/><path d="M0 327.111h512V184.89H0V327.11z" fill="#eee"/><path d="M512 184.889V128c0-31.417-25.473-56.889-56.889-56.889H56.89C25.472 71.111 0 96.583 0 128v56.889h512z" fill="#f93"/><path d="M312.889 256c0-31.431-25.473-56.902-56.903-56.902-31.417 0-56.888 25.472-56.888 56.902 0 31.418 25.472 56.889 56.888 56.889 31.432 0 56.903-25.472 56.903-56.889" fill="navy"/><path d="M298.666 256c0-23.566-19.115-42.681-42.681-42.681S213.319 232.434 213.319 256s19.1 42.666 42.666 42.666 42.681-19.1 42.681-42.666" fill="#eee"/><path d="M256 213.334l2.076 32.199 14.251-28.943-10.396 30.535 24.235-21.305-21.291 24.249 30.535-10.396-28.942 14.25L298.666 256l-32.198 2.076 28.942 14.237-30.535-10.383 21.291 24.235-24.235-21.29 10.396 30.535-14.25-28.942L256 298.666l-2.076-32.198-14.252 28.942 10.397-30.535-24.249 21.291 21.305-24.235-30.535 10.383 28.942-14.236L213.334 256l32.199-2.076-28.943-14.251 30.535 10.396-21.305-24.249 24.249 21.305-10.396-30.535 14.25 28.943 2.077-32.2z" fill="navy" opacity=".6"/><path d="M241.778 256c0-7.851 6.37-14.223 14.222-14.223s14.223 6.372 14.223 14.223-6.372 14.223-14.223 14.223-14.223-6.372-14.223-14.223" fill="navy"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 490.652 490.652" id="license"><path d="M456.607 9.904H34.04C15.269 9.904 0 25.17 0 43.945V310.17c0 18.77 15.269 34.039 34.04 34.039h260.642c-4.043-10.35-6.534-21.737-7.159-33.742l-253.974-.297.491-266.718 423.063.493V310.17c0 .279-.23.492-.495.492l-15.251-.019c-.637 11.942-3.111 23.263-7.138 33.565h22.389c18.777 0 34.045-15.27 34.045-34.039V43.945c-.001-18.775-15.268-34.041-34.046-34.041z"/><path d="M364.447 381.02c33.354 0 60.386-34.152 60.386-76.289 0-42.131-27.031-76.284-60.386-76.284-33.35 0-60.38 34.153-60.38 76.284 0 42.137 27.031 76.289 60.38 76.289zM81.087 118.345h149.561c9.273 0 16.776-7.499 16.776-16.772 0-9.271-7.504-16.775-16.776-16.775H81.087c-9.271 0-16.771 7.504-16.771 16.775 0 9.273 7.499 16.772 16.771 16.772zM415.167 177.061c0-9.273-7.505-16.779-16.776-16.779H81.087c-9.271 0-16.771 7.506-16.771 16.779 0 9.271 7.499 16.77 16.771 16.77h317.304c9.271 0 16.776-7.498 16.776-16.77zM81.087 235.768c-9.271 0-16.771 7.504-16.771 16.771 0 9.273 7.499 16.777 16.771 16.777h149.561c9.273 0 16.776-7.504 16.776-16.777 0-9.268-7.504-16.771-16.776-16.771H81.087zM415.425 374.452c-.065.082-23.295 26.869-55.728 23.081l40.331 81.577a2.93 2.93 0 0 0 2.637 1.639h.215a2.95 2.95 0 0 0 2.571-2.031l10.174-31.234a3.024 3.024 0 0 1 1.488-1.728 3.07 3.07 0 0 1 2.28-.13l30.977 10.88c.324.112.651.163.98.163.836 0 1.623-.348 2.195-.982.803-.9.964-2.18.441-3.244l-38.561-77.991zM313.653 374.598c-.049-.047-.118-.096-.185-.146l-38.561 77.973a2.953 2.953 0 0 0 .448 3.262 2.961 2.961 0 0 0 3.174.819l30.978-10.88c.328-.113.658-.16.982-.16.442 0 .884.096 1.294.29.706.36 1.245.987 1.493 1.741l10.173 31.221c.378 1.133 1.391 1.936 2.572 2.014h.21a2.95 2.95 0 0 0 2.642-1.621l26.206-53.01-16.755-33.893c-.066-.034-14.672-5.884-24.671-17.61z"/></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" id="nicaragua"><Title>Nicaragua</Title><path d="M512 384c0 31.418-25.473 56.889-56.889 56.889H56.89C25.472 440.889 0 415.417 0 384V128c0-31.418 25.472-56.889 56.889-56.889H455.11C486.53 71.111 512 96.584 512 128v256z" fill="#265fb5"/><path d="M512 327.111H0V184.89h512V327.11z" fill="#eee"/><path d="M320.811 256c0 35.797-29.014 64.811-64.811 64.811-35.783 0-64.811-29.014-64.811-64.811s29.027-64.811 64.811-64.811c35.797 0 64.811 29.013 64.811 64.811" fill="#a9bf4c"/><path d="M312.889 256c0 31.418-25.473 56.889-56.889 56.889S199.111 287.416 199.111 256s25.473-56.889 56.889-56.889 56.889 25.471 56.889 56.889" fill="#eee"/><path d="M209.891 286.649l45.909-79.517 45.909 79.517H209.89z" fill="#265fb5"/><path d="M215.04 283.591l40.818-70.685 40.803 70.685H215.04z" fill="#55acee"/><path d="M215.04 283.591l9.841-17.052 61.483-.783 10.297 17.835H215.04z" fill="#bbddf5"/><path d="M222.891 272.441l15.331-12.445 6.67 7.553 5.774-6.215 4.893 4.892 4.665-5.12 5.561 5.12 5.106-5.334 4.665 5.334 4.451-4.892 7.325 9.102 1.338 2.674s-7.78 1.55-18.446.669c-10.667-.896-16.882 1.55-25.33 2.66-8.448 1.109-23.553-1.109-23.553-1.109l1.55-2.889z" fill="#5c913b"/><g fill="#e2f09f"><path d="M237.995 262.67l10.226 11.107-5.12.442-5.774-8.434.668-3.115zM250.226 263.553l8.22 7.338-3.782.654-3.996-5.546-.442-2.446zM260.665 263.111l7.111 6.67L266 270.89l-5.774-6.229.44-1.55zM269.995 263.111l-2.888 2.66 1.338 1.565 1.55-4.225zM278.884 263.339l-3.328 4.224 1.109 1.99 2.446-4.664-.227-1.55z"/></g><path d="M256.426 233.671l.939 13.227 7.566-10.867-5.675 11.805 11.805-5.66-10.851 7.553 13.213.939-13.213.952 10.851 7.553-11.805-5.66 5.675 11.805-7.566-10.867-.939 13.226-.939-13.226-7.566 10.867 5.675-11.805-11.805 5.66 10.851-7.553-13.212-.953 13.212-.938-10.85-7.553 11.804 5.66-5.675-11.805 7.566 10.866.94-13.226z" fill="#bbddf5"/><path d="M256 244.665l-2.66 2.66s.654 4.011.441 4.679C253.554 252.658 256 256 256 256l3.327-3.996-.88-5.334-2.447-2.005z" fill="#dd2e44"/><path d="M257.28 240.071c10.894 0 17.109 5.334 17.109 5.334l-3.996-7.111s-6.443-4.893-13.995-4.893c-7.567 0-16 6.001-16 6.001l-3.783 7.553s9.771-6.884 20.665-6.884" fill="#269"/><path d="M257.28 240.071c10.894 0 17.109 5.334 17.109 5.334l-2.888-5.106s-7.78-4.665-15.331-4.665-16.896 5.987-16.896 5.987l-2.66 5.334c.001 0 9.772-6.884 20.666-6.884" fill="#ffcc4d"/><path d="M257.28 240.071c10.894 0 17.109 5.334 17.109 5.334l-2.005-3.327s-5.988-4.224-16.214-3.783c-7.553 0-18.005 5.561-18.005 5.561l-1.55 3.1c0-.001 9.771-6.885 20.665-6.885" fill="#dd2e44"/><g fill="#eee"><path d="M264.291 322.873h-14.165V309.29h14.165v13.582zM247.865 203.975h-10.183l-5.106-12.459 10.198-1.137 5.091 13.596zM271.075 209.635l-6.784-2.261 3.385-16.426 13.028 1.137-9.629 17.55zM209.934 279.281l-19.811 4.523-.57-13.583 19.812-2.83.569 11.89zM215.04 293.433l-16.426 6.812-3.954-10.766 15.289-5.106 5.091 9.06zM324.309 280.989l-18.105-5.091v-9.63l20.366-2.83-2.261 17.55zM315.306 299.663l-16.981-7.937 5.646-10.182 18.703 5.66-7.368 12.459z"/></g></svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 45" id="pakistan"><defs><clipPath id="a"><path d="M0 36h36V0H0v36z"/></clipPath></defs><g clip-path="url(#a)" transform="matrix(1.25 0 0 -1.25 0 45)"><path d="M27.61 20.952l-1.213-2.022-.208 2.349-2.298.528 2.17.924-.207 2.349 1.548-1.78 2.17.925-1.212-2.023 1.548-1.78-2.298.53zM22.5 10.528a7.5 7.5 0 0 0-7.5 7.5c0 3.72 2.711 6.798 6.263 7.389a6.494 6.494 0 0 1-3.763-5.89 6.5 6.5 0 0 1 6.5-6.5 6.494 6.494 0 0 1 5.89 3.764c-.592-3.552-3.67-6.263-7.39-6.263M32 31H9V5h23a4 4 0 0 1 4 4v18a4 4 0 0 1-4 4" fill="#004600"/><path d="M4 31a4 4 0 0 1-4-4V9a4 4 0 0 1 4-4h5v26H4z" fill="#eee"/><path d="M29.572 24.225l-2.17-.924-1.548 1.779.207-2.35-2.17-.922 2.298-.528.208-2.35 1.213 2.022 2.298-.53-1.548 1.78 1.212 2.023z" fill="#fff"/><path d="M24 13.028a6.496 6.496 0 1 0-2.737 12.39c-3.552-.591-6.263-3.67-6.263-7.39a7.5 7.5 0 0 1 7.5-7.5c3.72 0 6.799 2.71 7.39 6.263A6.496 6.496 0 0 0 24 13.028" fill="#fff"/></g></svg>

After

Width:  |  Height:  |  Size: 977 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 45" id="spain"><defs><clipPath id="a"><path d="M0 36h36V0H0v36z"/></clipPath></defs><g clip-path="url(#a)" transform="matrix(1.25 0 0 -1.25 0 45)"><path d="M36 9a4 4 0 0 0-4-4H4a4 4 0 0 0-4 4v18a4 4 0 0 0 4 4h28a4 4 0 0 0 4-4V9z" fill="#c60a1d"/><path d="M36 12H0v12h36V12z" fill="#ffc400"/><path d="M9 19v-3a3 3 0 1 1 6 0v3H9z" fill="#ea596e"/><path d="M12 17h3v3h-3v-3z" fill="#f4a2b2"/><path d="M12 17H9v3h3v-3z" fill="#dd2e44"/><path d="M15 21.5c0-.829-1.343-1.5-3-1.5s-3 .671-3 1.5 1.343 1.5 3 1.5 3-.671 3-1.5" fill="#ea596e"/><path d="M15 22.25c0 .414-1.343.75-3 .75s-3-.336-3-.75 1.343-.75 3-.75 3 .336 3 .75" fill="#ffac33"/><path d="M7 13h1v7H7v-7zM17 13h-1v7h1v-7z" fill="#99aab5"/><path d="M9 13H6v1h3v-1zM18 13h-3v1h3v-1zM8 20H7v1h1v-1zM17 20h-1v1h1v-1z" fill="#66757f"/></g></svg>

After

Width:  |  Height:  |  Size: 851 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="time"><path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/><path d="M0 0h24v24H0z" fill="none"/><path d="M12.5 7H11v6l5.25 3.15.75-1.23-4.5-2.67z"/></svg>

After

Width:  |  Height:  |  Size: 321 B

View File

@ -0,0 +1 @@
<svg width='100px' height='100px' xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="verdaccio"><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 stroke="null" fill-rule="evenodd" fill="none"><use transform="translate(-37.027 -40.362) scale(1.71429)" x="22.366" y="28.311" xlink:href="#a" filter="url(#b)" fill="#000"/><use transform="translate(-37.027 -40.362) scale(1.71429)" 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="translate(-37.027 -40.362) scale(1.71429)" x="22.366" y="28.311" xlink:href="#c" filter="url(#d)" fill="#000"/><use transform="translate(-37.027 -40.362) scale(1.71429)" 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"/><use transform="translate(-37.027 -40.362) scale(1.71429)" 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"/><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></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,54 @@
/**
* @prettier
* @flow
*/
import React from 'react';
import type { Node } from 'react';
import capitalize from 'lodash/capitalize';
import { Svg, Img, ImgWrapper } from './styles';
import { IProps, IIconsMap } from './types';
import brazil from './img/brazil.svg';
import china from './img/china.svg';
import india from './img/india.svg';
import nicaragua from './img/nicaragua.svg';
import pakistan from './img/pakistan.svg';
import austria from './img/austria.svg';
import spain from './img/spain.svg';
import earth from './img/earth.svg';
import verdaccio from './img/verdaccio.svg';
import license from './img/license.svg';
import time from './img/time.svg';
export const Icons: $Shape<IIconsMap> = {
// flags
brazil,
spain,
china,
nicaragua,
pakistan,
india,
austria,
earth,
verdaccio,
license,
time,
};
const Icon = ({ className, name, size = 'sm', img = false, pointer = false, ...props }: IProps): Node => {
const title = capitalize(name);
return img ? (
<ImgWrapper className={className} pointer={pointer} size={size} title={title} {...props}>
<Img alt={title} src={Icons[name]} />
</ImgWrapper>
) : (
<Svg className={className} pointer={pointer} size={size} {...props}>
<title>{title}</title>
<use xlinkHref={`${Icons[name]}#${name}`} />
</Svg>
);
};
export default Icon;

View File

@ -0,0 +1,55 @@
/**
* @prettier
* @flow
*/
import styled, { css } from 'react-emotion';
import { IProps } from './types';
const getSize = (size: string) => {
switch (size) {
case 'md':
return `
width: 18px;
height: 18px;
`;
case 'lg':
return `
width: 20px;
height: 20px;
`;
default:
return `
width: 14px;
height: 14px;
`;
}
};
const commonStyle = ({ size = 'sm', pointer, modifiers }: IProps) => css`
&& {
display: inline-block;
cursor: ${pointer ? 'pointer' : 'default'};
${getSize(size)};
${modifiers && modifiers};
}
`;
export const Svg = styled.svg`
&& {
${commonStyle};
}
`;
export const ImgWrapper = styled.span`
&& {
${commonStyle};
}
`;
export const Img = styled.img`
&& {
width: 100%;
height: auto;
}
`;

View File

@ -0,0 +1,32 @@
/**
* @prettier
* @flow
*/
import { Icons } from './index';
import type { Styles } from '../../../../types';
export interface IIconsMap {
brazil: string;
spain: string;
china: string;
nicaragua: string;
pakistan: string;
austria: string;
india: string;
earth: string;
verdaccio: string;
license: string;
time: string;
[key: string]: string;
}
export interface IProps {
name: $Keys<typeof Icons>;
className?: string;
// $FlowFixMe
onClick?: (event: SyntheticMouseEvent<SVGElement | HTMLSpanElement>) => void;
size?: 'sm' | 'md';
pointer?: boolean;
img?: boolean;
modifiers?: Styles;
}

View File

@ -0,0 +1,46 @@
import React, {Component} from 'react';
import { DetailContextConsumer } from '../../pages/version/index';
import Card from '@material-ui/core/Card/index';
import CardContent from '@material-ui/core/CardContent/index';
import CopyToClipBoard from '../CopyToClipBoard';
import Button from '@material-ui/core/Button';
import CardActions from '@material-ui/core/CardActions';
class Install extends Component<any, any> {
render() {
return (
<DetailContextConsumer>
{(context) => {
return this.renderCopyCLI(context);
}}
</DetailContextConsumer>
);
};
renderCopyCLI = ({packageName}) => {
return (
<Card>
<CardContent>
<CopyToClipBoard text={`npm install ${packageName}`} />
<CopyToClipBoard text={`pnpm install ${packageName}`} />
<CopyToClipBoard text={`yarn add ${packageName}`} />
<CardActions>
{this.renderDownloadButton()}
</CardActions>
</CardContent>
</Card>
);
}
renderDownloadButton = () => {
return (
<Button color={"primary"} size={'small'} variant={"contained"}>
{'Download Tarball'}
</Button>
);
}
}
export default Install;

View File

@ -0,0 +1,27 @@
/**
* @prettier
* @flow
*/
import React from 'react';
import styled from 'react-emotion';
import { fontWeight } from '../../utils/styles/sizes';
import type { Node } from 'react';
import { IProps } from './types';
const Wrapper = styled.div`
font-weight: ${({ weight }) => fontWeight[weight]};
text-transform: ${({ capitalize }) => (capitalize ? 'capitalize' : 'none')};
${({ modifiers }: IProps) => modifiers && modifiers};
`;
const Label = ({ text = '', capitalize = false, weight = 'regular', ...props }: IProps): Node => {
return (
<Wrapper capitalize={capitalize} weight={weight} {...props}>
{text}
</Wrapper>
);
};
export default Label;

View File

@ -0,0 +1,12 @@
/**
* @prettier
* @flow
*/
import type { Styles } from '../../../../types';
export interface IProps {
text: string;
capitalize?: boolean;
weight?: string;
modifiers?: Styles;
}

View File

@ -0,0 +1,31 @@
/**
* @prettier
* @flow
*/
import styled, { css } from 'react-emotion';
export const Content = styled.div`
&& {
background-color: #ffffff;
flex: 1;
display: flex;
position: relative;
flex-direction: column;
}
`;
export const Container = styled.div`
&& {
display: flex;
flex-direction: column;
min-height: 100vh;
overflow: hidden;
${({ isLoading }) =>
isLoading &&
css`
${Content} {
background-color: #f5f6f8;
}
`}
`;

View File

@ -0,0 +1,51 @@
/* eslint no-unused-vars: 0 */
import React, {Component, Fragment} from 'react';
import { DetailContextConsumer } from '../../pages/version/index';
import Card from '@material-ui/core/Card/index';
import CardContent from '@material-ui/core/CardContent/index';
import Avatar from '@material-ui/core/Avatar';
import Notes from '@material-ui/icons/Notes';
import Typography from "@material-ui/core/Typography/index";
class License extends Component<any, any> {
render() {
return (
<DetailContextConsumer>
{(context) => {
return this.renderAuthor(context);
}}
</DetailContextConsumer>
);
};
renderAuthor = ({packageMeta}) => {
const { license } = packageMeta.latest;
if (!license) {
return null;
}
return (
<Card>
<CardContent style={{ textAling: 'center'}}>
{this.renderLicense(license)}
</CardContent>
</Card>
);
}
renderLicense = (license) => {
return (
<Fragment>
<Notes style={{ fontSize: 38 }} />
<Typography color={"textPrimary"} gutterBottom={true} variant={'caption'}>
{license}
</Typography>
</Fragment>
);
}
}
export default License;

View File

@ -0,0 +1,16 @@
/**
* @prettier
* @flow
*/
import React from 'react';
import type { Node } from 'react';
import { IProps } from './types';
const Link = ({ children, to = '#', blank = false, ...props }: IProps): Node => (
<a href={to} target={blank ? '_blank' : '_self'} {...props}>
{children}
</a>
);
export default Link;

View File

@ -0,0 +1,12 @@
/**
* @prettier
* @flow
*/
import type { Node } from 'react';
export interface IProps {
children?: Node;
to?: string;
blank?: boolean;
}

View File

@ -0,0 +1,23 @@
/**
* @prettier
* @flow
*/
import React from 'react';
import type { Node } from 'react';
import Logo from '../Logo';
import Spinner from '../Spinner';
import { Wrapper, Badge } from './styles';
const Loading = (): Node => (
<Wrapper>
<Badge>
<Logo md={true} />
</Badge>
<Spinner />
</Wrapper>
);
export default Loading;

View File

@ -0,0 +1,24 @@
/**
* @prettier
* @flow
*/
import styled from 'react-emotion';
export const Wrapper = styled.div`
&& {
transform: translate(-50%, -50%);
top: 50%;
left: 50%;
position: absolute;
}
`;
export const Badge = styled.div`
&& {
margin: 0 0 30px 0;
border-radius: 25px;
box-shadow: 0 10px 20px 0 rgba(69, 58, 100, 0.2);
background: #f7f8f6;
}
`;

View File

@ -0,0 +1,240 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Button from '@material-ui/core/Button';
import DialogTitle from '@material-ui/core/DialogTitle';
import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import SnackbarContent from '@material-ui/core/SnackbarContent';
import ErrorIcon from '@material-ui/icons/Error';
import InputLabel from '@material-ui/core/InputLabel';
import Input from '@material-ui/core/Input';
import FormControl from '@material-ui/core/FormControl';
import FormHelperText from '@material-ui/core/FormHelperText';
import classes from "./login.scss";
export default class LoginModal extends Component {
static propTypes = {
visibility: PropTypes.bool,
error: PropTypes.object,
onCancel: PropTypes.func,
onSubmit: PropTypes.func,
};
static defaultProps = {
error: {},
onCancel: () => {},
onSubmit: () => {},
visibility: true,
}
constructor(props) {
super(props);
this.state = {
form: {
username: {
required: true,
pristine: true,
helperText: 'Field required',
value: '',
},
password: {
required: true,
pristine: true,
helperText: 'Field required',
value: '',
},
},
error: props.error,
};
}
/**
* set login modal's username and password to current state
* Required to login
*/
setCredentials = (name, e) => {
const { form } = this.state;
this.setState({
form: {
...form,
[name]: {
...form[name],
value: e.target.value,
pristine: false,
},
},
});
}
setUsername = (event) => {
this.setCredentials('username', event);
}
setPassword = (event) => {
this.setCredentials('password', event);
}
validateCredentials = (event) => {
const { form } = this.state;
// prevents default submit behavior
event.preventDefault();
this.setState({
form: Object.keys(form).reduce((acc, key) => ({
...acc,
...{ [key]: {...form[key], pristine: false } },
}), {}),
}, () => {
if (!Object.keys(form).some(id => !form[id])) {
this.submitCredentials();
}
});
}
submitCredentials = async () => {
const { form } = this.state;
const { onSubmit } = this.props;
await onSubmit(form.username.value, form.password.value);
// let's wait for API response and then set
// username and password filed to empty state
this.setState({
form: Object.keys(form).reduce((acc, key) => ({
...acc,
...{ [key]: {...form[key], value: "", pristine: true } },
}), {}),
});
}
renderErrorMessage(title, description) {
return (
<span>
<div>
<strong>
{title}
</strong>
</div>
<div>
{description}
</div>
</span>);
}
renderMessage(title, description) {
return (
<div
className={classes.loginErrorMsg}
id={"client-snackbar"}>
<ErrorIcon className={classes.loginIcon} />
{this.renderErrorMessage(title, description)}
</div>);
}
renderLoginError({ type, title, description } = {}) {
return type === 'error' && (
<SnackbarContent
className={classes.loginError}
message={this.renderMessage(title, description)}
/>
);
}
renderNameField = () => {
const { form: { username } } = this.state;
return (
<FormControl
error={!username.value && !username.pristine}
fullWidth={true}
required={username.required}
>
<InputLabel htmlFor={"username"}>{'Username'}</InputLabel>
<Input
id={"login--form-username"}
onChange={this.setUsername}
placeholder={"Your username"}
value={username.value}
/>
{!username.value && !username.pristine && (
<FormHelperText id={"username-error"}>
{username.helperText}
</FormHelperText>
)}
</FormControl>
);
}
renderPasswordField = () => {
const { form: { password } } = this.state;
return (
<FormControl
error={!password.value && !password.pristine}
fullWidth={true}
required={password.required}
style={{ marginTop: '8px' }}
>
<InputLabel htmlFor={"password"}>{'Password'}</InputLabel>
<Input
id={"login--form-password"}
onChange={this.setPassword}
placeholder={"Your strong password"}
type={"password"}
value={password.value}
/>
{!password.value && !password.pristine && (
<FormHelperText id={"password-error"}>
{password.helperText}
</FormHelperText>
)}
</FormControl>
);
}
renderActions = () => {
const { form: { username, password } } = this.state;
const { onCancel } = this.props;
return (
<DialogActions className={"dialog-footer"}>
<Button
color={"inherit"}
id={"login--form-cancel"}
onClick={onCancel}
type={"button"}
>
{'Cancel'}
</Button>
<Button
color={"inherit"}
disabled={!password.value || !username.value}
id={"login--form-submit"}
type={"submit"}
>
{'Login'}
</Button>
</DialogActions>
);
}
render() {
const { visibility, onCancel, error } = this.props;
return (
<Dialog
fullWidth={true}
id={"login--form-container"}
maxWidth={"xs"}
onClose={onCancel}
open={visibility}
>
<form noValidate={true} onSubmit={this.validateCredentials}>
<DialogTitle>{'Login'}</DialogTitle>
<DialogContent>
{this.renderLoginError(error)}
{this.renderNameField()}
{this.renderPasswordField()}
</DialogContent>
{this.renderActions()}
</form>
</Dialog>
);
}
}

View File

@ -0,0 +1,22 @@
@import '../../styles/variables';
.loginDialog {
min-width: 300px;
}
.loginError {
background-color: $red !important;
min-width: inherit !important;
margin-bottom: 10px !important;
}
.loginErrorMsg {
display: flex;
align-items: center;
}
.loginIcon {
opacity: 0.9;
margin-right: 8px;
}

View File

@ -0,0 +1 @@
<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="verdaccio"><Title>Verdaccio</Title><defs><path id="b" d="M48 17.6L32.8 48H24L.4.8h15.2l12.8 25.6 4.4-8.8H48z"/><filter x="-20%" y="-11.7%" width="139.9%" height="140.3%" filterUnits="objectBoundingBox" id="a"><feOffset dy="4" in="SourceAlpha" result="shadowOffsetOuter1"/><feGaussianBlur stdDeviation="2.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"/><feComposite in="shadowBlurOuter1" in2="SourceAlpha" operator="out" result="shadowBlurOuter1"/><feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.0906646286 0" in="shadowBlurOuter1"/></filter><path id="d" d="M50.8 12H35.6L41.2.8h15.2L50.8 12z"/><filter x="-45.7%" y="-49.1%" width="191.3%" height="269.6%" filterUnits="objectBoundingBox" id="c"><feOffset dy="4" in="SourceAlpha" result="shadowOffsetOuter1"/><feGaussianBlur stdDeviation="2.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"/><feComposite in="shadowBlurOuter1" in2="SourceAlpha" operator="out" result="shadowBlurOuter1"/><feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.0906646286 0" in="shadowBlurOuter1"/></filter></defs><g fill="none" fill-rule="evenodd"><rect fill="#F7F8F6" width="100" height="100" rx="37"/><g transform="translate(22 29)"><use fill="#000" filter="url(#a)" xlink:href="#b"/><path stroke="#405236" stroke-width="2.4" d="M46.058 18.8H33.542L28.4 29.083 14.858 2H2.342l22.4 44.8h7.316l14-28z" stroke-linejoin="square" fill="#405236"/></g><g transform="translate(22 29)"><use fill="#000" filter="url(#c)" xlink:href="#d"/><path stroke="#CD4000" stroke-width="2.4" d="M50.058 10.8l4.4-8.8H41.942l-4.4 8.8h12.516z" stroke-linejoin="square" fill="#CD4000"/></g><path d="M54.06 75.8l2.575-5.112L36.857 31H24.342l22.4 44.8h7.319z" stroke="#405236" stroke-width="2.4" fill="#4A5E3F"/><path d="M59.6 31h15.221M55.6 35h15.221M51.6 39.8h15.221" stroke="#CD4000" stroke-width="2.4" stroke-linecap="square"/></g></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,29 @@
/**
* @prettier
* @flow
*/
import styled, { css } from 'react-emotion';
import logo from './img/logo.svg';
const Logo = styled.div`
&& {
display: inline-block;
vertical-align: middle;
box-sizing: border-box;
background-position: center;
background-size: contain;
background-image: url(${logo});
background-repeat: no-repeat;
width: 40px;
height: 40px;
${props =>
props.md &&
css`
width: 90px;
height: 90px;
`};
}
`;
export default Logo;

View File

@ -0,0 +1,16 @@
/**
* @prettier
* @flow
*/
import React from 'react';
import { IProps } from './types';
import { Wrapper } from './styles';
const NoItems = ({ text }: IProps) => (
<Wrapper>
<h2>{text}</h2>
</Wrapper>
);
export default NoItems;

View File

@ -0,0 +1,12 @@
/**
* @prettier
* @flow
*/
import styled from 'react-emotion';
export const Wrapper = styled.div`
&& {
margin: 5em 0;
}
`;

View File

@ -0,0 +1,8 @@
/**
* @prettier
* @flow
*/
export interface IProps {
text: string;
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 495.2 495.2"><path fill="#d38c0d" d="M325.6 224.4L495.2 126 325.6 28.4l-168.8 99.2z"/><g fill="#efbb67"><path d="M170.4 224.4l168-97.6-168-98.4L0 126.8z"/><path d="M416 368.4l-168 98.4-168-98.4v-196L248 74l168 98.4z"/></g><path fill="#d38c0d" d="M248 74l168 98.4v196l-168 98.4"/><path fill="#efbb67" d="M326.4 314.8L495.2 218l-169.6-98.4L156 218z"/><path fill="#d38c0d" d="M170.4 316.4l168.8-99.2-168.8-97.6L0 218z"/><path fill="#704a0e" d="M248.8 270.8L416 172.4 248.8 74 78.4 172.4z"/><path fill="#513307" d="M248.8 270.8L416 172.4 248.8 74"/><path fill="#2d1c05" d="M248.8 270.8l36-21.6-36-20.8-36 20.8z"/><g fill="#0dd396"><path d="M368 379.6l40-23.2v-12.8l-40 23.2zM368 356.4l40-23.2v-12.8l-40 23.2zM368 333.2l40-23.2v-13.6l-40 24z"/></g></svg>

After

Width:  |  Height:  |  Size: 802 B

View File

@ -0,0 +1,53 @@
/**
* @prettier
*/
import React from 'react';
import { withRouter } from 'react-router-dom';
import withWidth, { isWidthUp } from '@material-ui/core/withWidth/index';
import ListItem from '@material-ui/core/ListItem/index';
import Typography from '@material-ui/core/Typography/index';
import { Wrapper, Inner, EmptyPackage, Heading, Card, List } from './styles';
import PackageImg from './img/package.svg';
// eslint-disable-next-line react/prop-types
const NotFound = ({ history, width }) => {
const handleGoTo = to => () => {
history.push(to);
};
const handleGoBack = () => () => {
history.goBack();
};
const renderList = () => (
<List>
<ListItem button={true} divider={true} onClick={handleGoTo('/')}>
{'Home'}
</ListItem>
<ListItem button={true} divider={true} onClick={handleGoBack()}>
{'Back'}
</ListItem>
</List>
);
const renderSubTitle = () => (
<Typography variant={'subtitle1'}>
<div>{"The page you're looking for doesn't exist."}</div>
<div>{'Perhaps these links will help find what you are looking for:'}</div>
</Typography>
);
return (
<Wrapper>
<Inner>
<EmptyPackage alt={'404 - Page not found'} src={PackageImg} />
<Heading variant={isWidthUp('sm', width) ? 'h2' : 'h4'}>{"Sorry, we couldn't find it..."}</Heading>
{renderSubTitle()}
<Card>{renderList()}</Card>
</Inner>
</Wrapper>
);
};
export default withRouter(withWidth()(NotFound));

View File

@ -0,0 +1,46 @@
/**
* @prettier
* @flow
*/
import styled from 'react-emotion';
import Typography from '@material-ui/core/Typography/index';
import { default as MuiList } from '@material-ui/core/List/index';
import { default as MuiCard } from '@material-ui/core/Card/index';
export const Wrapper = styled('div')`
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
flex: 1;
padding: 16px;
`;
export const Inner = styled('div')`
max-width: 650px;
display: flex;
flex-direction: column;
`;
export const EmptyPackage = styled('img')`
width: 150px;
margin: 0 auto;
`;
export const Heading = styled(Typography)`
&& {
color: #4b5e40;
}
`;
export const List = styled(MuiList)`
&& {
padding: 0;
color: #4b5e40;
}
`;
export const Card = styled(MuiCard)`
margin-top: 24px;
`;

View File

@ -0,0 +1,105 @@
/**
* @prettier
* @flow
*/
import React from 'react';
import type { Element } from 'react';
import { spacing } from '../../utils/styles/mixings';
import Tag from '../Tag';
import { formatDate, formatDateDistance } from '../../utils/package';
import { IProps } from './types';
import {
WrapperLink,
Header,
MainInfo,
Name,
Version,
Overview,
Published,
OverviewItem,
Description,
Icon,
Text,
Details,
Avatar,
Author,
Field,
Content,
Footer,
} from './styles';
const getInitialsName = (name: string) =>
name
.split(' ')
.reduce((accumulator, currentValue) => accumulator.charAt(0) + currentValue.charAt(0), '')
.toUpperCase();
const Package = ({ name: label, version, time, author: { name, avatar }, description, license, keywords = [] }: IProps): Element<WrapperLink> => {
const renderMainInfo = () => (
<MainInfo>
<Name>{label}</Name>
<Version>{`v${version}`}</Version>
</MainInfo>
);
const renderAuthorInfo = () => (
<Author>
<Avatar alt={name} src={avatar}>
{!avatar && getInitialsName(name)}
</Avatar>
<Details>
<Text text={name} weight={'bold'} />
</Details>
</Author>
);
const renderLicenseInfo = () =>
license && (
<OverviewItem>
<Icon modifiers={spacing('margin', '4px', '5px', '0px', '0px')} name={'license'} pointer={true} />
{license}
</OverviewItem>
);
const renderPublishedInfo = () => (
<OverviewItem>
<Icon name={'time'} pointer={true} />
<Published modifiers={spacing('margin', '0px', '5px', '0px', '0px')}>{`Published on ${formatDate(time)}`}</Published>
{`${formatDateDistance(time)} ago`}
</OverviewItem>
);
const renderDescription = () =>
description && (
<Field>
<Description>{description}</Description>
</Field>
);
return (
<WrapperLink className={'package'} to={`/-/web/version/${label}`}>
<Header>
{renderMainInfo()}
<Overview>
{renderLicenseInfo()}
{renderPublishedInfo()}
</Overview>
</Header>
<Content>
<Field>{renderAuthorInfo()}</Field>
{renderDescription()}
</Content>
{keywords.length > 0 && (
<Footer>
{keywords.sort().map((keyword, index) => (
<Tag key={index}>{keyword}</Tag>
))}
</Footer>
)}
</WrapperLink>
);
};
export default Package;

View File

@ -0,0 +1,199 @@
/**
* @prettier
* @flow
*/
import styled, { css } from 'react-emotion';
import { Link } from 'react-router-dom';
import { default as Photo } from '@material-ui/core/Avatar';
import { default as Ico } from '../Icon';
import mq from '../../utils/styles/media';
import { ellipsis } from '../../utils/styles/mixings';
import colors from '../../utils/styles/colors';
import Label from '../Label';
// HEADER
export const Header = styled.div`
&& {
display: flex;
flex-direction: column;
padding: 0 0 5px 0;
}
`;
export const Name = styled.span`
&& {
${ellipsis('50%')};
}
`;
export const MainInfo = styled.span`
&& {
font-size: 16px;
font-weight: 600;
line-height: 30px;
flex: 1;
color: #3a8bff;
padding: 0 10px 0 0;
overflow: hidden;
display: flex;
align-items: center;
:hover {
${Name} {
text-decoration: underline;
}
}
}
`;
export const OverviewItem = styled.span`
&& {
display: flex;
align-items: center;
margin: 0 0 5px 0;
color: ${colors.greyLight};
}
`;
export const Overview = styled.span`
&& {
position: relative;
display: flex;
flex-direction: column;
}
`;
export const Version = styled.span`
&& {
font-size: 12px;
padding: 0 0 0 10px;
margin: 0 0 0 5px;
color: #9f9f9f;
position: relative;
${ellipsis('100%')};
:before {
content: '•';
position: absolute;
left: 0;
}
}
`;
export const Icon = styled(Ico)`
&& {
margin: 1px 5px 0 0;
fill: ${colors.greyLight};
}
`;
export const Published = styled.span`
&& {
display: none;
color: ${colors.greyLight};
${({ modifiers }) => modifiers};
}
`;
// Content
export const Field = styled.div`
&& {
padding: 0 0 5px 0;
}
`;
export const Content = styled.div`
&& {
${Field} {
:last-child {
padding: 0;
}
}
}
`;
export const Text = styled(Label)`
&& {
color: #908ba1;
}
`;
export const Details = styled.span`
&& {
margin-left: 5px;
line-height: 14px;
display: flex;
flex-direction: column;
}
`;
export const Author = styled.div`
&& {
display: flex;
align-items: center;
}
`;
export const Avatar = styled(Photo)`
&& {
width: 30px;
height: 30px;
background: #4b5e40;
font-size: 15px;
}
`;
export const Description = styled.div`
&& {
margin: 5px 0;
}
`;
// Footer
export const Footer = styled.div`
&& {
display: none;
padding: 5px 0 0 0;
}
`;
// Container
export const WrapperLink = styled(Link)`
&& {
font-size: 12px;
background-color: white;
margin: 0 0 15px 0;
transition: box-shadow 0.15s;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.15);
border-radius: 3px;
padding: 10px;
text-decoration: none;
display: block;
color: #2f273c;
${mq.medium(css`
${Header} {
flex-direction: row;
justify-content: space-between;
align-items: center;
}
${OverviewItem} {
margin: 0 0 0 10px;
}
${Overview} {
flex-direction: row;
${OverviewItem} {
:first-child {
margin: 0;
}
}
}
${Footer} {
display: block;
}
${Published} {
display: inline-block;
}
`)};
}
`;

View File

@ -0,0 +1,20 @@
/**
* @prettier
* @flow
*/
export interface IProps {
name: string;
version: string;
time: string;
author: IAuthor;
description?: string;
keywords?: string[];
license?: string;
}
export interface IAuthor {
name: string;
avatar: string;
email: string;
}

View File

@ -0,0 +1,31 @@
import React from 'react';
import PropTypes from 'prop-types';
import isNil from 'lodash/isNil';
import Readme from '../Readme';
import classes from './packageDetail.scss';
const displayState = (description) => {
return !isNil(description) ? <Readme description={description} /> : '';
};
const PackageDetail = ({packageName, readMe}) => {
return (
<div className={classes.pkgDetail}>
<h1 className={classes.title}>
{packageName}
</h1>
<div className={classes.readme}>
{displayState(readMe)}
</div>
</div>
);
};
PackageDetail.propTypes = {
readMe: PropTypes.string,
packageName: PropTypes.string.isRequired,
};
export default PackageDetail;

View File

@ -0,0 +1,16 @@
@import '../../styles/variables';
@import '../../styles/mixins';
.pkgDetail {
.title {
font-size: $font-size-xxl;
font-weight: $font-weight-semibold;
margin: 0 0 40px;
padding-bottom: 5px;
@include border-bottom-default($greyGainsboro);
}
.readme {
margin-bottom: 5em;
}
}

View File

@ -0,0 +1,52 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import Package from '../Package';
import Help from '../Help';
import { formatAuthor, formatLicense } from '../../utils/package';
import classes from './packageList.scss';
export default class PackageList extends React.Component {
static propTypes = {
packages: PropTypes.array,
};
renderPackages = () => {
return (
<Fragment>
{this.renderList()}
</Fragment>
);
}
renderList = () => {
const { packages } = this.props;
return (
packages.map((pkg, i) => {
const { name, version, description, time, keywords } = pkg;
const author = formatAuthor(pkg.author);
const license = formatLicense(pkg.license);
return (
<Package key={i} {...{ name, version, author, description, license, time, keywords }} />
);
})
);
}
render() {
return (
<div className={"package-list-items"}>
<div className={classes.pkgContainer}>
{this.hasPackages() ? this.renderPackages(): <Help /> }
</div>
</div>
);
}
hasPackages() {
const {packages} = this.props;
return packages.length > 0;
}
}

View File

@ -0,0 +1,12 @@
@import '../../styles/mixins';
.pkgContainer {
margin: 0;
padding: 0;
.listTitle {
font-weight: $font-weight-regular;
font-size: $font-size-xl;
margin: 0 0 10px 0;
}
}

View File

@ -0,0 +1,25 @@
import React from 'react';
import PropTypes from 'prop-types';
import classes from './style.scss';
export default function Module({title, description, children, className}) {
return (
<div className={`${classes.module} ${className}`}>
<h2 className={classes.moduleTitle}>
{title}
{description && <span>{description}</span>}
</h2>
<div>
{children}
</div>
</div>
);
}
Module.propTypes = {
title: PropTypes.string.isRequired,
description: PropTypes.string,
children: PropTypes.any.isRequired,
className: PropTypes.string,
};

View File

@ -0,0 +1,24 @@
@import '../../../styles/variables';
@import '../../../styles/mixins';
.module {
margin-bottom: 10px;
.moduleTitle {
display: flex;
align-items: flex-end;
font-size: $font-size-lg;
margin: 0 0 10px;
padding: 5px 0;
font-weight: $font-weight-semibold;
@include border-bottom-default($greyGainsboro);
span { // description
font-size: $font-size-sm;
color: $greyChateau;
margin-left: auto;
font-weight: $font-weight-light;
}
}
}

View File

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

View File

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

View File

@ -0,0 +1,104 @@
import React from 'react';
import PropTypes from 'prop-types';
import get from 'lodash/get';
import LastSync from './modules/LastSync';
import DistTags from './modules/DistTags';
import Maintainers from './modules/Maintainers';
import Dependencies from './modules/Dependencies';
import PeerDependencies from './modules/PeerDependencies';
import Infos from './modules/Infos';
import {
formatLicense,
formatRepository,
getLastUpdatedPackageTime,
getRecentReleases,
} from '../../utils/package';
import API from '../../utils/api';
import {DIST_TAGS} from '../../../lib/constants';
export default class PackageSidebar extends React.Component {
state = {};
static propTypes = {
packageName: PropTypes.string.isRequired,
};
constructor(props) {
super(props);
this.loadPackageData = this.loadPackageData.bind(this);
}
async componentDidMount() {
const { packageName } = this.props;
await this.loadPackageData(packageName);
}
async loadPackageData(packageName) {
let packageMeta;
try {
packageMeta = await API.request(`sidebar/${packageName}`, 'GET');
} catch (err) {
this.setState({
failed: true,
});
}
this.setState({
packageMeta,
});
}
render() {
const { packageMeta } = this.state;
if (packageMeta) {
const {time, _uplinks} = packageMeta;
// Infos component
const license = formatLicense(get(packageMeta, 'latest.license', null));
const repository = formatRepository(
get(packageMeta, 'latest.repository', null)
);
const homepage = get(packageMeta, 'latest.homepage', null);
// dist-tags
const distTags = packageMeta[DIST_TAGS];
// Lastsync component
const recentReleases = getRecentReleases(time);
const lastUpdated = getLastUpdatedPackageTime(_uplinks);
// Dependencies component
const dependencies = get(packageMeta, 'latest.dependencies', {});
const peerDependencies = get(packageMeta, 'latest.peerDependencies', {});
// Maintainers component
return (
<aside className={'sidebar-info'}>
{time && (
<LastSync
lastUpdated={lastUpdated}
recentReleases={recentReleases}
/>
)}
<DistTags distTags={distTags} />
<Infos
homepage={homepage}
license={license}
repository={repository}
/>
{/* TODO: Refacor later, when we decide to show only maintainers/authors */}
<Maintainers packageMeta={packageMeta} />
<Dependencies dependencies={dependencies} />
<PeerDependencies dependencies={peerDependencies} />
{/* Package management module? Help us implement it! */}
</aside>
);
}
return (
<aside className={'sidebar-loading'}>{'Loading package information...'}</aside>
);
}
}

View File

@ -0,0 +1,50 @@
import React from 'react';
import PropTypes from 'prop-types';
import Module from '../../Module';
import {getDetailPageURL} from '../../../../utils/url';
import ModuleContentPlaceholder from '../../ModuleContentPlaceholder';
import classes from './style.scss';
export const NO_DEPENDENCIES = 'Zero Dependencies!';
export const DEP_ITEM_CLASS = 'dependency-item';
const renderDependenciesList = (dependencies, dependenciesList) => {
return (
<ul>
{dependenciesList.map((dependenceName, index) => {
return (
<li
className={DEP_ITEM_CLASS}
key={index}
title={`Depend on version: ${dependencies[dependenceName]}`}
>
<a href={getDetailPageURL(dependenceName)}>{dependenceName}</a>
{index < dependenciesList.length - 1 && <span>{',&nbsp;'}</span>}
</li>
);
})}
</ul>
);
};
const Dependencies = ({dependencies = {}, title = 'Dependencies'}) => {
const dependenciesList = Object.keys(dependencies);
return (
<Module className={classes.dependenciesModule} title={title}>
{dependenciesList.length > 0 ? (
renderDependenciesList(dependencies, dependenciesList)
) : (
<ModuleContentPlaceholder text={NO_DEPENDENCIES} />
)}
</Module>
);
};
Dependencies.propTypes = {
dependencies: PropTypes.object,
title: PropTypes.string,
};
export default Dependencies;

View File

@ -0,0 +1,13 @@
@import '../../../../styles/variables';
.dependenciesModule {
li {
display: inline-block;
font-size: $font-size-sm;
line-height: $line-height-xxs;
a {
color: inherit;
}
}
}

View File

@ -0,0 +1,50 @@
import React from 'react';
import propTypes from 'prop-types';
import Module from '../../Module';
import ModuleContentPlaceholder from '../../ModuleContentPlaceholder';
import classes from './style.scss';
const renderDistTags = (distTags) => {
const tags = Object.entries(distTags);
return (
<ul>
{tags.map((tagItem) => {
const [tag, version] = tagItem;
return (
<li className={'dist-tag-item'} key={tag}>
<span>{tag}</span>
<span>{version}</span>
</li>
);
})}
</ul>
);
};
const DistTags = ({distTags = {}}) => {
const hasTags = Object.keys(distTags).length > 0;
return (
<Module
className={classes.releasesModule}
description={''}
title={'Dist-Tags'}
>
{hasTags ? (
renderDistTags(distTags)
) : (
<ModuleContentPlaceholder text={'Not Available!'} />
)}
</Module>
);
};
DistTags.propTypes = {
distTags: propTypes.object,
};
export default DistTags;

View File

@ -0,0 +1,13 @@
@import '../../../../styles/variables';
.releasesModule {
li {
display: flex;
font-size: $font-size-sm;
line-height: $line-height-xs;
span:last-child {
margin-left: auto;
}
}
}

View File

@ -0,0 +1,40 @@
import React from 'react';
import PropTypes from 'prop-types';
import Module from '../../Module';
import ModuleContentPlaceholder from '../../ModuleContentPlaceholder';
import classes from './style.scss';
const renderSection = (title, url) => (
<li>
<span>{title}</span>
<a href={url} rel={'noopener noreferrer'} target={'_blank'}>
{url}
</a>
</li>
);
const Infos = ({homepage, repository, license}) => {
const showInfo = homepage || repository || license;
return (
<Module className={classes.infosModule} title={'Infos'}>
{showInfo ? (
<ul>
{homepage && renderSection('Homepage', homepage)}
{repository && renderSection('Repository', repository)}
{license && (
<li>
<span>{'License'}</span>
<span>{license}</span>
</li>)}
</ul>) : <ModuleContentPlaceholder text={'Not Available!'} />}
</Module>);
};
Infos.propTypes = {
homepage: PropTypes.string,
repository: PropTypes.string,
license: PropTypes.string,
};
export default Infos;

View File

@ -0,0 +1,21 @@
@import '../../../../styles/variables';
@import '../../../../styles/mixins';
.infosModule {
li {
display: flex;
font-size: $font-size-sm;
line-height: $line-height-xs;
a {
color: inherit;
max-width: 150px;
@include ellipsis;
}
a:last-child,
span:last-child {
margin-left: auto;
}
}
}

View File

@ -0,0 +1,43 @@
import React from 'react';
import propTypes from 'prop-types';
import Module from '../../Module';
import ModuleContentPlaceholder from '../../ModuleContentPlaceholder';
import classes from './style.scss';
const renderRecentReleases = (recentReleases) => (
<ul>
{recentReleases.map((versionInfo) => {
const {version, time} = versionInfo;
return (
<li className={'last-sync-item'} key={version}>
<span>{version}</span>
<span>{time}</span>
</li>
);
})}
</ul>
);
const LastSync = ({recentReleases = [], lastUpdated = ''}) => {
return (
<Module
className={classes.releasesModule}
description={lastUpdated}
title={'Last Sync'}
>
{recentReleases.length ? (
renderRecentReleases(recentReleases)
) : (
<ModuleContentPlaceholder text={'Not Available!'} />
)}
</Module>
);
};
LastSync.propTypes = {
recentReleases: propTypes.array,
lastUpdated: propTypes.string,
};
export default LastSync;

View File

@ -0,0 +1,13 @@
@import '../../../../styles/variables';
.releasesModule {
li {
display: flex;
font-size: $font-size-sm;
line-height: $line-height-xs;
span:last-child {
margin-left: auto;
}
}
}

View File

@ -0,0 +1,22 @@
import React from 'react';
import PropTypes from 'prop-types';
import classes from './style.scss';
const MaintainerInfo = ({title, name, avatar}) => {
const avatarDescription = `${title} ${name}'s avatar`;
return (
<div className={classes.maintainer} title={name}>
<img alt={avatarDescription} src={avatar} title={avatarDescription} />
<span className={'maintainer-name'}>{name}</span>
</div>
);
};
MaintainerInfo.propTypes = {
title: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
avatar: PropTypes.string.isRequired,
};
export default MaintainerInfo;

View File

@ -0,0 +1,26 @@
@import '../../../../../styles/variables';
@import '../../../../../styles/mixins';
.maintainer {
display: flex;
line-height: $line-height-xl;
cursor: default;
&:not(:last-child) {
margin-bottom: 10px;
}
img {
width: 30px;
height: 30px;
margin-right: 10px;
border-radius: 100%;
flex-shrink: 0;
}
span {
font-size: $font-size-sm;
flex-shrink: 1;
@include ellipsis;
}
}

View File

@ -0,0 +1,122 @@
import React from 'react';
import PropTypes from 'prop-types';
import get from 'lodash/get';
import filter from 'lodash/filter';
import size from 'lodash/size';
import has from 'lodash/has';
import uniqBy from 'lodash/uniqBy';
import Module from '../../Module';
import MaintainerInfo from './MaintainerInfo';
import ModuleContentPlaceholder from '../../ModuleContentPlaceholder';
import classes from './style.scss';
const CONTRIBUTORS_TO_SHOW = 5;
export default class Maintainers extends React.Component {
static propTypes = {
packageMeta: PropTypes.object.isRequired,
};
state = {};
constructor(props) {
super(props);
this.handleShowAllContributors = this.handleShowAllContributors.bind(this);
}
get author() {
return get(this, 'props.packageMeta.latest.author');
}
get contributors() {
const contributors = get(this, 'props.packageMeta.latest.contributors', {});
return filter(contributors, (contributor) => {
return (
contributor.name !== get(this, 'author.name') &&
contributor.email !== get(this, 'author.email')
);
});
}
get showAllContributors() {
const { showAllContributors } = this.state;
return showAllContributors || size(this.contributors) <= 5;
}
get uniqueContributors() {
if (!this.contributors) {
return [];
}
return uniqBy(this.contributors, (contributor) => contributor.name).slice(
0,
CONTRIBUTORS_TO_SHOW
);
}
handleShowAllContributors() {
this.setState({
showAllContributors: true,
});
}
renderContributors() {
if (!this.contributors) return null;
return (this.showAllContributors
? this.contributors
: this.uniqueContributors
).map((contributor, index) => {
return (
<MaintainerInfo
avatar={contributor.avatar}
key={index}
name={contributor.name}
title={'Contributors'}
/>
);
});
}
renderAuthorAndContributors(author) {
return (
<div>
<ul className={'maintainer-author'}>
{author &&
author.name && (
<MaintainerInfo
avatar={author.avatar}
name={author.name}
title={'Author'}
/>
)}
{this.renderContributors()}
</ul>
{!this.showAllContributors && (
<button
className={classes.showAllContributors}
onClick={this.handleShowAllContributors}
title={'Current list only show the author and first 5 contributors unique by name'}
>
{'Show all contributor'}
</button>
)}
</div>
);
}
render() {
const contributors = this.renderContributors();
return (
<Module className={classes.maintainersModule} title={'Maintainers'}>
{contributors.length || has(this.author, 'name') ? (
this.renderAuthorAndContributors(this.author)
) : (
<ModuleContentPlaceholder text={'Not Available!'} />
)}
</Module>
);
}
}

View File

@ -0,0 +1,13 @@
@import '../../../../styles/variables';
.maintainersModule {
.showAllContributors {
cursor: pointer;
width: 100%;
background: none;
border: none;
font-size: $font-size-sm;
text-align: center;
padding: 10px 0;
}
}

View File

@ -0,0 +1,18 @@
import React from 'react';
import PropTypes from 'prop-types';
import Dependencies from '../Dependencies';
export const TITLE = 'Peer Dependencies';
const PeerDependencies = ({dependencies = {}, title = TITLE}) => {
return (
<Dependencies dependencies={dependencies} title={title} />
);
};
PeerDependencies.propTypes = {
dependencies: PropTypes.object,
title: PropTypes.string,
};
export default PeerDependencies;

View File

@ -0,0 +1,13 @@
/**
* @prettier
* @flow
*/
import React from 'react';
import 'github-markdown-css';
import { IProps } from './types';
const Readme = ({ description }: IProps) => <div className={'markdown-body'} dangerouslySetInnerHTML={{ __html: description }} />;
export default Readme;

View File

@ -0,0 +1,8 @@
/**
* @prettier
* @flow
*/
export interface IProps {
description: string;
}

View File

@ -0,0 +1,90 @@
/**
* @prettier
* @flow
*/
import React, { Component } from 'react';
import type { IProps, IState } from './types';
import { CommandContainer } from './styles';
import CopyToClipBoard from '../CopyToClipBoard';
import Tabs from '@material-ui/core/Tabs/index';
import Tab from '@material-ui/core/Tab/index';
import Typography from '@material-ui/core/Typography/index';
import { getCLISetRegistry, getCLIChangePassword, getCLISetConfigRegistry } from '../../utils/cli-utils';
import { NODE_MANAGER } from '../../utils/constants';
/* eslint react/prop-types:0 */
function TabContainer({ children }) {
return (
<CommandContainer>
<Typography component={'div'} style={{ padding: 0, minHeight: 170 }}>
{children}
</Typography>
</CommandContainer>
);
}
class RegistryInfoContent extends Component<IProps, IState> {
state = {
tabPosition: 0,
};
render() {
return <div>{this.renderTabs()}</div>;
}
renderTabs() {
const { scope, registryUrl } = this.props;
const { tabPosition } = this.state;
return (
<React.Fragment>
<Tabs indicatorColor={'primary'} onChange={this.handleChange} textColor={'primary'} value={tabPosition} variant={'fullWidth'}>
<Tab label={NODE_MANAGER.npm} />
<Tab label={NODE_MANAGER.pnpm} />
<Tab label={NODE_MANAGER.yarn} />
</Tabs>
{tabPosition === 0 && <TabContainer>{this.renderNpmTab(scope, registryUrl)}</TabContainer>}
{tabPosition === 1 && <TabContainer>{this.renderPNpmTab(scope, registryUrl)}</TabContainer>}
{tabPosition === 2 && <TabContainer>{this.renderYarnTab(scope, registryUrl)}</TabContainer>}
</React.Fragment>
);
}
renderNpmTab(scope: string, registryUrl: string) {
return (
<React.Fragment>
<CopyToClipBoard text={getCLISetConfigRegistry(`${NODE_MANAGER.npm} set`, scope, registryUrl)} />
<CopyToClipBoard text={getCLISetRegistry(`${NODE_MANAGER.npm} adduser`, registryUrl)} />
<CopyToClipBoard text={getCLIChangePassword(NODE_MANAGER.npm, registryUrl)} />
</React.Fragment>
);
}
renderPNpmTab(scope: string, registryUrl: string) {
return (
<React.Fragment>
<CopyToClipBoard text={getCLISetConfigRegistry(`${NODE_MANAGER.pnpm} set`, scope, registryUrl)} />
<CopyToClipBoard text={getCLISetRegistry(`${NODE_MANAGER.pnpm} adduser`, registryUrl)} />
<CopyToClipBoard text={getCLIChangePassword(NODE_MANAGER.pnpm, registryUrl)} />
</React.Fragment>
);
}
renderYarnTab(scope: string, registryUrl: string) {
return (
<React.Fragment>
<CopyToClipBoard text={getCLISetConfigRegistry(`${NODE_MANAGER.yarn} config set`, scope, registryUrl)} />
</React.Fragment>
);
}
handleChange = (event: any, tabPosition: number) => {
event.preventDefault();
this.setState({ tabPosition });
};
}
export default RegistryInfoContent;

View File

@ -0,0 +1,7 @@
import styled from 'react-emotion';
export const CommandContainer = styled.div`
&& {
padding-top: 20px;
}
`;

Some files were not shown because too many files have changed in this diff Show More