diff --git a/app/app/urls.py b/app/app/urls.py index d01c364e7f5..c387116562d 100644 --- a/app/app/urls.py +++ b/app/app/urls.py @@ -135,6 +135,7 @@ name='profile_job_opportunity' ), url(r'^api/v0.1/profile/(?P.*)', dashboard.views.profile_details, name='profile_details'), + url(r'^api/v0.1/user_card/(?P.*)', dashboard.views.user_card, name='user_card'), url(r'^api/v0.1/banners', dashboard.views.load_banners, name='load_banners'), url( r'^api/v0.1/get_suggested_contributors', diff --git a/app/assets/v2/css/base.css b/app/assets/v2/css/base.css index f578c8de564..ef59419fe2f 100644 --- a/app/assets/v2/css/base.css +++ b/app/assets/v2/css/base.css @@ -1516,6 +1516,21 @@ div.busyOverlay { padding: 1.4em 1.3em 1.3em; } +.popover-user-card { + background-color: #FFF; + padding: 0; + width: 100%; + max-width: 30em; + font-size: 12px; +} + +.user-card_donut-chart { + width: 56px; +} +.user-card_pie-chart { + width: 30px; +} + .big-popover { max-width: 482px; } diff --git a/app/assets/v2/js/activity.js b/app/assets/v2/js/activity.js index 93002720234..57164931a30 100644 --- a/app/assets/v2/js/activity.js +++ b/app/assets/v2/js/activity.js @@ -7,7 +7,7 @@ $(document).ready(function() { var linkify = function(new_text) { new_text = new_text.replace(/(?:^|\s)#([a-zA-Z\d-]+)/g, ' #$1'); - new_text = new_text.replace(/\B@([a-zA-Z0-9_-]*)/g, ' @$1'); + new_text = new_text.replace(/\B@([a-zA-Z0-9_-]*)/g, ' @$1'); return new_text; }; // inserts links into the text where there are URLS detected @@ -58,9 +58,9 @@ $(document).ready(function() { const $target = $(this).parents('.activity_detail_content'); const html = `

- Full Screen | - Pop Out | - Open in New Tab | + Full Screen | + Pop Out | + Open in New Tab | Leave Video Call

`; @@ -669,7 +669,7 @@ $(document).ready(function() { ${show_more_box}
@@ -680,9 +680,9 @@ $(document).ready(function() { - + ${comment['name']} - + @${comment['profile_handle']} ${comment['match_this_round'] ? ` @@ -950,7 +950,7 @@ function throttle(fn, wait) { } }; } - + window.addEventListener('scroll', throttle(function() { var offset = 800; diff --git a/app/assets/v2/js/grants/fund.js b/app/assets/v2/js/grants/fund.js index 64bb8c5e0e1..2fe84f40785 100644 --- a/app/assets/v2/js/grants/fund.js +++ b/app/assets/v2/js/grants/fund.js @@ -360,8 +360,8 @@ $(document).ready(function() { let realTokenAmount = Number(data.amount_per_period * Math.pow(10, decimals)); let realApproval; - const approve_buffer = 100000; + if (data.contract_version == 1 || data.num_periods == 1) { realApproval = Number(((grant_amount + gitcoin_grant_amount) * data.num_periods * Math.pow(10, decimals)) + approve_buffer); diff --git a/app/assets/v2/js/pages/join_tribe.js b/app/assets/v2/js/pages/join_tribe.js index 8623d7e3709..9d1cc4c7ea3 100644 --- a/app/assets/v2/js/pages/join_tribe.js +++ b/app/assets/v2/js/pages/join_tribe.js @@ -58,6 +58,23 @@ const joinTribeDirect = (elem) => { }; +const followRequest = (handle, elem, cb, cbError) => { + if (!document.contxt.github_handle) { + _alert('Please login first.', 'error'); + return; + } + + const url = `/tribe/${handle}/join/`; + const sendJoin = fetchData (url, 'POST', {}, {'X-CSRFToken': $("input[name='csrfmiddlewaretoken']").val()}); + + $.when(sendJoin).then(function(response) { + return cb(handle, elem, response); + }).fail(function(error) { + return cbError(error); + }); +}; + + const tribeLeader = () => { $('[data-tribeleader]').each(function(index, elem) { diff --git a/app/assets/v2/js/user_card.js b/app/assets/v2/js/user_card.js new file mode 100644 index 00000000000..db67f397dc6 --- /dev/null +++ b/app/assets/v2/js/user_card.js @@ -0,0 +1,293 @@ +$('body').on('mouseover', '[data-usercard]', function(e) { + openContributorPopOver($(this).data('usercard'), $(this)); +}); + +$('body').on('show.bs.popover', '[data-usercard]', function() { + $('body [data-usercard]').not(this).popover('hide'); +}); + +let popoverData = []; +let controller = null; + +const renderPopOverData = function(data) { + const unique_orgs = data.profile.orgs ? Array.from(new Set(data.profile.orgs)) : []; + let orgs = unique_orgs && unique_orgs.map((_organization, index) => { + if (index < 5) { + return ` + ${_organization} + `; + } else if (index < 6) { + return `+${data.orgs.length - 5}`; + } + }).join(' '); + + function percentCalc(value, total) { + let result = value * 100 / total; + + return isNaN(result) ? 0 : result; + } + + let dashoffset = 25; + + function calcDashoffset(thispercentage) { + let oldOffset = dashoffset; + + accumulate += thispercentage; + dashoffset = (100 - accumulate) + 25; + return oldOffset; + } + + function objSetup(sent, received, total, color, colorlight, stringsOverwrite) { + let prodTotal = sent + received; + + return { + 'percent': percentCalc(prodTotal, total), + 'percentsent': percentCalc(sent, prodTotal), + 'amountsent': sent, + 'percentreceived': percentCalc(received, prodTotal), + 'amountreceived': received, + 'color': color, + 'colorlight': colorlight, + 'dashoffset': calcDashoffset(percentCalc(prodTotal, total)), + 'strings': stringsOverwrite + }; + } + + let tips_total = data.profile_dict.total_tips_sent + data.profile_dict.total_tips_received; + let bounties_total = data.profile_dict.funded_bounties_count + data.profile_dict.count_bounties_completed; + let grants_total = data.profile_dict.total_grant_created + data.profile_dict.total_grant_contributions; + let total = tips_total + bounties_total + grants_total; + let accumulate = 0; + let tips_total_percent = objSetup( + data.profile_dict.total_tips_sent, + data.profile_dict.total_tips_received, + total, + '#89CD69', + '#BFE1AF', + {'type': 'Tips', 'sent': 'Sent', 'received': 'Received'} + ); + let bounties_total_percent = objSetup( + data.profile_dict.funded_bounties_count, + data.profile_dict.count_bounties_completed, + total, + '#8E98FF', + '#ADB4FF', + {'type': 'Bounties', 'sent': 'Created', 'received': 'Worked'} + ); + let grants_total_percent = objSetup( + data.profile_dict.total_grant_created, + data.profile_dict.total_grant_contributions, + total, + '#FF83FA', + '#FFB2FC', + {'type': 'Grants', 'sent': 'Fund', 'received': 'Contrib'} + ); + + let mount_graph = [ tips_total_percent, bounties_total_percent, grants_total_percent ]; + let graphs = mount_graph.map((graph) => { + return ``; + }).join(' '); + + const renderAvatarData = function() { + return ` + + + ${graphs} + + + + + + + + + + `; + }; + + const renderPie = function(dataGraph) { + return ` +
+ ${dataGraph.strings.type} +
+ + + + + +
+
+ ${dataGraph.amountsent} + ${dataGraph.strings.sent} +
+
+ ${dataGraph.amountreceived} + ${dataGraph.strings.received} +
+
+
+
+ `; + }; + + const followBtn = function(data) { + if (data.is_following) { + return ``; + } + return ``; + }; + + return ` +
+
+
+ ${renderAvatarData()} +
+ ${orgs.length ? orgs : ''} +
+
+
+ ${data.is_authenticated && data.profile.handle !== document.contxt.github_handle ? followBtn(data) : ''} +
+
+

${data.profile.data.name || data.profile.handle}

+ @${data.profile.handle} + +
+ + + + + + + + + + + + + + | + + + +
+ +
${data.profile.keywords.map( + (keyword, index) => { + if (index < 5) { + return `${keyword}`; + } else if (index < 6) { + return `+${data.profile.keywords.length - 5}`; + } + }).join(' ')}
+
+ + Joined + + ${data.profile_dict.scoreboard_position_funder ? `#${data.profile_dict.scoreboard_position_funder} Funder` : '' } + ${data.profile_dict.scoreboard_position_contributor ? `#${data.profile_dict.scoreboard_position_contributor} Contributor` : '' } +
+ +
+ ${mount_graph.map((graph)=> renderPie(graph)).join(' ')} +
+ +
+ ${data.profile.followers} Followers + ${data.profile.following} Following +
+
+ `; +}; + +const cb = (handle, elem, response) => { + $(elem).attr('disabled', false); + popoverData.filter(item => item[handle])[0][handle].is_following = response.is_member; + response.is_member ? $(elem).html('Unfollow ') : $(elem).html('Follow '); +}; + +const addFollowAction = () => { + $('[data-follow]').each(function(index, elem) { + $(elem).on('click', function(e) { + $(elem).attr('disabled', true); + + const handle = $(elem).data('follow'); + const error = () => { + $(elem).attr('disabled', false); + }; + + followRequest(handle, elem, cb, error); + }); + + }); +}; + +function openContributorPopOver(contributor, element) { + + const contributorURL = `/api/v0.1/user_card/${contributor}`; + + if (popoverData.filter(index => index[contributor]).length === 0) { + if (controller) { + controller.abort(); + } + controller = new AbortController(); + const signal = controller.signal; + + userRequest = fetch(contributorURL, { method: 'GET', signal }) + .then(response => response.json()) + .then(response => { + popoverData.push({ [contributor]: response }); + controller = null; + setupPopover(element, response); + }) + .catch(err => { + return console.warn({ message: err }); + }); + } else { + setupPopover(element, popoverData.filter(item => item[contributor])[0][contributor]); + } +} + +function setupPopover(element, data) { + element.popover({ + sanitizeFn: function(content) { + return DOMPurify.sanitize(content); + }, + placement: 'auto', + // container: element, + trigger: 'manual', + delay: { 'show': 200, 'hide': 500 }, + template: ` + `, + content: function() { + return renderPopOverData(data); + }, + html: true + }).on('mouseenter', function() { + var _this = this; + + $(this).popover('show'); + $('.popover-user-card').on('mouseleave', function() { + $(_this).popover('hide'); + }); + }).on('mouseleave', function() { + var _this = this; + + setTimeout(function() { + if (!$('.popover-user-card:hover').length) { + $(_this).popover('hide'); + } + }, 100); + }); + $(element).popover('show'); + + addFollowAction(); + +} diff --git a/app/dashboard/views.py b/app/dashboard/views.py index fae6f0c0706..ed2c2f862d0 100644 --- a/app/dashboard/views.py +++ b/app/dashboard/views.py @@ -2107,6 +2107,50 @@ def profile_details(request, handle): return JsonResponse(response, safe=False) +def user_card(request, handle): + """Display profile keywords. + + Args: + handle (str): The profile handle. + + """ + try: + profile = profile_helper(handle, True) + except (ProfileNotFoundException, ProfileHiddenException): + raise Http404 + + if not settings.DEBUG: + network = 'mainnet' + else: + network = 'rinkeby' + + if request.user.is_authenticated: + is_following = True if TribeMember.objects.filter(profile=request.user.profile, org=profile).count() else False + else: + is_following = False + + profile_dict = profile.as_dict + followers = TribeMember.objects.filter(org=profile).count() + following = TribeMember.objects.filter(profile=profile).count() + response = { + 'is_authenticated': request.user.is_authenticated, + 'is_following': is_following, + 'profile' : { + 'avatar_url': profile.avatar_url, + 'handle': profile.handle, + 'orgs' : profile.organizations, + 'created_on' : profile.created_on, + 'keywords' : profile.keywords, + 'data': profile.data, + 'followers':followers, + 'following':following, + }, + 'profile_dict':profile_dict + } + + return JsonResponse(response, safe=False) + + def profile_keywords(request, handle): """Display profile details. diff --git a/app/retail/templates/shared/activity.html b/app/retail/templates/shared/activity.html index d33748f2bd1..3a4bf504a95 100644 --- a/app/retail/templates/shared/activity.html +++ b/app/retail/templates/shared/activity.html @@ -64,7 +64,7 @@ {% include 'profiles/presence_indicator.html' with last_chat_status=row.profile.last_chat_status chat_id=row.profile.chat_id handle=row.profile.handle additionalclasses="mini" show_even_if_offline=1 %} {% firstof row.profile.data.name or row.profile.handle %} - @{{ row.profile.handle }} + @{{ row.profile.handle }} {% if row.hackathonevent %} @@ -89,7 +89,7 @@
{% if row.activity_type == 'new_tip' %} {% trans "tipped" %} - + @{{ row.metadata.to_username }} {% elif row.activity_type == 'mini_clr_payout' %} diff --git a/app/townsquare/templates/townsquare/index.html b/app/townsquare/templates/townsquare/index.html index 6b7aa35dfd6..7ec5816ce7d 100644 --- a/app/townsquare/templates/townsquare/index.html +++ b/app/townsquare/templates/townsquare/index.html @@ -186,5 +186,6 @@ +