+
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}
+
);
- };
+}
+
+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 &&
-
- }
+
);
- }
}
-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 &&
-
||
- ''
- }
-
-
-
- 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 &&
-
|| ''
- }
- {
- !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 (
+
+ );
+
+}
+
+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.