Skip to content

Commit 07c35f6

Browse files
committed
feat: Add love button to GeoKrety
1 parent 2b05e86 commit 07c35f6

18 files changed

Lines changed: 505 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: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@
2929
{/if}
3030

3131
<div class="btn-group pull-right" role="group">
32+
{if $f3->get('SESSION.CURRENT_USER')}
33+
<button class="btn btn-xs toggle-love" data-gkid="{$geokret->gkid}" data-liked="{if $geokret->isLovedByCurrentUser()}1{else}0{/if}" title="{t}Vote for this GeoKret{/t}" style="{if $geokret->isLovedByCurrentUser()}color: #d9534f;{/if}">
34+
❤️ <span class="loves-count">{$geokret->loves_count|default:0}</span>
35+
</button>
36+
{/if}
3237
{if $f3->get('SESSION.CURRENT_USER') and $geokret->owner and $geokret->owner->email and isset($current_user) and $current_user->canSendMail()}
3338
<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}">
3439
{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>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
$('#modal').on('show.bs.modal', function(event) {
2+
let button = $(event.relatedTarget);
3+
let typeName = button.data('type');
4+
let id = button.data('id');
5+
6+
if (typeName === 'geokret-love') {
7+
modalLoad("{'geokret_love'|alias:'gkid=%ID%'}".replace('%ID%', id));
8+
} else if (typeName === 'geokret-unlove') {
9+
modalLoad("{'geokret_unlove'|alias:'gkid=%ID%'}".replace('%ID%', id));
10+
}
11+
});
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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+
// Find counter - either inside button or adjacent sibling
10+
const $counter = $btn.find('.loves-count').length > 0
11+
? $btn.find('.loves-count')
12+
: $btn.siblings('.loves-count');
13+
14+
const endpoint = isLiked
15+
? "{/literal}{'geokret_unlove'|alias:'@gkid='}{literal}".replace('@gkid=', gkid)
16+
: "{/literal}{'geokret_love'|alias:'@gkid='}{literal}".replace('@gkid=', gkid);
17+
18+
const csrfToken = "{/literal}{$f3->get('SESSION.csrf')}{literal}";
19+
20+
$.ajax({
21+
url: endpoint,
22+
type: 'POST',
23+
dataType: 'json',
24+
data: {
25+
csrf_token: csrfToken
26+
},
27+
headers: {
28+
'X-Requested-With': 'XMLHttpRequest'
29+
},
30+
success: function(response) {
31+
const newIsLiked = !isLiked;
32+
33+
// Update button state
34+
$btn.data('liked', newIsLiked ? 1 : 0);
35+
36+
if (newIsLiked) {
37+
$btn.css('color', '#d9534f');
38+
$btn.attr('title', "{/literal}{t}Remove your love for this GeoKret{/t}{literal}");
39+
} else {
40+
$btn.css('color', '');
41+
$btn.attr('title', "{/literal}{t}Love this GeoKret{/t}{literal}");
42+
}
43+
44+
// Update counter
45+
if (response.loves_count !== undefined) {
46+
$counter.text(response.loves_count);
47+
}
48+
49+
// Show feedback
50+
const message = newIsLiked ? "{/literal}{t}You have loved this GeoKret! ❤️{/t}{literal}" : "{/literal}{t}You have removed your love for this GeoKret.{/t}{literal}";
51+
if (typeof GK !== 'undefined' && GK.flashMessage) {
52+
GK.flashMessage(message, newIsLiked ? 'success' : 'info', 3000);
53+
}
54+
},
55+
error: function(xhr) {
56+
if (xhr.status === 401) {
57+
window.location.href = "{/literal}{'login'|alias}{literal}";
58+
} else {
59+
if (typeof GK !== 'undefined' && GK.flashMessage) {
60+
GK.flashMessage("{/literal}{t}Error toggling love{/t}{literal}", 'danger');
61+
}
62+
}
63+
}
64+
});
65+
});
66+
// {/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.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
namespace GeoKrety\Controller;
4+
5+
use GeoKrety\Model\GeokretLove as GeokretLoveModel;
6+
use GeoKrety\Traits\GeokretLoader;
7+
8+
class GeokretLove extends Base {
9+
use GeokretLoader;
10+
11+
public function post(\Base $f3) {
12+
$this->checkCsrf();
13+
14+
$love = new GeokretLoveModel();
15+
$love->load(['user = ? AND geokret = ?', $this->current_user->id, $this->geokret->id]);
16+
17+
if (!$love->dry()) {
18+
// Already loved, return current state
19+
echo json_encode([
20+
'success' => false,
21+
'message' => _('You have already loved this GeoKret.'),
22+
'loves_count' => $this->geokret->loves_count,
23+
]);
24+
exit;
25+
}
26+
27+
$love->geokret = $this->geokret->id;
28+
$love->user = $this->current_user;
29+
$love->save();
30+
31+
echo json_encode([
32+
'success' => true,
33+
'message' => _('You have loved this GeoKret! ❤️'),
34+
'loves_count' => $this->geokret->loves_count + 1,
35+
]);
36+
exit;
37+
}
38+
}

0 commit comments

Comments
 (0)