Skip to content

Commit 061d1a6

Browse files
committed
feat: Add love button to GeoKrety
1 parent 2b05e86 commit 061d1a6

18 files changed

Lines changed: 539 additions & 2 deletions

website/app-templates/smarty/blocks/geokret/actions.tpl

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,32 @@
3232
</div>
3333
{/function}
3434

35+
{function love}
36+
<div class="col-md-4">
37+
<button class="btn btn-link toggle-love" data-gkid="{$geokret->gkid}" data-liked="0" title="{t}Love this GeoKret{/t}">
38+
❤️ {t}Love{/t}
39+
</button>
40+
<span class="badge loves-count">{$geokret->loves_count|default:0}</span>
41+
</div>
42+
{/function}
43+
44+
{function unlove}
45+
<div class="col-md-4">
46+
<button class="btn btn-link toggle-love" data-gkid="{$geokret->gkid}" data-liked="1" title="{t}Remove your love for this GeoKret{/t}" style="color: #d9534f;">
47+
❤️ {t}Unlove{/t}
48+
</button>
49+
<span class="badge loves-count">{$geokret->loves_count|default:0}</span>
50+
</div>
51+
{/function}
52+
53+
{function lovecount}
54+
<div class="col-md-4">
55+
❤️
56+
<span title="{t}Hearts received{/t}">{t}Loves{/t}</span>
57+
<span class="badge">{$geokret->loves_count|default:0}</span>
58+
</div>
59+
{/function}
60+
3561
{function log}
3662
<div class="col-md-4">{fa icon="pencil"} <a id="geokretDetailsLogThisGeokretLink" href="{'move_create'|alias}?tracking_code={$geokret->tracking_code}">{t}Log this GeoKret{/t}</a></div>
3763
{/function}
@@ -97,7 +123,7 @@
97123
</div>
98124
<div class="row">
99125
{statistics}
100-
{empty}
126+
{lovecount}
101127
{transfer}
102128
</div>
103129
<div class="row">
@@ -116,11 +142,19 @@
116142
{adopt}
117143
{label}
118144
</div>
145+
<div class="row">
146+
{if $geokret->isLovedByCurrentUser()}{unlove}{else}{love}{/if}
147+
{empty}
148+
{empty}
149+
</div>
119150
{/if}
120151
{else}
121152
<div class="row">
122153
{statistics}
123154
</div>
155+
<div class="row">
156+
{lovecount}
157+
</div>
124158
{/if}
125159
</div>
126160
</div>

website/app-templates/smarty/blocks/geokret/details.tpl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@
2929
{/if}
3030

3131
<div class="btn-group pull-right" role="group">
32+
{if $f3->get('SESSION.CURRENT_USER') && !$geokret->isOwner()}
33+
{assign var="is_loved" value=$geokret->isLovedByCurrentUser()}
34+
<button class="btn btn-xs toggle-love" data-gkid="{$geokret->gkid}" data-liked="{if $is_loved}1{else}0{/if}" title="{t}Vote for this GeoKret{/t}" style="{if $is_loved}color: #d9534f;{/if}">
35+
❤️ <span class="loves-count">{$geokret->loves_count|default:0}</span>
36+
</button>
37+
{/if}
3238
{if $f3->get('SESSION.CURRENT_USER') and $geokret->owner and $geokret->owner->email and isset($current_user) and $current_user->canSendMail()}
3339
<button id="userContactButton" class="btn btn-primary btn-xs" title="{t user=$geokret->owner->username}Send a message to %1{/t}" data-toggle="modal" data-target="#modal" data-type="user-contact-by-geokret" data-id="{$geokret->gkid}">
3440
{fa icon="envelope"}

website/app-templates/smarty/elements/geokrety_as_list_user_owned_geokrety.tpl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@
3131
<td class="text-right">
3232
{$geokret->caches_count}
3333
</td>
34+
<td class="text-right">
35+
❤️ {$geokret->loves_count|default:0}
36+
</td>
3437
<td class="text-right">
3538
{if $geokret->isHolder()}
3639
<a class="btn btn-default btn-xs" href="{'move_create'|alias}?tracking_code={$geokret->tracking_code}" title="{t}Move this GeoKret{/t}">🛩️</a>

website/app-templates/smarty/help-pages/en/help.html

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,12 @@ <h3><i class="fa fa-cogs"></i> Advanced Features</h3>
112112
QR Code
113113
</a>
114114
</div>
115+
<div class="help-item">
116+
<a href="#loves">
117+
<i class="fa fa-heart"></i>
118+
Loving a GeoKret
119+
</a>
120+
</div>
115121
<div class="help-item">
116122
<a href="#othertrackable">
117123
<i class="fa fa-link"></i>
@@ -1073,6 +1079,26 @@ <h3>How to decode?</h3>
10731079
</div>
10741080
</div>
10751081

1082+
<div class="panel panel-default">
1083+
<div class="panel-heading">
1084+
<a class="anchor" id="loves"></a>
1085+
Loving a GeoKret ❤️
1086+
</div>
1087+
<div class="panel-body">
1088+
<p>
1089+
Registered users can express appreciation for a GeoKret by clicking the heart button (❤️) shown on the
1090+
GeoKret details page. This works as a simple "like" system — one love per user per GeoKret.
1091+
</p>
1092+
<ul>
1093+
<li>The heart button is visible in the top-right corner of the GeoKret panel (for logged-in users who do not own the GeoKret).</li>
1094+
<li>Clicking the button <strong>toggles</strong> your love: click once to love it, click again to remove your love.</li>
1095+
<li>The current love count is displayed next to the heart icon.</li>
1096+
<li>GeoKret owners can see the love count on their GeoKret, but cannot love their own GeoKret.</li>
1097+
<li>The most loved GeoKrety are shown on the <a href="{'statistics_site'|alias}">statistics page</a>.</li>
1098+
</ul>
1099+
</div>
1100+
</div>
1101+
10761102
<div class="panel panel-default">
10771103
<div class="panel-heading">
10781104
<a class="anchor" id="othertrackable"></a>
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// {literal}
2+
$(document).on("click", ".toggle-love", function(e) {
3+
e.preventDefault();
4+
5+
const $btn = $(this);
6+
const gkid = $btn.data("gkid");
7+
const isLiked = parseInt($btn.data("liked")) === 1;
8+
9+
const endpoint = isLiked
10+
? "{/literal}{'geokret_unlove'|alias:'@gkid='}{literal}".replace("@gkid=", gkid)
11+
: "{/literal}{'geokret_love'|alias:'@gkid='}{literal}".replace("@gkid=", gkid);
12+
13+
const csrfToken = "{/literal}{$f3->get('SESSION.csrf')}{literal}";
14+
15+
$.ajax({
16+
url: endpoint,
17+
type: "POST",
18+
dataType: "json",
19+
data: {
20+
csrf_token: csrfToken
21+
},
22+
headers: {
23+
"X-Requested-With": "XMLHttpRequest"
24+
},
25+
success(response) {
26+
const newIsLiked = !isLiked;
27+
28+
// Update all buttons for this GeoKret (panel heading + actions panel may both exist)
29+
function updateBtn($b) {
30+
$b.data("liked", newIsLiked ? 1 : 0);
31+
if (newIsLiked) {
32+
$b.css("color", "#d9534f");
33+
$b.attr("title", "{/literal}{t}Remove your love for this GeoKret{/t}{literal}");
34+
} else {
35+
$b.css("color", "");
36+
$b.attr("title", "{/literal}{t}Love this GeoKret{/t}{literal}");
37+
}
38+
if (response.loves_count !== undefined) {
39+
var $c = $b.find(".loves-count").length > 0
40+
? $b.find(".loves-count")
41+
: $b.siblings(".loves-count");
42+
$c.text(response.loves_count);
43+
}
44+
}
45+
46+
$(".toggle-love[data-gkid=\"" + gkid + "\"]").each(function() {
47+
updateBtn($(this));
48+
});
49+
50+
// Show feedback
51+
const message = newIsLiked ? "{/literal}{t}You have loved this GeoKret! ❤️{/t}{literal}" : "{/literal}{t}You have removed your love for this GeoKret.{/t}{literal}";
52+
if (typeof GK !== "undefined" && GK.flashMessage) {
53+
GK.flashMessage(message, newIsLiked ? "success" : "info", 3000);
54+
}
55+
},
56+
error(xhr) {
57+
if (xhr.status === 401) {
58+
window.location.href = "{/literal}{'login'|alias}{literal}";
59+
} else {
60+
if (typeof GK !== "undefined" && GK.flashMessage) {
61+
GK.flashMessage("{/literal}{t}Error toggling love{/t}{literal}", "danger");
62+
}
63+
}
64+
}
65+
});
66+
});
67+
// {/literal}

website/app-templates/smarty/pages/geokret_details.tpl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ $("#mapid").data({ map: map });
4646
{include 'js/dialogs/dialog_picture_actions.tpl.js'}
4747
{include 'js/dialogs/dialog_geokret_mark_as_archived.tpl.js'}
4848
{include 'js/dialogs/dialog_geokret_watch.tpl.js'}
49+
{include 'js/geokrety/geokret_loves_toggle.tpl.js'}
4950

5051
{if $geokret->isOwner()}
5152
{include 'js/geokrety/geokret_avatar_upload.tpl.js'}

website/app-templates/smarty/pages/statistics_site.tpl

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,77 @@
313313
</div>
314314
</section>
315315

316+
<!-- Most Loved GeoKrety -->
317+
<section class="statistics-section">
318+
<h2>❤️ {t}Most Loved GeoKrety{/t}</h2>
319+
<p class="help-block">{t}The top 10 GeoKrety that received the most love from the community.{/t}</p>
320+
<div class="row">
321+
<div class="col-md-12">
322+
<div class="panel panel-default">
323+
<div class="panel-heading">
324+
<h3 class="panel-title">❤️ {t}Top 10 Most Loved GeoKrety{/t}</h3>
325+
</div>
326+
<div class="panel-body">
327+
{if $top_loved_geokrety}
328+
<div style="max-height: 600px; overflow-y: auto;">
329+
<table class="table table-striped table-hover">
330+
<thead>
331+
<tr>
332+
<th class="text-right">#</th>
333+
<th>{t}GeoKret{/t}</th>
334+
<th>{t}Owner{/t}</th>
335+
<th class="text-right">❤️ {t}Loves{/t}</th>
336+
<th class="text-center">{t}Trend{/t}</th>
337+
</tr>
338+
</thead>
339+
<tbody>
340+
{foreach from=$top_loved_geokrety item=gk name=loves_rank}
341+
<tr>
342+
<td class="text-right">{$smarty.foreach.loves_rank.iteration}</td>
343+
<td>
344+
<a href="{'geokret_details'|alias:sprintf('@gkid=%s', $gk.gkid_formatted)}">
345+
{$gk.gkid_formatted} - {$gk.name|escape}
346+
</a>
347+
</td>
348+
<td>
349+
{if $gk.owner_username}
350+
<a href="{'user_details'|alias:sprintf('@userid=%d', $gk.owner)}">{$gk.owner_username|escape}</a>
351+
{else}
352+
<em>{t}Abandoned{/t}</em>
353+
{/if}
354+
</td>
355+
<td class="text-right"><strong>{$gk.loves_count}</strong></td>
356+
<td class="text-center loves-trend-col" data-gkid="{$gk.id}"><span class="text-muted">-</span></td>
357+
</tr>
358+
{/foreach}
359+
</tbody>
360+
</table>
361+
</div>
362+
{else}
363+
<p class="text-muted text-center"><em>{t}No loved GeoKrety yet. Be the first to show some love!{/t}</em></p>
364+
{/if}
365+
</div>
366+
</div>
367+
</div>
368+
</div>
369+
<div class="row" style="margin-top: 20px;">
370+
<div class="col-md-12">
371+
<div class="panel panel-default">
372+
<div class="panel-heading">
373+
<h3 class="panel-title">❤️ {t}Love Trend{/t}</h3>
374+
</div>
375+
<div class="panel-body">
376+
<p class="help-block">{t}Monthly love activity on the platform.{/t}</p>
377+
<div id="loves-chart-container" style="width: 100%; height: 350px;">
378+
<svg id="loves-chart" style="width: 100%; height: 100%;"></svg>
379+
</div>
380+
<div id="loves-cache-info" class="text-muted small text-center" style="margin-top: 10px;"></div>
381+
</div>
382+
</div>
383+
</div>
384+
</div>
385+
</section>
386+
316387
<!-- Media & Engagement -->
317388
<section class="statistics-section">
318389
<h2>{t}Media & Engagement{/t}</h2>
@@ -819,7 +890,14 @@ $(document).ready(function() {
819890
formatCacheDuration: formatCacheDuration
820891
});
821892

822-
// Time series mode switcher
893+
initTimeSeriesChart({
894+
anchor: "#loves-chart",
895+
dataUrl: "{/literal}{'api_v1_stats_loves_registrations'|alias}{literal}",
896+
title: "{/literal}{t}Love Votes{/t}{literal}",
897+
color: "#d9534f",
898+
cacheInfoElement: "#loves-cache-info",
899+
formatCacheDuration: formatCacheDuration
900+
});
823901
$(document).on("click", ".time-series-mode button", function() {
824902
const $button = $(this);
825903
const mode = $button.data("mode");
@@ -884,6 +962,30 @@ $(document).ready(function() {
884962
}
885963
});
886964

965+
// Load love trends for Most Loved GeoKrety table
966+
$.ajax({
967+
url: "{/literal}{'api_v1_stats_loves_registrations'|alias}{literal}",
968+
method: "GET",
969+
dataType: "json",
970+
success: function(response) {
971+
const data = response.data || response;
972+
if (data && data.length > 0) {
973+
// Build a map of love counts by month
974+
const lovesTrend = data.map(item => item.count);
975+
976+
// Populate all trend cells with the same sparkline
977+
$(".loves-trend-col").each(function() {
978+
const $cell = $(this);
979+
const sparkline = createSparkline(lovesTrend, 80, 25, "loves");
980+
$cell.html(sparkline);
981+
});
982+
}
983+
},
984+
error: function() {
985+
// Silently fail - trend is optional
986+
}
987+
});
988+
887989
// Year filter state
888990
let currentFilterYear = "all";
889991
const currentYear = new Date().getFullYear();

website/app-templates/smarty/pages/user_owned_geokrety.tpl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
<th class="text-center">{t}Last log{/t}</th>
2525
<th class="text-right">📏 {t}Distance{/t}</th>
2626
<th class="text-right"><img src="{GK_CDN_IMAGES_URL}/log-icons/2caches.png" title="{t}Caches visited count{/t}" /></th>
27+
<th class="text-right" title="{t}Loves count{/t}">❤️</th>
2728
<th class="text-center" title="{t}Actions{/t}">🔧</th>
2829
</tr>
2930
</thead>
@@ -60,6 +61,7 @@ $('#userOwnedGeoKretyTable').dataTable({
6061
{ "searchable": false, "name": "last_log" },
6162
{ "searchable": false, "name": "distance" },
6263
{ "searchable": false, "name": "caches_count" },
64+
{ "searchable": false, "name": "loves_count" },
6365
{ "searchable": false, "orderable": false }
6466
],
6567
});

website/app/GeoKrety/Controller/API/v1/Statistics.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,42 @@ public function geokrety_registrations() {
144144
echo $result[0]['response'] ?? '{"data":[],"ttl":0}';
145145
}
146146

147+
/**
148+
* Get loves (votes) over time (monthly aggregation).
149+
*/
150+
public function loves_registrations() {
151+
$db = \Base::instance()->get('DB');
152+
$ttl = GK_SITE_CACHE_TTL_STATISTICS_REGISTRATIONS;
153+
154+
$sql = <<<SQL
155+
SELECT json_build_object(
156+
'data', COALESCE(json_agg(
157+
json_build_object(
158+
'date', date,
159+
'count', count,
160+
'cumulative', cumulative
161+
)
162+
ORDER BY month
163+
), '[]'::json),
164+
'ttl', {$ttl}
165+
)::text AS response
166+
FROM (
167+
SELECT
168+
DATE_TRUNC('month', created_on_datetime) AS month,
169+
TO_CHAR(DATE_TRUNC('month', created_on_datetime), 'YYYY-MM-DD') AS date,
170+
COUNT(*) AS count,
171+
SUM(COUNT(*)) OVER (ORDER BY DATE_TRUNC('month', created_on_datetime)) AS cumulative
172+
FROM gk_loves
173+
WHERE created_on_datetime IS NOT NULL
174+
GROUP BY DATE_TRUNC('month', created_on_datetime)
175+
) stats
176+
SQL;
177+
178+
$result = $db->exec($sql, null, $ttl);
179+
180+
echo $result[0]['response'] ?? '{"data":[],"ttl":0}';
181+
}
182+
147183
/**
148184
* Get activity snapshot for KPI tiles.
149185
* Optional query parameter: min_moves (default 10) for active countries.

0 commit comments

Comments
 (0)