diff --git a/app/app/urls.py b/app/app/urls.py index f8103f2dd35..a7ffe6fec11 100644 --- a/app/app/urls.py +++ b/app/app/urls.py @@ -90,6 +90,7 @@ # api views url(r'^api/v0.1/profile/(.*)?/keywords', dashboard.views.profile_keywords, name='profile_keywords'), + url(r'^api/v0.1/profile/(?P.*)', dashboard.views.profile_details, name='profile_details'), url( r'^api/v0.1/social_contribution_email', dashboard.views.social_contribution_email, diff --git a/app/assets/v2/css/base.css b/app/assets/v2/css/base.css index 9c72a6f7542..193e3c9a57b 100644 --- a/app/assets/v2/css/base.css +++ b/app/assets/v2/css/base.css @@ -1588,6 +1588,10 @@ div.busyOverlay { max-width: 482px; } +.popover_card { + border-radius: 5px; + border: 1px solid #E5E5E5; +} /* Custom icons */ .g-icon { diff --git a/app/assets/v2/css/bounty.css b/app/assets/v2/css/bounty.css index fb0e5d7cf5b..0b0a4243b21 100644 --- a/app/assets/v2/css/bounty.css +++ b/app/assets/v2/css/bounty.css @@ -471,6 +471,30 @@ a.btn { margin-bottom: 2em; } +.earned { + color: #8E2ABE; + font-size: 14px; + line-height: 13px; + font-weight: 600; +} + +.username, .contributor-position, .in-progress, .current_status { + color: #0D0764; +} + +.specialty { + font-weight: 600; + font-size: 10px; + line-height: 11px; +} +.completed-bounties { + color: #05B66A; +} + +.abandoned-bounties, .removed-bounties { + color: #F5A623; +} + .code-snippet { background-color: #F2F6F9; padding: 1rem; diff --git a/app/assets/v2/css/user_popover.css b/app/assets/v2/css/user_popover.css new file mode 100644 index 00000000000..344825c1e1d --- /dev/null +++ b/app/assets/v2/css/user_popover.css @@ -0,0 +1,3 @@ +.popover-body { + padding: 0; +} diff --git a/app/assets/v2/js/user_popover.js b/app/assets/v2/js/user_popover.js new file mode 100644 index 00000000000..5ce9d054cd1 --- /dev/null +++ b/app/assets/v2/js/user_popover.js @@ -0,0 +1,140 @@ +let popoverData = []; + +const renderPopOverData = json => { + return ` +
+
+

${ + json.profile.handle +}

+ Specialty: ${json.profile.keywords + .slice(0, 3) + .toString()} + ${Object.keys(json.profile.organizations).map( + org => + `${org}` + )} +
+ ~ ${ + json.profile.total_earned +} ETH earned +
+
+ #${ + json.profile.position +} +

Gitcoin Contributor

+
+
+
+ ${ + json.statistics.work_completed +} +

Bounties Completed

+
+
+ ${ + json.statistics.work_in_progress +} +

Bounties In Progress

+
+
+ ${ + json.statistics.work_abandoned +} +

Bounties Abandoned

+
+
+ ${ + json.statistics.work_removed +} +

Bounties Removed

+
+
+
+
+ + `; +}; + +const openContributorPopOver = (contributor, element) => { + let contributorURL = `/api/v0.1/profile/${contributor}`; + + if (popoverData.filter(index => index[contributor]).length === 0) { + fetch(contributorURL, { method: 'GET' }) + .then(response => response.json()) + .then(response => { + element.popover({ + placement: 'auto', + trigger: 'hover', + template: + '', + content: renderPopOverData(response), + html: true + }); + $(element).popover('show'); + popoverData.push({ [contributor]: response }); + }) + .catch(err => { + return _alert({ message: err }, 'error'); + }); + } else { + element.popover({ + placement: 'auto', + trigger: 'hover', + template: + '', + content: renderPopOverData( + popoverData.filter(item => item[contributor])[0][contributor] + ), + html: true + }); + $(element).popover('show'); + } +}; + +const currentStatus = status => { + const activity_names = { + new_bounty: gettext('New Bounty'), + start_work: gettext('Work Started'), + stop_work: gettext('Work Stopped'), + work_submitted: gettext('Work Submitted'), + work_done: gettext('Work Done'), + worker_approved: gettext('Worker Approved'), + worker_rejected: gettext('Worker Rejected'), + worker_applied: gettext('Worker Applied'), + increased_bounty: gettext('Increased Funding'), + killed_bounty: gettext('Canceled Bounty'), + new_crowdfund: gettext('New Crowdfund Contribution'), + new_tip: gettext('New Tip'), + receive_tip: gettext('Tip Received'), + bounty_abandonment_escalation_to_mods: gettext( + 'Escalated for Abandonment of Bounty' + ), + bounty_abandonment_warning: gettext('Warned for Abandonment of Bounty'), + bounty_removed_slashed_by_staff: gettext( + 'Dinged and Removed from Bounty by Staff' + ), + bounty_removed_by_staff: gettext('Removed from Bounty by Staff'), + bounty_removed_by_funder: gettext('Removed from Bounty by Funder'), + bounty_changed: gettext('Bounty Details Changed'), + extend_expiration: gettext('Extended Bounty Expiration') + }; + + return activity_names[status] || 'Unknown activity '; +}; diff --git a/app/dashboard/models.py b/app/dashboard/models.py index 3495304728a..ae9576bdf97 100644 --- a/app/dashboard/models.py +++ b/app/dashboard/models.py @@ -2800,12 +2800,17 @@ def to_representation(self, instance): dict: The serialized Profile. """ + return { 'id': instance.id, 'handle': instance.handle, 'github_url': instance.github_url, 'avatar_url': instance.avatar_url, - 'url': instance.get_relative_url() + 'keywords': instance.keywords, + 'url': instance.get_relative_url(), + 'position': instance.get_contributor_leaderboard_index(), + 'organizations': instance.get_who_works_with(), + 'total_earned': instance.get_eth_sum() } diff --git a/app/dashboard/templates/bounty/details.html b/app/dashboard/templates/bounty/details.html index 08dc0f0b967..9ed6b129940 100644 --- a/app/dashboard/templates/bounty/details.html +++ b/app/dashboard/templates/bounty/details.html @@ -8,6 +8,7 @@ + @@ -238,7 +239,7 @@
{% trans "Funder" %}
[[if activity_type != 'unknown_event']]
-
+
@@ -448,13 +449,21 @@
{% trans "Funder" %}
+ + diff --git a/app/dashboard/views.py b/app/dashboard/views.py index f4fb29c00fe..1c00c0f4e77 100644 --- a/app/dashboard/views.py +++ b/app/dashboard/views.py @@ -1566,9 +1566,44 @@ def quickstart(request): return TemplateResponse(request, 'quickstart.html', {}) -def profile_keywords(request, handle): +def profile_details(request, handle): """Display profile keywords. + Args: + handle (str): The profile handle. + + """ + try: + profile = profile_helper(handle, True) + activity = Activity.objects.filter(profile=profile).order_by('-created_on').first() + count_work_completed = Activity.objects.filter(profile=profile, activity_type='work_done').count() + count_work_in_progress = Activity.objects.filter(profile=profile, activity_type='start_work').count() + count_work_abandoned = Activity.objects.filter(profile=profile, activity_type='stop_work').count() + count_work_removed = Activity.objects.filter(profile=profile, activity_type='bounty_removed_by_funder').count() + + except (ProfileNotFoundException, ProfileHiddenException): + raise Http404 + + response = { + 'profile': ProfileSerializer(profile).data, + 'recent_activity': { + 'activity_metadata': activity.metadata, + 'activity_type': activity.activity_type, + 'created': activity.created, + }, + 'statistics': { + 'work_completed': count_work_completed, + 'work_in_progress': count_work_in_progress, + 'work_abandoned': count_work_abandoned, + 'work_removed': count_work_removed + } + } + return JsonResponse(response, safe=False) + + +def profile_keywords(request, handle): + """Display profile details. + Args: handle (str): The profile handle.