diff --git a/README.md b/README.md index 49cab3b6f..6a004f15e 100644 --- a/README.md +++ b/README.md @@ -49,12 +49,12 @@ We love the default GitHub profiles and we want to enhance them: GitHub. We are showing them **all**, even those you don't own and those owned by organizations you're not in.[1](#footnote1) * The GitHub profiles are listing all the repos you own but they sort them only by age of the - latest commit. We prefer to **sort repos** by a combination of how active they are, how much you - have contributed to them, how popular they are, etc. For each user we want to see first the latest - greatest repos they have most contributed to. -* On GitHub only repos earn stars. We push it one step further by transferring these **stars to - users**. If you have built 23% of a 145 stars repo, you deserve 33 stars for that contribution. We - add all these stars and clearly show how many of them you earned in total. + latest commit. We prefer to **sort repos** by a combination of how much you + have contributed to them, their size, how popular they are, etc. For each user we want to see + first the latest greatest repos they have most contributed to. +* On GitHub only repos earn stars. We push it one step further by having **users earn stars**: + You earn stars when you contribute to a repo. + We add all these earned stars and show how many you've earned in total. * The GitHub profiles don't clearly show how big your contribution to a repo was, when you don't own it. Maybe you wrote 5%. Maybe 90%. We **make it clear**. * GitHub detects programming languages. We want to also know about diff --git a/docs/repo-settings.md b/docs/repo-settings.md index 7a4dbe6f3..8c77643ff 100644 --- a/docs/repo-settings.md +++ b/docs/repo-settings.md @@ -33,7 +33,8 @@ See [this example](../.ghuser.io.json). ```json { "_comment": "Repo metadata for ghuser.io. See https://github.com/ghuser-io/ghuser.io/blob/master/docs/repo-settings.md", - "techs": ["React", "Node.js", "Reframe", "Bootstrap", "Semantic UI", "AWS"] + "techs": ["React", "Node.js", "Reframe", "hapi", "Bootstrap", "Semantic UI", "AWS", "Ansible", + "Travis CI"] } ``` diff --git a/docs/repo-settings.png b/docs/repo-settings.png index a9c20ebb1..e476f0201 100644 Binary files a/docs/repo-settings.png and b/docs/repo-settings.png differ diff --git a/docs/screenshot-data-age.png b/docs/screenshot-data-age.png index 1428add1d..f221f4ed7 100644 Binary files a/docs/screenshot-data-age.png and b/docs/screenshot-data-age.png differ diff --git a/docs/screenshot.png b/docs/screenshot.png index 72045f282..3192da459 100644 Binary files a/docs/screenshot.png and b/docs/screenshot.png differ diff --git a/reframe/views/All.css b/reframe/views/All.css index 1dbbc484a..e413d51ac 100644 --- a/reframe/views/All.css +++ b/reframe/views/All.css @@ -49,6 +49,10 @@ a:hover display: inline-block; color: #bcc0c4; } +/* avoid external link icon above to be shown in a new line */ +.external { + white-space: nowrap; +} .container-lg { max-width: 1012px; @@ -64,3 +68,22 @@ a:hover .text-bold { font-weight: 600; } + +body { + padding-bottom: 90px; +} + +.emoji { + height: 1.3em; + width: auto; +} + +.icon-vertical-align { + vertical-align: middle !important; +} + +/* TODO remove this */ +/* this is too global */ +.icon { + vertical-align: middle !important; +} diff --git a/reframe/views/profile/Avatar.css b/reframe/views/profile/Avatar.css index 0a48f3cfc..b3b36a0dc 100644 --- a/reframe/views/profile/Avatar.css +++ b/reframe/views/profile/Avatar.css @@ -1,8 +1,32 @@ .avatar { max-width: 100%; height: auto; + display: inline-flex; + align-items: center; + justify-content: center; } +.avatar-small { + float: left; + width: 50px; + height: 50px; + margin-right: .5em; + margin-bottom: -.5em; +} + +.avatar-add { + border-width: medium !important; + text-align: center; + opacity: 0.55; + transition: opacity 0.5s; +} +.avatar-add:hover { + opacity: 1; +} .avatar-add-sign { + display: inline-block; vertical-align: middle; + background-color: #eee; + width: 24px; + margin-top: -5px; } diff --git a/reframe/views/profile/Avatar.js b/reframe/views/profile/Avatar.js index ce7a2ed0a..4861d767b 100644 --- a/reframe/views/profile/Avatar.js +++ b/reframe/views/profile/Avatar.js @@ -2,13 +2,8 @@ import React from 'react'; import './Avatar.css'; -const Avatar = props => { - if (props.type == "add") { // special button for explaining how to add an avatar - return
- + -
; - } - return ; -}; +const Avatar = ({classes, url}) => ( + +); export default Avatar; diff --git a/reframe/views/profile/AvatarAdd.js b/reframe/views/profile/AvatarAdd.js new file mode 100644 index 000000000..89feb884f --- /dev/null +++ b/reframe/views/profile/AvatarAdd.js @@ -0,0 +1,11 @@ +import React from 'react'; + +export default AvatarAdd; + +function AvatarAdd() { + return ( +
+ + +
+ ); +} diff --git a/reframe/views/profile/leftpanel/LeftPanel.js b/reframe/views/profile/leftpanel/LeftPanel.js index 1542579eb..8200ea7a7 100644 --- a/reframe/views/profile/leftpanel/LeftPanel.js +++ b/reframe/views/profile/leftpanel/LeftPanel.js @@ -8,20 +8,17 @@ import VCardDetails from './VCardDetails'; import './LeftPanel.css'; import Avatar from '../Avatar'; import {urls} from '../../../ghuser'; +import {getTotalEarnedStars} from './../rightpanel/contrib/badges/Badges'; const LeftPanel = props => { - let stars = 0; - for (const repo in props.contribs && props.contribs.repos) { - const contrib = props.contribs.repos[repo]; - stars += contrib.percentage * contrib.stargazers_count / 100; - } + const totalEarnedStars = getTotalEarnedStars(props.contribs); return (
+ url={props.user.html_url} stars={totalEarnedStars} /> { ); - const memberOf = []; - for (const org of props.userOrgs) { - if (props.allOrgs[org]) { - memberOf.push(orgAvatar(org)); - } - } const contributedTo = []; for (const org of props.contribOrgs) { if (props.allOrgs[org]) { @@ -24,26 +18,14 @@ const Orgs = props => { } } - if (!memberOf.length && !contributedTo.length) { - return
; - } - - const sections = []; - let classes = 'mb-1'; - if (memberOf.length) { - sections.push(

Member of

{memberOf}
); - } - if (contributedTo.length) { - if (memberOf.length) { - classes = `mt-4 ${classes}`; - } - sections.push( -

Contributed to

{contributedTo}
- ); + if( contributedTo.length===0 ) { + return null; } return ( -
{sections}
+
+

Contributed to

{contributedTo}
+
); }; diff --git a/reframe/views/profile/leftpanel/VCard.css b/reframe/views/profile/leftpanel/VCard.css index a46e8a4db..ae5982a4a 100644 --- a/reframe/views/profile/leftpanel/VCard.css +++ b/reframe/views/profile/leftpanel/VCard.css @@ -20,4 +20,13 @@ .vcard-stars { white-space: nowrap; + position: relative; + border-radius: 5px; + padding-top: 1px; + padding-bottom: 1px; + padding-left: 5px; + padding-right: 7px; + + border: 1px solid #e0e0e0; + background-color: #fafafa; } diff --git a/reframe/views/profile/leftpanel/VCard.js b/reframe/views/profile/leftpanel/VCard.js index 678e74ed7..80c8a547d 100644 --- a/reframe/views/profile/leftpanel/VCard.js +++ b/reframe/views/profile/leftpanel/VCard.js @@ -1,21 +1,31 @@ import React from 'react'; import './VCard.css'; -import {bigNum} from '../numbers'; +import {bigNum} from '../../utils/pretty-numbers'; -const VCard = props => { - const stars = Math.round(props.stars); +const VCard = props => ( +
+

+
{props.name}
+ +

+
+); +const Stars = ({stars, login}) => { + stars = Math.round(stars); + if( stars < 1 ) { + return null; + } return ( -
-

-
{props.name}
-
- {props.login} - {stars >= 1 && ★ {bigNum(stars)}} -
-

-
+ + +   + {bigNum(stars)} + ); }; diff --git a/reframe/views/profile/rightpanel/RightPanel.css b/reframe/views/profile/rightpanel/RightPanel.css deleted file mode 100644 index c8d59a470..000000000 --- a/reframe/views/profile/rightpanel/RightPanel.css +++ /dev/null @@ -1,49 +0,0 @@ -.user-profile-nav { - border-bottom-width: 1px; - border-bottom-style: solid; - border-bottom-color: #d1d5da; -} - -.UnderlineNav-body { - display: flex; - margin-bottom: -1px; -} - -.UnderlineNav-item { - padding: 16px 8px; - margin-right: 16px; - font-size: 14px; - line-height: 1.5; - color: #586069; - text-align: center; - border-bottom: 2px solid transparent; -} - -.UnderlineNav-item:hover, -.UnderlineNav-item:focus { - color: #24292e; - text-decoration: none; - border-bottom-color: #d1d5da; - transition: 0.2s ease; -} - -.UnderlineNav-item.selected { - font-weight: 600; - color: #24292e; - border-bottom-color: #e36209; -} - -.contribs { - font-size: 14px; -} - -.updated-hint { - text-align: right; -} - -.avatar-small { - float: left; - width: 50px; - margin-right: .5em; - margin-bottom: -.5em; -} diff --git a/reframe/views/profile/rightpanel/RightPanel.js b/reframe/views/profile/rightpanel/RightPanel.js index cb2de4442..8330ddaf2 100644 --- a/reframe/views/profile/rightpanel/RightPanel.js +++ b/reframe/views/profile/rightpanel/RightPanel.js @@ -6,8 +6,8 @@ import * as Parser from 'html-react-parser'; import {urls} from '../../../ghuser'; import CreateYourProfile from './CreateYourProfile'; import ProfileBeingCreated from './ProfileBeingCreated'; -import Contrib from './contrib/Contrib'; -import './RightPanel.css'; +import {Contrib} from './contrib/Contrib'; +import {getContribDisplayOrder} from './contrib/getContribScore'; const RightPanel = props => { // Use these queues to avoid filling up the event loop: @@ -21,24 +21,23 @@ const RightPanel = props => { ); }; - const compare = (a, b) => { - if (a.total_score < b.total_score) { - return 1; - } - if (a.total_score > b.total_score) { - return -1; - } - return 0; - }; - const repos = []; if (props.contribs) { const contribs = Object.values(props.contribs.repos); - contribs.sort(compare); + contribs.sort(getContribDisplayOrder); const uniqueNames = []; - for (const contrib of contribs) { + for (const i in contribs) { + const contrib = contribs[i]; + + // Don't include repos where user has made 0 commits. This happens when a user + // makes a PR that is not merged. + if( contrib.percentage===0 ) { + continue; + } + + // We don't want to have two repos with the same name. This happens when a user is // contributing to a project and has a fork with the same name: if (uniqueNames.indexOf(contrib.name) > -1) { @@ -48,6 +47,7 @@ const RightPanel = props => { repos.push( ); } @@ -93,22 +93,12 @@ const RightPanel = props => { return (
- -
+
{repos}
{ props.contribs && -
+
Updated {moment(props.fetchedat).fromNow()}.
} diff --git a/reframe/views/profile/rightpanel/contrib/Badge.js b/reframe/views/profile/rightpanel/contrib/Badge.js deleted file mode 100644 index c00414d9a..000000000 --- a/reframe/views/profile/rightpanel/contrib/Badge.js +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react'; - -const Badge = props => ( - {props.text} -); - -export default Badge; diff --git a/reframe/views/profile/rightpanel/contrib/Contrib.css b/reframe/views/profile/rightpanel/contrib/Contrib.css index a7a08f112..11a265347 100644 --- a/reframe/views/profile/rightpanel/contrib/Contrib.css +++ b/reframe/views/profile/rightpanel/contrib/Contrib.css @@ -1,15 +1,19 @@ -.contrib-name { - display: inline-block; - vertical-align: middle; +.contrib-link-icon { + text-align: center; + width: 18px; } -.avatar-add { - border-width: medium !important; - background-color: #eee; - width: 30px !important; - height: 30px; - text-align: center; - vertical-align: middle; - margin-top: -3px; - margin-bottom: 1em !important; +.small-text { + font-size: 0.8em; + opacity: 0.85; } + +.repo-descr { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: calc(100% + - 50px - .5em /*avatar-small*/ + - 2em); /*dropdown icon*/ +} + diff --git a/reframe/views/profile/rightpanel/contrib/Contrib.js b/reframe/views/profile/rightpanel/contrib/Contrib.js index d62d7c9a7..b1d1b4955 100644 --- a/reframe/views/profile/rightpanel/contrib/Contrib.js +++ b/reframe/views/profile/rightpanel/contrib/Contrib.js @@ -1,15 +1,27 @@ import React from 'react'; -import * as moment from 'moment'; -import Badge from './Badge'; -import RepoDescrAndDetails from './RepoDescrAndDetails'; -import './Contrib.css'; -import Avatar from '../../Avatar'; -import {withSeparator} from '../../css'; -import {bigNum, roundHalf} from '../../numbers'; import * as db from '../../../../db'; import {urls} from '../../../../ghuser'; +import RichText from '../../../utils/RichText'; +import {Accordion, AccordionHead, AccordionBody, AccordionBadgerIcon, stopPropagationOnLinks} from '../../../utils/Accordion'; +import ProgressBar from '../../../utils/ProgressBar'; +import {numberOf} from '../../../utils/pretty-numbers'; + +import Avatar from '../../Avatar'; +import AvatarAdd from '../../AvatarAdd'; +import AddSettings from '../../AddSettings'; +import {Badges, BadgesMini, BadgesMultiLine, getContribType} from './badges/Badges'; +import {getContribScore} from './getContribScore'; +import {getCommitCounts, getRepoAvatar} from './getContribInfo'; +import Language from './Language'; + +import './Contrib.css'; + + +export {Contrib}; + + class Contrib extends React.Component { constructor(props) { super(props); @@ -31,117 +43,268 @@ class Contrib extends React.Component { } render() { - const strStars = numStars => `★ ${bigNum(numStars)}`; - const strLastPushed = pushedAt => `last pushed ${moment(pushedAt).fromNow()}`; - const strNumCommits = numCommits => `${bigNum(numCommits)} non-merge commits`; + if( ! this.state.loading && this.state.repo && this.props.i>=10 ) { + return ; + } const avatar = () => { if (this.state.loading) { - return ; - } - if (this.state.repo && this.state.repo.settings && this.state.repo.settings.avatar_url) { - return ; - } - if (this.state.repo && this.state.repo.organization && - this.state.repo.organization.avatar_url) { - return ; - } - return ; - }; - - const badges = (owner, isFork, percentage, numContributors, popularity, numStars, activity, - pushedAt, maturity, numCommits, isMaintainer) => { - const result = []; - if (!isFork && this.props.username === owner || percentage >= 80) { - result.push( - + return ( + + + ); - } else if (isMaintainer) { - result.push( - - ); - } - if (numContributors > 1) { - result.push(); - } - if (popularity > 2.5) { - result.push(); } - if (activity > 2.5) { - result.push(); + const repoAvatar = getRepoAvatar(this.state.repo); + if( repoAvatar ) { + return ; } - if (maturity > 2.5) { - result.push(); - } - return result; + return ( + + ); }; - const earnedStars = (percentage, numStars) => { - let earned = percentage * numStars / 100; - if (earned < .5) { - return ''; - } + const LEFT_PADDING = 67; - earned = bigNum(earned); - const total = bigNum(numStars); - let displayStr = `★ ${earned}`; - if (earned !== total) { - displayStr += ` / ${total}`; - } + const badgesLine = ( + + ); + const accordionHead = ( + +
+ {avatar()} +
+ + {badgesLine} + +
+ ); + + const accordionBody = ( + + ); + + return ( + + {accordionHead} + {accordionBody} + + ); + } +} + +function ContribMini(props) { + const LEFT_PADDING = 100; + + const badgeLine = ( + + ); + + const accordionHead = ( + + {badgeLine} + + + ); + + const accordionBody = ( + + ); + return ( + + {accordionHead} + {accordionBody} + + ); +} + +function ContribHeader({username, contrib: {name, full_name}, repo}) { + if( ! repo ) { + return null; + } + const display_name = repo.owner===username ? name : full_name; return ( - {displayStr} +
+ + { repo.owner !== username && + repo.owner+'/' + } + {name} + +     + {RichText(repo.description)} +
); - }; +} + +function Languages({repo, style={}}) { + + const languageViews = []; + + const languages = repo && repo.languages; - const userIsMaintainer = this.props.contrib.percentage >= 15; + const techs = repo && repo.settings && repo.settings.techs; + + if (languages) { + for (const language of Object.keys(languages)) { + languageViews.push(); + } + } + if( techs ) { + for (const tech of techs) { + languageViews.push(); + } + } + + if( languageViews.length === 0 ) { + return null; + } return ( -
- {avatar()} -

- {this.props.contrib.name} - { - this.state.repo && this.state.repo.fork && - - } -

- { - this.state.repo && - badges(this.state.repo.owner, this.state.repo.fork, this.props.contrib.percentage, - Object.keys(this.state.repo.contributors || []).length, this.props.contrib.popularity, - this.state.repo.stargazers_count, this.props.contrib.activity, - this.state.repo.pushed_at, this.props.contrib.maturity, - this.props.contrib.total_commits_count, userIsMaintainer) - } - { - this.state.repo && - earnedStars(this.props.contrib.percentage, this.state.repo.stargazers_count)} - { - this.state.repo && - - } +
+ {languageViews} +
); - } } -export default Contrib; +function ContribExpandedContent({repo, username, contrib, style={}, className="", pushToFunctionQueue}) { + const Spacer = ({mod}) =>
; + + const languagesView = Languages({repo}); + + return ( + + + {languagesView && ( + + {languagesView} + + + )} + + + + + + + + ); +} + +function BadgesExplanation(props) { + return ( +
+ +
+ How these badges are determined and the earned stars calculated is explained +
+
+ ); +} + +function ScoreExplanation({contrib}) { + const {contribScore, userCommitsCount, starBoost, contribBoost} = getContribScore(contrib); + + const contribScorePretty = Math.round(contribScore); + const starBoostPretty = starBoost.toFixed(2); + const contribBoostPretty = contribBoost.toFixed(2); + + return ( +
+ Contribution score: {contribScorePretty} +
+ Calculation: {contribScorePretty} = {userCommitsCount} (user commits) * {starBoostPretty} (star boost) * {contribBoostPretty} (contrib boost) +
+ The contributions on this page are sorted according to this score, more infos +
+
+ ); +} + +function ExplainerTicket () { + const RELATED_ISSUE_ID = 156; + return ( + here + ); +} + +function ContribLinks({contrib, username, repo, pushToFunctionQueue}) { + + const {commits_count__user, commits_count__percentage, commits_count__total} = getCommitCounts(contrib); + const contribType = getContribType(contrib); + + const isMaintainer = contribType === 'contrib_crown'; + + const CommitLink = ({children}) => ( + isMaintainer ? ( + children + ) : ( + {children} + ) + ); + + return ( +
+
+   + + {' '} + {username} wrote {numberOf(commits_count__user, 'commit')} + {' '} + ({Math.round(commits_count__percentage*100)}% of all {numberOf(commits_count__total, 'commit')}) +
+ { + !isMaintainer && repo && repo.pulls_authors && repo.pulls_authors.indexOf(username) !== -1 && ( + +   + {username}'s pull requests + + ) || null + } +
+ ); +} diff --git a/reframe/views/profile/rightpanel/contrib/Language.css b/reframe/views/profile/rightpanel/contrib/Language.css index c4b77184a..3ee767e09 100644 --- a/reframe/views/profile/rightpanel/contrib/Language.css +++ b/reframe/views/profile/rightpanel/contrib/Language.css @@ -7,7 +7,10 @@ border-radius: 50%; } -.language { +.repo-language { white-space: nowrap; display: inline-block; + padding-right: 1em; + font-size: 90%; + color: #586069; } diff --git a/reframe/views/profile/rightpanel/contrib/Language.js b/reframe/views/profile/rightpanel/contrib/Language.js index a6d432a52..7bf7ecc32 100644 --- a/reframe/views/profile/rightpanel/contrib/Language.js +++ b/reframe/views/profile/rightpanel/contrib/Language.js @@ -3,7 +3,7 @@ import React from 'react'; import './Language.css'; const Language = props => ( - + {props.name} diff --git a/reframe/views/profile/rightpanel/contrib/RepoDescrAndDetails.css b/reframe/views/profile/rightpanel/contrib/RepoDescrAndDetails.css deleted file mode 100644 index 8fcdc9949..000000000 --- a/reframe/views/profile/rightpanel/contrib/RepoDescrAndDetails.css +++ /dev/null @@ -1,51 +0,0 @@ -.icon { - vertical-align: middle !important; -} - -.title > span { - display: inline-block; - vertical-align: top; -} - -.repo-descr { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - max-width: calc(100% - - 50px - .5em /*avatar-small*/ - - 2em); /*dropdown icon*/ -} - -.emoji { - height: 1.3em; - width: auto; -} - -.add-a-tech, -.add-a-tech:hover { - background-color: #eee; - text-decoration: none; - display: inline-block; - width: 15px; - height: 15px; - text-align: center; - position: relative; - top: 3px; -} - -.add-a-tech-plus { - position: relative; - top: -3px; -} - -.contrib-details { - padding-right: 1em; - font-size: 90%; - color: #586069; -} - -.contrib-code-icon { - font-size: 150%; - width: 30px; - text-align: center; -} diff --git a/reframe/views/profile/rightpanel/contrib/RepoDescrAndDetails.js b/reframe/views/profile/rightpanel/contrib/RepoDescrAndDetails.js deleted file mode 100644 index d3ca3fea0..000000000 --- a/reframe/views/profile/rightpanel/contrib/RepoDescrAndDetails.js +++ /dev/null @@ -1,162 +0,0 @@ -import React from 'react'; -import {XmlEntities} from 'html-entities'; -import * as Autolinker from 'autolinker'; -import * as emoji from 'node-emoji'; -import * as Parser from 'html-react-parser'; - -import '../../../../browser/thirdparty/semantic-ui-2.3.2/accordion.min.css'; - -import Language from './Language'; -import ProgressBar from './ProgressBar'; -import AddSettings from '../../AddSettings'; -import './RepoDescrAndDetails.css'; -import {_, roundHalf} from '../../numbers'; -import {urls} from '../../../../ghuser'; - -class RepoDescrAndDetails extends React.Component { - constructor(props) { - super(props); - this.semanticAccordion = React.createRef(); - } - - componentDidMount() { - this.setupSemanticUi(); - } - - componentDidUpdate() { - this.setupSemanticUi(); - } - - setupSemanticUi() { - this.props.pushToFunctionQueue(1, () => $(this.semanticAccordion.current).accordion()); - } - - render() { - const humanReadablePercentage = val => { - const result = roundHalf(val); - if (result < 1) { - return '< 1'; - } - return `${result}`; - }; - - const languages = []; - if (this.props.languages) { - for (const language of Object.keys(this.props.languages)) { - languages.push(); - } - } - for (const tech of this.props.techs) { - languages.push(); - } - - return ( -
-
- - { - Parser(emoji.emojify( - Autolinker.link((new XmlEntities).encode(this.props.descr), { - className: 'external' - }), name => ( - // See https://developer.github.com/v3/emojis/ : - `` - ) - )) - } - - -
-
- { - Object.keys(languages).length > 0 && -
- {languages} - -
|| - '' - } - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
user's contribution: - - - // {humanReadablePercentage(this.props.contrib.percentage)} % of the project -
project popularity: - - - // {roundHalf(this.props.contrib.popularity)} / 5 - ( {this.props.strStars} ) -
project activity: - - - // {roundHalf(this.props.contrib.activity)} / 5 - ({this.props.strLastPushed}) -
project maturity: - - - // {roundHalf(this.props.contrib.maturity)} / 5 - ({this.props.strNumCommits}) -
contribution score:* - - - // {roundHalf(this.props.contrib.total_score)} / {this.props.contrib.max_total_score} - ← {this.props.contrib.total_score_human_formula} -
-
(* all contributions on this page are sorted according to this score)
- { - !this.props.userIsMaintainer && this.props.contrib.percentage && -
-   - user's commits -
|| '' - } - { - !this.props.userIsMaintainer && this.props.pulls_authors && this.props.pulls_authors.indexOf(this.props.username) !== -1 && - || '' - } -
-
- ); - } -} - -export default RepoDescrAndDetails; diff --git a/reframe/views/profile/rightpanel/contrib/badges/Badges.css b/reframe/views/profile/rightpanel/contrib/badges/Badges.css new file mode 100644 index 000000000..84b18e228 --- /dev/null +++ b/reframe/views/profile/rightpanel/contrib/badges/Badges.css @@ -0,0 +1,91 @@ +.icon-contrib-crown { + background-image: url('./crown.svg'); +} +.icon-contrib-bronze { + background-image: url('./medal_bronze.svg'); +} +.icon-contrib-silver { + background-image: url('./medal_silver.svg'); +} +.icon-contrib-gold { + background-image: url('./medal_gold.svg'); +} +.icon-repo-scale { + background-image: url('./biceps.png'); +} +.icon-contrib-crown, +.icon-contrib-bronze, +.icon-contrib-silver, +.icon-contrib-gold, +.icon-repo-scale { + display: inline-block; +} + +.contrib-type-icon, .icon-repo-scale { + width: 13px; + height: 13px; + background-size: contain; +} +.contrib-badge .contrib-type-icon { + margin-top: 4px; + margin-bottom: -1px; +} +.contrib-badge .icon-repo-scale { + margin-top: 3px; +} + + +.icon-repo-scale-micro { + opacity: 0.4; + filter: grayscale(100%); +} +.icon-repo-scale-small { + opacity: 0.6; + filter: grayscale(60%); +} +.icon-repo-scale-medium { + opacity: 0.8; + filter: grayscale(30%); +} + +.earned-stars-text { + font-size: 1em; +} +.contrib-range-title { + font-size: 1em; +} + +.earned-stars-icon-color { + color: #4b4; +} +.earned-stars-text-color { + color: #119f11; +} + +.contrib-badge { + display: inline-flex; + align-items: center; + text-align: center; + padding-left: 8px; + padding-right: 7px; +} +.badge-desc { + font-size: .8em; + margin-left: 5px; +} +.contrib-badge { + position: relative; +} +.badge-border { + position: absolute; + top: 0; + left: 0; + z-index: -1; + box-sizing: content-box; + width: 100%; + height: 85%; + border-radius: 5px; + + border: 1px solid #efefef; + background-color: #fbfbfb; +} diff --git a/reframe/views/profile/rightpanel/contrib/badges/Badges.js b/reframe/views/profile/rightpanel/contrib/badges/Badges.js new file mode 100644 index 000000000..9a28c7743 --- /dev/null +++ b/reframe/views/profile/rightpanel/contrib/badges/Badges.js @@ -0,0 +1,349 @@ +import React from 'react'; +import {bigNum, numberOf} from '../../../../utils/pretty-numbers'; +import './Badges.css'; +import {getCommitCounts, getRepoAvatar} from '../getContribInfo'; + +export {Badges, BadgesMini, BadgesMultiLine}; +export {getContribType}; +export {getTotalEarnedStars}; + +function Badges({contrib, username, style={}}) { + const badgeInfos = getInfoForBadges(contrib, username); + const badgeProps = {...contrib, ...badgeInfos, fixedWidth: true}; + + return ( +
+ + + {/* + + */} + +
+ ); +} + +function BadgesMultiLine({contrib, username}) { + const badgeInfos = getInfoForBadges(contrib, username); + const badgeProps = {...contrib, ...badgeInfos, inlineHint: true, showMoreInfo: true, style: {marginBottom: 5}}; + + return ( +
+ + + {/* + + */} + +
+ ); +} + +function BadgesMini({contrib, repo, username, style={}}) { + const {contribTypeIcon, contribTypeHint, repoScaleIcon, repoScaleHint, earnedStars, earnedStarsHint} = getInfoForBadges(contrib, username); + const repoAvatar = getRepoAvatar(repo); + const repoImage = ( + repoAvatar && ( + + ) || ( +
+ ) + ); + + const Star = () => ; + const starsView = ( + {bigNum(earnedStars)}} + hint={earnedStarsHint} + outerStyle={{textAlign: 'left', minWidth: 40}} + fixedWidth={true} + /> + ); + return ( +
+ {repoImage} + {contribTypeIcon} + {repoScaleIcon} + {starsView} +
+ ); +} + +function HintWrapper({hint, children}) { + return ( +
{children}
+ ); +} + +function RepoScale({repoScale, repoScaleIcon, repoScaleHint, ...props}) { + return ( + + ); +} + +function ContribRange({contribRange, ...props}) { + // desc={contribRange.precise.from+' -> '+contribRange.precise.to} + return ( + {contribRange.coarse}
} + width={160} + {...props} + /> + ); +} + +function EarnedStars({earnedStars, earnedStarsHint, stargazers_count, showMoreInfo, ...props}) { + const Star = () => ; + return ( +
} + desc={{bigNum(earnedStars)}{(showMoreInfo || earnedStars!==stargazers_count) && / {bigNum(stargazers_count)}}} + hint={earnedStarsHint} + width={105} + {...props} + /> + ); +} + +function ContribType({contribTypeIcon, contribTypeText, contribTypeHint, ...props}) { + return ( + + ); +} + +function Badge({head, desc, width, hint, fixedWidth, inlineHint, style={}}) { + const innerStyle = {display: 'inline-block', width: fixedWidth && width}; + const badge = ( +
+
+
{head}
+ {desc &&
{desc}
} +
+
+
+ ); + + if( ! inlineHint ) { + Object.assign(innerStyle, style); + return badge; + }; + + return ( +
+ {badge}  :  {hint} +
+ ); +} + +function BadgeMini({head, width, hint, innerStyle={}, outerStyle={}}) { + return ( +
+
+
{head}
+
+
+
+ ); + +} + +function getInfoForBadges(contrib, username) { + const {contribType, ...contribTypeAssets} = getContribTypeAssets(contrib, username); + + const contribRange = getContribRange(contrib); + + return { + contribType, + ...contribTypeAssets, + contribRange, + ...getEarnedStars(contrib, contribType, username), + ...getRepoScaleAssets(contrib), + }; +} +function getContribTypeAssets(contrib, username="user") { + const contribType = getContribType(contrib); + + const {iconClassName, text, hint} = getAssets(); + const contribTypeIcon =
; + const contribTypeText = text; + const contribTypeHint = hint; + + return {contribType, contribTypeText, contribTypeIcon, contribTypeHint}; + + function getAssets() { + const hintPrefix = username+' has '; + const hintSuffix = ' to this repo'; + const hint = ( + contribType==='contrib_crown' && ( + hintPrefix+'substantially contributed'+hintSuffix + ) || + contribType==='contrib_gold' && ( + hintPrefix+'contributed a lot of times'+hintSuffix + ) || + contribType==='contrib_silver' && ( + hintPrefix+'often contributed'+hintSuffix + ) || + contribType==='contrib_bronze' && ( + hintPrefix+'contributed one or a couple of times'+hintSuffix + ) + ); + + const contrib_type_name = contribType.slice('contrib_'.length); + + const text = ( + contribType==='contrib_crown' ? ( + 'maintainer' + ) : ( + contrib_type_name+' contrib' + ) + ); + + const iconClassName = 'icon-contrib-'+contrib_type_name; + + return { + iconClassName, + text, + hint, + }; + } +} +function getContribType(contrib) { + const { + commits_count__user: userCommitsCount, + commits_count__percentage: userCommitsPercentage, + commits_count__total: totalCommitsCount, + } = getCommitCounts(contrib); + + const THREADSHOLD_CROWN = 0.1; + const THRESHOLD_GOLD = 50; + const THRESHOLD_SILVER = 5; + + const contribType = ( + userCommitsCount > 1 && + (totalCommitsCount * THREADSHOLD_CROWN <= 1 || userCommitsPercentage >= THREADSHOLD_CROWN) && ( + 'contrib_crown' + ) || + userCommitsCount > THRESHOLD_GOLD && ( + 'contrib_gold' + ) || + userCommitsCount > THRESHOLD_SILVER && ( + 'contrib_silver' + ) || ( + 'contrib_bronze' + ) + ); + + return contribType; +} +function getContribRange(contrib) { + const {name} = contrib; + + const coarse = name.charCodeAt(0); + const repoScaleHint = 'this repo seems to be a '+repoScale+' project'; + return {repoScale, repoScaleIcon, repoScaleHint}; +} +function getRepoScale(totalCommitsCount) { + const THREADSHOLD_LARGE = 2000; + const THREADSHOLD_MEDIUM = 500; + const THREADSHOLD_SMALL = 50; + + const repoScale = ( + totalCommitsCount > THREADSHOLD_LARGE && 'large' || + totalCommitsCount > THREADSHOLD_MEDIUM && 'medium' || + totalCommitsCount > THREADSHOLD_SMALL && 'small' || + 'micro' + ); + + return repoScale; +} +function getEarnedStars(contrib, contribType, username) { + const assert_ = val => assert(val, 'computing earned stars'); + + const {stargazers_count: stars, percentage: userCommitsPercentage} = contrib; + + const isMaintainer = contribType==='contrib_crown'; + const isBronzeContributor = contribType==='contrib_bronze'; + const isSilverContributor = contribType==='contrib_silver'; + const isGoldContributor = contribType==='contrib_gold'; + + assert_(isMaintainer + isBronzeContributor + isSilverContributor + isGoldContributor === 1); + + const earnedStars_bronze = Math.min(10, Math.floor(0.5*stars)); + const earnedStars_silver = Math.max(earnedStars_bronze, Math.min(100, Math.ceil(0.1*stars))); + const earnedStars_gold = Math.max(earnedStars_silver, Math.ceil((userCommitsPercentage/100)*stars)); + + const earnedStars = ( + isMaintainer && stars || + isGoldContributor && earnedStars_gold || + isSilverContributor && earnedStars_silver || + isBronzeContributor && earnedStars_bronze + ); + + const earnedStarsHint = username+' earned '+numberOf(earnedStars, 'star')+' from a total of '+numberOf(stars, 'star'); + + assert_(earnedStars>=0 && (earnedStars|0)===earnedStars); + + return {earnedStars, earnedStarsHint}; +} + +function getTotalEarnedStars(contribs) { + let totalEarnedStars = 0; + for (const repo in contribs && contribs.repos) { + const contrib = contribs.repos[repo]; + const {earnedStars} = getInfoForBadges(contrib); + totalEarnedStars += earnedStars; + } + return totalEarnedStars; +} + +function assert(val, doingWhat) { + if( val ) { + return; + } + throw new Error('Internal error '+doingWhat); +} diff --git a/reframe/views/profile/rightpanel/contrib/badges/biceps.png b/reframe/views/profile/rightpanel/contrib/badges/biceps.png new file mode 100644 index 000000000..260c7e4b7 Binary files /dev/null and b/reframe/views/profile/rightpanel/contrib/badges/biceps.png differ diff --git a/reframe/views/profile/rightpanel/contrib/badges/crown.svg b/reframe/views/profile/rightpanel/contrib/badges/crown.svg new file mode 100644 index 000000000..18c10375b --- /dev/null +++ b/reframe/views/profile/rightpanel/contrib/badges/crown.svg @@ -0,0 +1,11 @@ + + + + background + + + + Layer 1 + + + \ No newline at end of file diff --git a/reframe/views/profile/rightpanel/contrib/badges/medal_bronze.svg b/reframe/views/profile/rightpanel/contrib/badges/medal_bronze.svg new file mode 100644 index 000000000..7cae92807 --- /dev/null +++ b/reframe/views/profile/rightpanel/contrib/badges/medal_bronze.svg @@ -0,0 +1,65 @@ + + + + medal_bronze + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/reframe/views/profile/rightpanel/contrib/badges/medal_gold.svg b/reframe/views/profile/rightpanel/contrib/badges/medal_gold.svg new file mode 100644 index 000000000..a74f3a54e --- /dev/null +++ b/reframe/views/profile/rightpanel/contrib/badges/medal_gold.svg @@ -0,0 +1,65 @@ + + + + medal_gold + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/reframe/views/profile/rightpanel/contrib/badges/medal_silver.svg b/reframe/views/profile/rightpanel/contrib/badges/medal_silver.svg new file mode 100644 index 000000000..ce183616e --- /dev/null +++ b/reframe/views/profile/rightpanel/contrib/badges/medal_silver.svg @@ -0,0 +1,65 @@ + + + + medal_silver + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/reframe/views/profile/rightpanel/contrib/getContribInfo.js b/reframe/views/profile/rightpanel/contrib/getContribInfo.js new file mode 100644 index 000000000..0b02b9950 --- /dev/null +++ b/reframe/views/profile/rightpanel/contrib/getContribInfo.js @@ -0,0 +1,16 @@ +export {getCommitCounts, getRepoAvatar}; + +function getCommitCounts(contrib) { + const {total_commits_count: commits_count__total} = contrib; + const commits_count__percentage = contrib.percentage/100; + const commits_count__user = Math.round(commits_count__percentage*commits_count__total); + return {commits_count__user, commits_count__percentage, commits_count__total}; +} + +function getRepoAvatar(repo) { + return ( + repo && repo.settings && repo.settings.avatar_url || + repo && repo.organization && repo.organization.avatar_url || + null + ); +} diff --git a/reframe/views/profile/rightpanel/contrib/getContribScore.js b/reframe/views/profile/rightpanel/contrib/getContribScore.js new file mode 100644 index 000000000..edce36375 --- /dev/null +++ b/reframe/views/profile/rightpanel/contrib/getContribScore.js @@ -0,0 +1,32 @@ +import {getCommitCounts} from './getContribInfo'; +export {getContribDisplayOrder}; +export {getContribScore}; + +function getContribDisplayOrder(contrib1, contrib2) { + return getContribScore(contrib2).contribScore - getContribScore(contrib1).contribScore; +} + +function getContribScore(contrib) { + const { + commits_count__user: userCommitsCount, + commits_count__percentage: userCommitsPercentage, + } = getCommitCounts(contrib); + const {stargazers_count: stars} = contrib; + + const starBoost = getStarBoost(stars); + const contribBoost = getContribBoost(userCommitsPercentage); + const contribScore = userCommitsCount*starBoost*contribBoost; + + return {contribScore, userCommitsCount, starBoost, contribBoost}; +} + +function getContribBoost(userCommitsPercentage) { + const contribBoost = 1 + ((1 - userCommitsPercentage) * 5); + return contribBoost; +} + +function getStarBoost(stars) { + const MAX_STARS = 100*1000; + const starBoost = 0.2 + (Math.log10(stars) / Math.log10(MAX_STARS) * 5); + return starBoost; +} diff --git a/reframe/views/utils/Accordion.css b/reframe/views/utils/Accordion.css new file mode 100644 index 000000000..2ce826992 --- /dev/null +++ b/reframe/views/utils/Accordion.css @@ -0,0 +1,45 @@ +/* CSS copied and adapted from https://github.com/stuartjnelson/badger-accordion */ +.badger-accordion__header-icon { + float: right; + position: absolute; + z-index: -1; + + right: 50%; + bottom: -10px; + + right: 20px; + bottom: 5px; +} +.badger-accordion__header-icon { + display: block; + height: 20px; + width: 20px; + margin-left: auto; + position: relative; + transition: all ease-in-out 0.2s; } + .badger-accordion__header-icon:after, .badger-accordion__header-icon:before { + background-color: #ddd; + content: ""; + height: 2px; + position: absolute; + top: 10px; + transition: all ease-in-out 0.13333s, background-color 0.7s; + width: 15px; } + .badger-accordion__header-icon:before { + left: 0; + transform: rotate(45deg) translate3d(8px, 14px, 0); + transform-origin: 100%; } + .badger-accordion__header-icon:after { + transform: rotate(-45deg) translate3d(-8px, 14px, 0); + right: 0; + transform-origin: 0%; } + + .active .badger-accordion__header-icon:before { + transform: rotate(45deg) translate3d(10px, 10px, 0); } + .active .badger-accordion__header-icon:after { + transform: rotate(-45deg) translate3d(-10px, 10px, 0); } + + .contrib-head:hover .badger-accordion__header-icon:after, + .contrib-head:hover .badger-accordion__header-icon:before { + background-color: #77f; + } diff --git a/reframe/views/utils/Accordion.js b/reframe/views/utils/Accordion.js new file mode 100644 index 000000000..15052ced4 --- /dev/null +++ b/reframe/views/utils/Accordion.js @@ -0,0 +1,69 @@ +import React from 'react'; +import '../../browser/thirdparty/semantic-ui-2.3.2/accordion.min.css'; +import './Accordion.css'; + +export {Accordion}; +export {AccordionHead} +export {AccordionBody}; +export {AccordionIcon}; +export {AccordionBadgerIcon}; +export {stopPropagationOnLinks} + +const AccordionBadgerIcon = () =>
; +const AccordionIcon = () => ; + +const AccordionHead = ({className, ...props}) => ( +
+); +const AccordionBody = ({className, ...props}) => ( +
+); + +class Accordion extends React.Component { + constructor(props) { + super(props); + this.semanticAccordion = React.createRef(); + } + + componentDidMount() { + this.setupSemanticUi(); + } + + componentDidUpdate() { + this.setupSemanticUi(); + } + + setupSemanticUi() { + this.props.pushToFunctionQueue(1, () => $(this.semanticAccordion.current).accordion()); + } + + render() { + const {className="", pushToFunctionQueue, ...props} = this.props; + return ( +
+ ); + } +} + +function stopPropagationOnLinks(domEl) { + if( ! domEl ) { + return; + } + const applyStop = el => el.onclick = ev => ev.stopPropagation(); + if( domEl.tagName.toLowerCase()==='a' ) { + applyStop(domEl); + } + const linkEls = Array.from(domEl.querySelectorAll('a')); + linkEls.forEach(applyStop); +} diff --git a/reframe/views/profile/rightpanel/contrib/ProgressBar.css b/reframe/views/utils/ProgressBar.css similarity index 100% rename from reframe/views/profile/rightpanel/contrib/ProgressBar.css rename to reframe/views/utils/ProgressBar.css diff --git a/reframe/views/profile/rightpanel/contrib/ProgressBar.js b/reframe/views/utils/ProgressBar.js similarity index 90% rename from reframe/views/profile/rightpanel/contrib/ProgressBar.js rename to reframe/views/utils/ProgressBar.js index b3354d6e9..59eea0407 100644 --- a/reframe/views/profile/rightpanel/contrib/ProgressBar.js +++ b/reframe/views/utils/ProgressBar.js @@ -1,6 +1,6 @@ import React from 'react'; -import '../../../../browser/thirdparty/semantic-ui-2.3.2/progress.min.css'; +import '../../browser/thirdparty/semantic-ui-2.3.2/progress.min.css'; import './ProgressBar.css'; diff --git a/reframe/views/utils/RichText.js b/reframe/views/utils/RichText.js new file mode 100644 index 000000000..51e67b522 --- /dev/null +++ b/reframe/views/utils/RichText.js @@ -0,0 +1,19 @@ +import {XmlEntities} from 'html-entities'; +import * as Autolinker from 'autolinker'; +import * as emoji from 'node-emoji'; +import * as Parser from 'html-react-parser'; + +export default RichText; + +function RichText(text) { + return ( + Parser(emoji.emojify( + Autolinker.link((new XmlEntities).encode(text), { + className: 'external' + }), name => ( + // See https://developer.github.com/v3/emojis/ : + `` + ) + )) + ); +} diff --git a/reframe/views/profile/numbers.js b/reframe/views/utils/pretty-numbers.js similarity index 66% rename from reframe/views/profile/numbers.js rename to reframe/views/utils/pretty-numbers.js index 3b7382f66..df693ae00 100644 --- a/reframe/views/profile/numbers.js +++ b/reframe/views/utils/pretty-numbers.js @@ -15,5 +15,15 @@ const bigNum = num => { export { roundHalf, - bigNum + bigNum, + numberOf, }; + +function numberOf(n, what, abbreviate) { + return ( + (abbreviate?bigNum(n):n)+ + ' '+ + what+ + (n===1 ? '' : 's') + ); +} diff --git a/reframe/views/utils/readme.md b/reframe/views/utils/readme.md new file mode 100644 index 000000000..04989ce4a --- /dev/null +++ b/reframe/views/utils/readme.md @@ -0,0 +1,3 @@ +This directory holds generic utilities to create views. + +Generic in the sense that they are ghuser.io agnostic.