Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
0887db6
Working on leaderboards. some refactoring. Now highlighting the curre…
xXPinkmagicXx Mar 19, 2026
8529462
Highligting work for current user.
xXPinkmagicXx Mar 19, 2026
f9ff758
Now the frontend works with half months
xXPinkmagicXx Mar 19, 2026
8bcace0
Now with the curent period show above the table
xXPinkmagicXx Mar 19, 2026
1c31c8d
Added the exercises done leaderboard
xXPinkmagicXx Mar 19, 2026
e343485
Working on the leaderboards
xXPinkmagicXx Mar 19, 2026
bcbd408
Merge branch 'gamification' into feature/leaderboards
gabortodor Mar 22, 2026
f838656
Started generalizing leaderboards
gabortodor Mar 22, 2026
352d837
Merge branch 'gamification' into feature/leaderboards
gabortodor Mar 22, 2026
51a9d45
Worked on friends + leaderboards
gabortodor Mar 22, 2026
628f738
Worked on leaderboard design
gabortodor Mar 23, 2026
cf77318
Worked on leaderboard design
gabortodor Mar 23, 2026
3d2b7cc
Improved UI, dark mode support, and other refactors
klnyzzz33 Mar 25, 2026
f8507fa
Continued visual refactoring & fixed minor issues
klnyzzz33 Mar 25, 2026
4af77d4
Continued visual refactoring & fixed minor issues
klnyzzz33 Mar 25, 2026
1b86666
Fixed bug of "NEW" labels not showing up on Badges tab
klnyzzz33 Mar 26, 2026
57fbd36
Unified loading labels across profile tabs, fixed bug where navigatio…
klnyzzz33 Mar 26, 2026
f209425
Friends tab SearchBar refactoring
klnyzzz33 Mar 26, 2026
181be21
Fixed bugs with /search_users
klnyzzz33 Mar 28, 2026
0a15bbd
Started refactoring of friend and leaderboard functionalities
gabortodor Mar 29, 2026
2e92a3f
Merge branch 'gamification' into feature/leaderboards
klnyzzz33 Mar 31, 2026
396e924
Continued refactoring Friends components
klnyzzz33 Mar 31, 2026
dd599b9
Continued refactoring Friends and Leaderboards components
klnyzzz33 Apr 1, 2026
54476f7
friend_user_id -> friend_username refactor
gabortodor Apr 1, 2026
6d586c4
working on previous leaderboards
xXPinkmagicXx Apr 1, 2026
5aab589
Now the leaderboard period is a week
xXPinkmagicXx Apr 1, 2026
6defd91
Merge branch 'feature/leaderboards' into feature/previous-leaderboarrds
xXPinkmagicXx Apr 1, 2026
7b57579
Started Leaderboards.js refactor
gabortodor Apr 1, 2026
d3e438f
Simplified Leaderboards.js
gabortodor Apr 1, 2026
2e8b6d5
Started classroom leaderboard implementation
gabortodor Apr 1, 2026
a95e5ba
Styled prev and next buttons
xXPinkmagicXx Apr 2, 2026
ed80783
Spacing for the button in current period
xXPinkmagicXx Apr 2, 2026
c290246
Merge branch 'feature/leaderboards' into feature/previous-leaderboarrds
xXPinkmagicXx Apr 2, 2026
a24b399
Clean up
xXPinkmagicXx Apr 2, 2026
57a5914
Merge pull request #1013 from zeeguu/feature/previous-leaderboarrds
xXPinkmagicXx Apr 2, 2026
4ac936d
Working on gamification feature flag
xXPinkmagicXx Mar 26, 2026
53f5360
Using the gamification feature flag is used for the profile route
xXPinkmagicXx Mar 26, 2026
b1210a2
Redirect logic between gamification feature and regular implementation
xXPinkmagicXx Mar 26, 2026
4f28195
has_gamification
xXPinkmagicXx Apr 2, 2026
d81bd77
Removed unnecessary isDark checks
gabortodor Apr 3, 2026
3ba8731
Simplified friend request notification call
gabortodor Apr 3, 2026
88eca3a
Fixed dark mode issues and leaderboard sliding window
gabortodor Apr 3, 2026
1ac5b78
Removed badges NotificationIcon dependency
gabortodor Apr 3, 2026
4509d60
Removed badges NotificationIcon dependency
gabortodor Apr 3, 2026
49f3868
Fixed issues with mobile view and text overflowing
klnyzzz33 Apr 3, 2026
ab47dfd
Merge remote-tracking branch 'origin/feature/leaderboards' into featu…
klnyzzz33 Apr 3, 2026
7bf7a7c
Working on handling errors for existing username
xXPinkmagicXx Apr 3, 2026
1ecf977
Simplified ProfileDetails page error handling and handled bugs with e…
klnyzzz33 Apr 4, 2026
e590578
Minor sidebar visual fixes
klnyzzz33 Apr 4, 2026
f26d396
Minor sidebar visual fixes
klnyzzz33 Apr 4, 2026
c1bff5f
Merge branch 'gamification' into feature/leaderboards
gabortodor Apr 5, 2026
ee717e5
Tracked backend changes in fetching streak information
klnyzzz33 Apr 5, 2026
c277605
Matched the style of the profile languages modal to the other languag…
klnyzzz33 Apr 5, 2026
80df907
Fixed URL encoding issues which causes friend profile to not load
klnyzzz33 Apr 5, 2026
b939f81
Leaderboard date calculation is now done with the date-fns library
klnyzzz33 Apr 6, 2026
996ee5a
Tracked api friend request change
gabortodor Apr 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 0 additions & 43 deletions public/static/avatars/owl.svg

This file was deleted.

1 change: 1 addition & 0 deletions src/api/Zeeguu_API.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,6 @@ import "./dailyAudio";
import "./sessionHistory";
import "./badges";
import "./friends";
import "./leaderboards";

export default Zeeguu_API;
4 changes: 2 additions & 2 deletions src/api/badges.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ Zeeguu_API.prototype.getBadgesForUser = function(callback) {
this._getJSON(`/badges`, callback);
};

Zeeguu_API.prototype.getBadgesForFriend = function(userId, callback) {
this._getJSON(`/badges/${userId}`, callback);
Zeeguu_API.prototype.getBadgesForFriend = function(username, callback) {
this._getJSON(`/badges/${username}`, callback);
};

Zeeguu_API.prototype.getNotShownUserBadges = function (callback) {
Expand Down
42 changes: 24 additions & 18 deletions src/api/friends.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,45 +7,51 @@ Zeeguu_API.prototype.getFriends = function(callback) {
});
}

Zeeguu_API.prototype.getFriendsForUser = function(userId, callback) {
this._getJSON(`get_friends/${userId}`, (data) => {
Zeeguu_API.prototype.getFriendsForUser = function(username, callback) {
this._getJSON(`get_friends/${username}`, (data) => {
callback(data);
});
}

Zeeguu_API.prototype.getFriendRequests = function(callback) {
this._getJSON(`get_friend_requests`, (data) => {
Zeeguu_API.prototype.getNumberOfReceivedFriendRequests = function(callback) {
this._getJSON(`get_number_of_received_friend_requests`, (data) => {
callback(data);
});
}

Zeeguu_API.prototype.searchUsers = function(username, callback) {
this._getJSON(`search_users/${username}`, (data) => {
Zeeguu_API.prototype.getReceivedFriendRequests = function(callback) {
this._getJSON(`get_received_friend_requests`, (data) => {
callback(data);
});
}

Zeeguu_API.prototype.getFriendDetails = function(friend_user_id, callback) {
this._getJSON(`get_user_details/${friend_user_id}`, (data) => {
Zeeguu_API.prototype.searchUsers = function(search_term, callback) {
this._getJSON(`search_users?query=${encodeURIComponent(search_term)}`, (data) => {
callback(data);
});
}

Zeeguu_API.prototype.sendFriendRequest = function(receiver_id) {
return this.apiPost("/send_friend_request", { receiver_id }, false);
Zeeguu_API.prototype.getFriendDetails = function(friend_username, callback) {
this._getJSON(`get_user_details/${friend_username}`, (data) => {
callback(data);
});
}

Zeeguu_API.prototype.sendFriendRequest = function(receiver_username) {
return this.apiPost("/send_friend_request", { receiver_username }, false);
}

Zeeguu_API.prototype.deleteFriendRequest = function(receiver_id) {
return this.apiPost("/delete_friend_request", { receiver_id }, false);
Zeeguu_API.prototype.deleteFriendRequest = function(receiver_username) {
return this.apiPost("/delete_friend_request", { receiver_username }, false);
}

Zeeguu_API.prototype.acceptFriendRequest = function(sender_id) {
return this.apiPost(`/accept_friend_request`, { sender_id }, false);
Zeeguu_API.prototype.acceptFriendRequest = function(sender_username) {
return this.apiPost(`/accept_friend_request`, { sender_username }, false);
}

Zeeguu_API.prototype.rejectFriendRequest = function(sender_id) {
return this.apiPost(`/reject_friend_request`, { sender_id }, false);
Zeeguu_API.prototype.rejectFriendRequest = function(sender_username) {
return this.apiPost(`/reject_friend_request`, { sender_username }, false);
}
Zeeguu_API.prototype.unfriend = function(receiver_id) {
return this.apiPost("/unfriend", { receiver_id }, false);
Zeeguu_API.prototype.unfriend = function(receiver_username) {
return this.apiPost("/unfriend", { receiver_username }, false);
}
46 changes: 46 additions & 0 deletions src/api/leaderboards.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Zeeguu_API } from "./classDef";

function fetchFriendsLeaderboard(api, metric, fromDate, toDate, callback) {
if (!metric || !fromDate || !toDate) {
console.error("Leaderboard requires metric, fromDate, and toDate");
callback([]);
return;
}

const params = new URLSearchParams({
metric,
from_date: fromDate,
to_date: toDate,
});

api._getJSON(`friends_leaderboard?${params.toString()}`, (data) => {
callback(data);
});
}

function fetchCohortLeaderboard(api, cohortId, metric, fromDate, toDate, callback) {
if (!metric || !fromDate || !toDate) {
console.error("Leaderboard requires metric, fromDate, and toDate");
callback([]);
return;
}

const params = new URLSearchParams({
metric,
from_date: fromDate,
to_date: toDate,
});

api._getJSON(`cohort_leaderboard/${cohortId}?${params.toString()}`, (data) => {
callback(data);
});
}


Zeeguu_API.prototype.getFriendsLeaderboard = function(metric, fromDate, toDate, callback) {
fetchFriendsLeaderboard(this, metric, fromDate, toDate, callback);
};

Zeeguu_API.prototype.getCohortLeaderboard = function(cohortId, metric, fromDate, toDate, callback) {
fetchCohortLeaderboard(this, cohortId, metric, fromDate, toDate, callback);
};
6 changes: 3 additions & 3 deletions src/api/userStats.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ Zeeguu_API.prototype.getAllLanguageStreaks = function (callback) {
};

Zeeguu_API.prototype.getAllDailyStreakForUser = function (callback) {
this._getJSON("all_daily_streak", callback);
this._getJSON("all_language_streaks_detailed", callback);
};

Zeeguu_API.prototype.getAllDailyStreakForFriend = function (userId, callback) {
this._getJSON(`all_daily_streak/${userId}`, callback);
Zeeguu_API.prototype.getAllDailyStreakForFriend = function (username, callback) {
this._getJSON(`all_language_streaks_detailed/${username}`, callback);
};
174 changes: 91 additions & 83 deletions src/badges/Badges.js
Original file line number Diff line number Diff line change
@@ -1,56 +1,68 @@
import React, { useContext, useEffect, useState } from "react";
import { APIContext } from "../contexts/APIContext";
import * as s from "./Badges.sc.js";
import NotificationIcon from "../components/NotificationIcon";

export default function Badges({ userId }) {
export default function Badges({ username }) {
const api = useContext(APIContext);

const iconBasePath = "../../../public/static/badges/";
const defaultLogoPath = "../../../public/static/images/zeeguuLogo.svg";

const [levels, setLevels] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);

const fetchBadgesCallback = (data) => {
if (!data || data.error) {
setError(data?.error || "Could not load badges.");
setIsLoading(false);
return;
}

useEffect(() => {
const handleData = (data) => {
const allLevels = [];
let hasNewBadges = false;

data.forEach((badge) => {
badge.levels.forEach((lvl) => {
const level = {
...lvl,
badgeName: badge.name,
current_value: badge.current_value,
description: lvl.description
? lvl.description.replace("{target_value}", lvl.target_value)
: badge.description.replace("{target_value}", lvl.target_value),
};

if (level.achieved && !level.is_shown) {
hasNewBadges = true;
}

allLevels.push(level);
});
const allLevels = [];
let hasNewBadges = false;

data.forEach((badge) => {
badge.levels.forEach((lvl) => {
const level = {
...lvl,
badgeName: badge.name,
current_value: badge.current_value,
description: lvl.description
? lvl.description.replace("{target_value}", lvl.target_value)
: badge.description.replace("{target_value}", lvl.target_value),
};

if (level.achieved && !level.is_shown) {
hasNewBadges = true;
}

allLevels.push(level);
});
});

setLevels(allLevels);

setLevels(allLevels);
if (!username && hasNewBadges) {
api.updateNotShownForUser();
}

setIsLoading(false);
setError(null);
};

if (!userId && hasNewBadges) {
api.updateNotShownForUser();
}
};
useEffect(() => {
setIsLoading(true);
setError(null);

if (userId) {
api.getBadgesForFriend(userId, handleData);
if (username) {
api.getBadgesForFriend(username, fetchBadgesCallback);
} else {
api.getBadgesForUser(handleData);
api.getBadgesForUser(fetchBadgesCallback);
}
}, [api, userId]);
}, [api, username]);

const getIcon = (level) =>
level.icon_name ? iconBasePath + level.icon_name : defaultLogoPath;
const getIcon = (level) => (level.icon_name ? iconBasePath + level.icon_name : defaultLogoPath);

const iconStyle = (achieved) => ({
filter: achieved ? "none" : "grayscale(100%)",
Expand All @@ -59,55 +71,51 @@ export default function Badges({ userId }) {

const formatDateTime = (iso) =>
iso
? new Date(iso).toLocaleString(undefined, {
dateStyle: "short",
timeStyle: "short",
}).replace(",", "")
? new Date(iso)
.toLocaleString(undefined, {
dateStyle: "short",
timeStyle: "short",
})
.replace(",", "")
: "—";

return (
<s.BadgeContainer>
{levels.map((level, index) => (
<s.BadgeCard key={index}>
{!userId && !level.is_shown && level.achieved && (
<NotificationIcon text="NEW" position="card-corner" isActive={true} />
)}

<div className="icon-container">
<img
src={getIcon(level)}
style={iconStyle(level.achieved)}
alt={level.name}
/>
</div>

<h3>{level.name || `Level ${level.badge_level}`}</h3>

<div>{level.description}</div>

{level.achieved ? (
<s.AchievedAtBox>{formatDateTime(level.achieved_at)}</s.AchievedAtBox>
) : (
<s.ProgressWrapper>
<s.ProgressBar>
<s.ProgressFill
style={{
width: `${
(Math.min(level.current_value, level.target_value) /
level.target_value) *
100
}%`,
}}
/>
</s.ProgressBar>

<s.ProgressText>
{level.current_value} / {level.target_value}
</s.ProgressText>
</s.ProgressWrapper>
)}
</s.BadgeCard>
))}
</s.BadgeContainer>
<>
{isLoading && <p>Loading badges...</p>}
{!isLoading && error && <p style={{ color: "#b00020" }}>{error}</p>}

{!isLoading && !error && (
<s.BadgeContainer>
{levels.map((level, index) => (
<s.BadgeCard key={index}>
{!username && !level.is_shown && level.achieved && <s.NewTag>NEW</s.NewTag>}
<s.IconContainer>
<s.BadgeIcon src={getIcon(level)} style={iconStyle(level.achieved)} alt={level.name} />
</s.IconContainer>
<s.BadgeTitle>{level.name || `Level ${level.badge_level}`}</s.BadgeTitle>
<s.BadgeDescription>{level.description}</s.BadgeDescription>

{level.achieved ? (
<s.AchievedAtBox>{formatDateTime(level.achieved_at)}</s.AchievedAtBox>
) : (
<s.ProgressWrapper>
<s.ProgressBar>
<s.ProgressFill
style={{
width: `${(Math.min(level.current_value, level.target_value) / level.target_value) * 100}%`,
}}
/>
</s.ProgressBar>

<s.ProgressText>
{level.current_value} / {level.target_value}
</s.ProgressText>
</s.ProgressWrapper>
)}
</s.BadgeCard>
))}
</s.BadgeContainer>
)}
</>
);
}
}
Loading