Repository: hydralauncher/hydra
Pull request #1486: feat: adding dynamic badges
Author: login: chubbygrannychaser
Created: 2025-03-04T19:46:19Z | Merged: 2025-03-04T20:45:36Z

<!-- Please be sure to add one of the labels in the right hand side Labels option before creating a PR: [feature], [fix], [documentation],[translation]. This will allow Actions to automatically categorize PRs when generating Releases. -->

**When submitting this pull request, I confirm the following (please check the boxes):**

- [ ] I have read and understood the [Contributor Guidelines](https://github.com/hydralauncher/hydra?tab=readme-ov-file#ways-you-can-contribute).
- [ ] I have checked that there are no duplicate pull requests related to this request.
- [ ] I have considered, and confirm that this submission is valuable to others.
- [ ] I accept that this submission may not be used and the pull request may be closed at the discretion of the maintainers.

**Fill in the PR content:**

-



Diff:
diff --git a/package.json b/package.json
index 136f3e2fa..3d2e56fde 100644
--- a/package.json
+++ b/package.json
@@ -71,6 +71,7 @@
     "react-loading-skeleton": "^3.4.0",
     "react-redux": "^9.1.1",
     "react-router-dom": "^6.22.3",
+    "react-tooltip": "^5.28.0",
     "sound-play": "^1.1.0",
     "sudo-prompt": "^9.2.1",
     "tar": "^7.4.3",
diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json
index bf91cc40f..f1e850190 100644
--- a/src/locales/en/translation.json
+++ b/src/locales/en/translation.json
@@ -444,9 +444,6 @@
     "show_achievements_on_profile": "Show your achievements on your profile",
     "show_points_on_profile": "Show your earned points on your profile"
   },
-  "badge": {
-    "badge_description_theme_creator": "Awarded to those who created a custom theme"
-  },
   "achievement": {
     "achievement_unlocked": "Achievement unlocked",
     "user_achievements": "{{displayName}}'s Achievements",
diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json
index 3556dbd17..0cefd188d 100644
--- a/src/locales/pt-BR/translation.json
+++ b/src/locales/pt-BR/translation.json
@@ -440,9 +440,6 @@
     "show_achievements_on_profile": "Exiba suas conquistas no perfil",
     "show_points_on_profile": "Exiba seus pontos ganhos no perfil"
   },
-  "badge": {
-    "badge_description_theme_creator": "Concedido àqueles que criaram um tema customizado"
-  },
   "achievement": {
     "achievement_unlocked": "Conquista desbloqueada",
     "your_achievements": "Suas Conquistas",
diff --git a/src/locales/ru/translation.json b/src/locales/ru/translation.json
index 494b8d066..c2aa40ce0 100644
--- a/src/locales/ru/translation.json
+++ b/src/locales/ru/translation.json
@@ -440,9 +440,6 @@
     "show_achievements_on_profile": "Покажите свои достижения в профиле",
     "show_points_on_profile": "Показывать заработанные очки в своем профиле"
   },
-  "badge": {
-    "badge_description_theme_creator": "Награждается тот, кто создал пользовательскую тему"
-  },
   "achievement": {
     "achievement_unlocked": "Достижение разблокировано",
     "user_achievements": "Достижения {{displayName}}",
diff --git a/src/main/events/index.ts b/src/main/events/index.ts
index 572cba0fa..8b8f4ebca 100644
--- a/src/main/events/index.ts
+++ b/src/main/events/index.ts
@@ -87,6 +87,7 @@ import "./themes/get-custom-theme-by-id";
 import "./themes/get-active-custom-theme";
 import "./themes/close-editor-window";
 import "./themes/toggle-custom-theme";
+import "./misc/get-badges";
 import { isPortableVersion } from "@main/helpers";
 
 ipcMain.handle("ping", () => "pong");
diff --git a/src/main/events/misc/get-badges.ts b/src/main/events/misc/get-badges.ts
new file mode 100644
index 000000000..7f4bc3eac
--- /dev/null
+++ b/src/main/events/misc/get-badges.ts
@@ -0,0 +1,22 @@
+import { Badge } from "@types";
+import { registerEvent } from "../register-event";
+import { HydraApi } from "@main/services";
+import { db, levelKeys } from "@main/level";
+
+const getBadges = async (_event: Electron.IpcMainInvokeEvent) => {
+  const language = await db
+    .get<string, string>(levelKeys.language, {
+      valueEncoding: "utf-8",
+    })
+    .then((language) => language || "en");
+
+  const params = new URLSearchParams({
+    locale: language,
+  });
+
+  return HydraApi.get<Badge[]>(`/badges?${params.toString()}`, null, {
+    needsAuth: false,
+  });
+};
+
+registerEvent("getBadges", getBadges);
diff --git a/src/main/main.ts b/src/main/main.ts
index 6bcd0ff63..a7484f58a 100644
--- a/src/main/main.ts
+++ b/src/main/main.ts
@@ -132,7 +132,9 @@ const migrateFromSqlite = async () => {
         );
 
         if (rest.language) {
-          await db.put(levelKeys.language, rest.language);
+          await db.put<string, string>(levelKeys.language, rest.language, {
+            valueEncoding: "utf-8",
+          });
         }
       }
     })
diff --git a/src/preload/index.ts b/src/preload/index.ts
index 5b3498ac9..7b94cda13 100644
--- a/src/preload/index.ts
+++ b/src/preload/index.ts
@@ -266,6 +266,7 @@ contextBridge.exposeInMainWorld("electron", {
   showItemInFolder: (path: string) =>
     ipcRenderer.invoke("showItemInFolder", path),
   getFeatures: () => ipcRenderer.invoke("getFeatures"),
+  getBadges: () => ipcRenderer.invoke("getBadges"),
   platform: process.platform,
 
   /* Auto update */
diff --git a/src/renderer/src/assets/icons/badge-theme-creator.svg b/src/renderer/src/assets/icons/badge-theme-creator.svg
deleted file mode 100644
index 0793a7e92..000000000
--- a/src/renderer/src/assets/icons/badge-theme-creator.svg
+++ /dev/null
@@ -1,29 +0,0 @@
-<svg width="240" height="246" viewBox="0 0 240 246" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M117.681 8.44054C120.27 4.75592 125.73 4.75592 128.319 8.44054L149.273 38.2669C151.08 40.8399 154.301 42.0121 157.339 41.203L192.563 31.8236C196.914 30.6649 201.097 34.1747 200.712 38.6612L197.591 74.9784C197.322 78.1113 199.036 81.0795 201.884 82.4128L234.895 97.8691C238.974 99.7785 239.922 105.156 236.743 108.345L211.008 134.16C208.788 136.387 208.193 139.762 209.517 142.614L224.871 175.674C226.767 179.758 224.037 184.486 219.552 184.886L183.245 188.119C180.113 188.398 177.487 190.601 176.669 193.637L167.18 228.832C166.007 233.179 160.876 235.047 157.184 232.47L127.292 211.609C124.714 209.809 121.286 209.809 118.708 211.609L88.8163 232.47C85.1236 235.047 79.9927 233.179 78.8204 228.832L69.3314 193.637C68.5129 190.601 65.8873 188.398 62.7553 188.119L26.4479 184.886C21.9627 184.486 19.2326 179.758 21.1293 175.674L36.4827 142.614C37.8072 139.762 37.212 136.387 34.992 134.16L9.25738 108.345C6.07823 105.156 7.02639 99.7785 11.1045 97.8691L44.1164 82.4128C46.9642 81.0795 48.6778 78.1113 48.4087 74.9784L45.2883 38.6611C44.9028 34.1747 49.0856 30.6649 53.437 31.8236L88.6606 41.203C91.6992 42.0121 94.9198 40.8399 96.7274 38.2669L117.681 8.44054Z" fill="url(#paint0_linear_1378_2496)"/>
-<path d="M117.681 8.44054C120.27 4.75592 125.73 4.75592 128.319 8.44054L149.273 38.2669C151.08 40.8399 154.301 42.0121 157.339 41.203L192.563 31.8236C196.914 30.6649 201.097 34.1747 200.712 38.6612L197.591 74.9784C197.322 78.1113 199.036 81.0795 201.884 82.4128L234.895 97.8691C238.974 99.7785 239.922 105.156 236.743 108.345L211.008 134.16C208.788 136.387 208.193 139.762 209.517 142.614L224.871 175.674C226.767 179.758 224.037 184.486 219.552 184.886L183.245 188.119C180.113 188.398 177.487 190.601 176.669 193.637L167.18 228.832C166.007 233.179 160.876 235.047 157.184 232.47L127.292 211.609C124.714 209.809 121.286 209.809 118.708 211.609L88.8163 232.47C85.1236 235.047 79.9927 233.179 78.8204 228.832L69.3314 193.637C68.5129 190.601 65.8873 188.398 62.7553 188.119L26.4479 184.886C21.9627 184.486 19.2326 179.758 21.1293 175.674L36.4827 142.614C37.8072 139.762 37.212 136.387 34.992 134.16L9.25738 108.345C6.07823 105.156 7.02639 99.7785 11.1045 97.8691L44.1164 82.4128C46.9642 81.0795 48.6778 78.1113 48.4087 74.9784L45.2883 38.6611C44.9028 34.1747 49.0856 30.6649 53.437 31.8236L88.6606 41.203C91.6992 42.0121 94.9198 40.8399 96.7274 38.2669L117.681 8.44054Z" fill="url(#paint1_linear_1378_2496)"/>
-<path d="M117.681 8.44054C120.27 4.75592 125.73 4.75592 128.319 8.44054L149.273 38.2669C151.08 40.8399 154.301 42.0121 157.339 41.203L192.563 31.8236C196.914 30.6649 201.097 34.1747 200.712 38.6612L197.591 74.9784C197.322 78.1113 199.036 81.0795 201.884 82.4128L234.895 97.8691C238.974 99.7785 239.922 105.156 236.743 108.345L211.008 134.16C208.788 136.387 208.193 139.762 209.517 142.614L224.871 175.674C226.767 179.758 224.037 184.486 219.552 184.886L183.245 188.119C180.113 188.398 177.487 190.601 176.669 193.637L167.18 228.832C166.007 233.179 160.876 235.047 157.184 232.47L127.292 211.609C124.714 209.809 121.286 209.809 118.708 211.609L88.8163 232.47C85.1236 235.047 79.9927 233.179 78.8204 228.832L69.3314 193.637C68.5129 190.601 65.8873 188.398 62.7553 188.119L26.4479 184.886C21.9627 184.486 19.2326 179.758 21.1293 175.674L36.4827 142.614C37.8072 139.762 37.212 136.387 34.992 134.16L9.25738 108.345C6.07823 105.156 7.02639 99.7785 11.1045 97.8691L44.1164 82.4128C46.9642 81.0795 48.6778 78.1113 48.4087 74.9784L45.2883 38.6611C44.9028 34.1747 49.0856 30.6649 53.437 31.8236L88.6606 41.203C91.6992 42.0121 94.9198 40.8399 96.7274 38.2669L117.681 8.44054Z" stroke="url(#paint2_linear_1378_2496)"/>
-<g opacity="0.9">
-<g style="mix-blend-mode:overlay">
-<path d="M113.207 107.103L113.873 107.307L114.295 106.754C120.652 98.4238 129.53 87.9999 139.582 79.6678C149.661 71.3136 160.772 65.1865 171.609 65.1865C172.421 65.1865 173.2 65.5092 173.775 66.0835C174.349 66.6578 174.672 67.4368 174.672 68.249C174.672 79.0868 168.546 90.1972 160.192 100.276C151.861 110.328 141.437 119.207 133.105 125.563L132.551 125.985L132.755 126.651C134.104 131.057 134.402 135.718 133.623 140.26C132.844 144.802 131.011 149.098 128.271 152.803C125.531 156.508 121.96 159.519 117.845 161.594C113.73 163.668 109.186 164.749 104.578 164.749H65.9851C65.3296 164.748 64.6916 164.538 64.1649 164.147C63.6381 163.757 63.2505 163.208 63.059 162.581C62.8674 161.954 62.8821 161.283 63.1008 160.665C63.3195 160.047 63.7307 159.515 64.274 159.149L64.2813 159.144C64.8739 158.736 75.1093 151.439 75.1093 135.28C75.1093 130.672 76.19 126.128 78.2646 122.013C80.3393 117.898 83.35 114.328 87.055 111.587C90.76 108.847 95.056 107.014 99.598 106.235C104.14 105.457 108.801 105.754 113.207 107.103ZM120.25 109.05L119.585 109.911L120.499 110.501C124.047 112.792 127.067 115.811 129.357 119.359L129.948 120.275L130.809 119.608C133.07 117.857 135.204 116.141 137.212 114.46L137.904 113.881L137.399 113.133C134.556 108.926 130.933 105.302 126.725 102.459L125.979 101.955L125.4 102.645C123.714 104.65 121.997 106.785 120.25 109.05ZM141.909 108.878L142.566 109.805L143.402 109.036C161.044 92.7992 166.529 80.4502 168.029 72.9963L168.328 71.5152L166.848 71.8195C159.403 73.3505 147.056 78.8076 130.817 96.4511L130.048 97.2872L130.975 97.9442C135.21 100.946 138.907 104.643 141.909 108.878Z" fill="black"/>
-<path d="M113.207 107.103L113.873 107.307L114.295 106.754C120.652 98.4238 129.53 87.9999 139.582 79.6678C149.661 71.3136 160.772 65.1865 171.609 65.1865C172.421 65.1865 173.2 65.5092 173.775 66.0835C174.349 66.6578 174.672 67.4368 174.672 68.249C174.672 79.0868 168.546 90.1972 160.192 100.276C151.861 110.328 141.437 119.207 133.105 125.563L132.551 125.985L132.755 126.651C134.104 131.057 134.402 135.718 133.623 140.26C132.844 144.802 131.011 149.098 128.271 152.803C125.531 156.508 121.96 159.519 117.845 161.594C113.73 163.668 109.186 164.749 104.578 164.749H65.9851C65.3296 164.748 64.6916 164.538 64.1649 164.147C63.6381 163.757 63.2505 163.208 63.059 162.581C62.8674 161.954 62.8821 161.283 63.1008 160.665C63.3195 160.047 63.7307 159.515 64.274 159.149L64.2813 159.144C64.8739 158.736 75.1093 151.439 75.1093 135.28C75.1093 130.672 76.19 126.128 78.2646 122.013C80.3393 117.898 83.35 114.328 87.055 111.587C90.76 108.847 95.056 107.014 99.598 106.235C104.14 105.457 108.801 105.754 113.207 107.103ZM120.25 109.05L119.585 109.911L120.499 110.501C124.047 112.792 127.067 115.811 129.357 119.359L129.948 120.275L130.809 119.608C133.07 117.857 135.204 116.141 137.212 114.46L137.904 113.881L137.399 113.133C134.556 108.926 130.933 105.302 126.725 102.459L125.979 101.955L125.4 102.645C123.714 104.65 121.997 106.785 120.25 109.05ZM141.909 108.878L142.566 109.805L143.402 109.036C161.044 92.7992 166.529 80.4502 168.029 72.9963L168.328 71.5152L166.848 71.8195C159.403 73.3505 147.056 78.8076 130.817 96.4511L130.048 97.2872L130.975 97.9442C135.21 100.946 138.907 104.643 141.909 108.878Z" stroke="url(#paint3_linear_1378_2496)" stroke-width="2"/>
-</g>
-</g>
-<defs>
-<linearGradient id="paint0_linear_1378_2496" x1="5.63736e-07" y1="12.92" x2="246" y2="233.08" gradientUnits="userSpaceOnUse">
-<stop stop-color="#0CF1CA"/>
-<stop offset="1" stop-color="#1DCCEB"/>
-</linearGradient>
-<linearGradient id="paint1_linear_1378_2496" x1="19.8951" y1="-3.50306e-06" x2="226.105" y2="246" gradientUnits="userSpaceOnUse">
-<stop stop-color="#0DDEBB"/>
-<stop offset="1" stop-color="#052520"/>
-</linearGradient>
-<linearGradient id="paint2_linear_1378_2496" x1="-1.9947e-06" y1="18.0561" x2="246" y2="227.944" gradientUnits="userSpaceOnUse">
-<stop stop-color="white" stop-opacity="0.7"/>
-<stop offset="1" stop-color="white" stop-opacity="0.1"/>
-</linearGradient>
-<linearGradient id="paint3_linear_1378_2496" x1="61.9253" y1="71.6411" x2="164.664" y2="169.814" gradientUnits="userSpaceOnUse">
-<stop stop-color="white"/>
-<stop offset="1" stop-color="white" stop-opacity="0.2"/>
-</linearGradient>
-</defs>
-</svg>
diff --git a/src/renderer/src/components/avatar/avatar.scss b/src/renderer/src/components/avatar/avatar.scss
index 57ad6ebfc..c45c28b1a 100644
--- a/src/renderer/src/components/avatar/avatar.scss
+++ b/src/renderer/src/components/avatar/avatar.scss
@@ -10,6 +10,7 @@
   cursor: pointer;
   color: globals.$muted-color;
   position: relative;
+  overflow: hidden;
 
   &__image {
     height: 100%;
diff --git a/src/renderer/src/components/avatar/avatar.tsx b/src/renderer/src/components/avatar/avatar.tsx
index 9d68998aa..99800acda 100644
--- a/src/renderer/src/components/avatar/avatar.tsx
+++ b/src/renderer/src/components/avatar/avatar.tsx
@@ -1,4 +1,5 @@
 import { PersonIcon } from "@primer/octicons-react";
+import cn from "classnames";
 
 import "./avatar.scss";
 
@@ -14,11 +15,18 @@ export interface AvatarProps
   src?: string | null;
 }
 
-export function Avatar({ size, alt, src, ...props }: AvatarProps) {
+export function Avatar({ size, alt, src, className, ...props }: AvatarProps) {
   return (
     <div className="profile-avatar" style={{ width: size, height: size }}>
       {src ? (
-        <img className="profile-avatar__image" alt={alt} src={src} {...props} />
+        <img
+          className={cn("profile-avatar__image", className)}
+          alt={alt}
+          src={src}
+          width={size}
+          height={size}
+          {...props}
+        />
       ) : (
         <PersonIcon size={size * 0.7} />
       )}
diff --git a/src/renderer/src/context/user-profile/user-profile.context.tsx b/src/renderer/src/context/user-profile/user-profile.context.tsx
index ce8319815..9b9d16b40 100644
--- a/src/renderer/src/context/user-profile/user-profile.context.tsx
+++ b/src/renderer/src/context/user-profile/user-profile.context.tsx
@@ -1,6 +1,6 @@
 import { darkenColor } from "@renderer/helpers";
 import { useAppSelector, useToast } from "@renderer/hooks";
-import type { UserProfile, UserStats } from "@types";
+import type { Badge, UserProfile, UserStats } from "@types";
 import { average } from "color.js";
 
 import { createContext, useCallback, useEffect, useState } from "react";
@@ -16,6 +16,7 @@ export interface UserProfileContext {
   getUserProfile: () => Promise<void>;
   setSelectedBackgroundImage: React.Dispatch<React.SetStateAction<string>>;
   backgroundImage: string;
+  badges: Badge[];
 }
 
 export const DEFAULT_USER_PROFILE_BACKGROUND = "#151515B3";
@@ -28,6 +29,7 @@ export const userProfileContext = createContext<UserProfileContext>({
   getUserProfile: async () => {},
   setSelectedBackgroundImage: () => {},
   backgroundImage: "",
+  badges: [],
 });
 
 const { Provider } = userProfileContext;
@@ -47,6 +49,7 @@ export function UserProfileContextProvider({
   const [userStats, setUserStats] = useState<UserStats | null>(null);
 
   const [userProfile, setUserProfile] = useState<UserProfile | null>(null);
+  const [badges, setBadges] = useState<Badge[]>([]);
   const [heroBackground, setHeroBackground] = useState(
     DEFAULT_USER_PROFILE_BACKGROUND
   );
@@ -101,12 +104,18 @@ export function UserProfileContextProvider({
     });
   }, [navigate, getUserStats, showErrorToast, userId, t]);
 
+  const getBadges = useCallback(async () => {
+    const badges = await window.electron.getBadges();
+    setBadges(badges);
+  }, []);
+
   useEffect(() => {
     setUserProfile(null);
     setHeroBackground(DEFAULT_USER_PROFILE_BACKGROUND);
 
     getUserProfile();
-  }, [getUserProfile]);
+    getBadges();
+  }, [getUserProfile, getBadges]);
 
   return (
     <Provider
@@ -118,6 +127,7 @@ export function UserProfileContextProvider({
         setSelectedBackgroundImage,
         backgroundImage: getBackgroundImageUrl(),
         userStats,
+        badges,
       }}
     >
       {children}
diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts
index 1b588d5c1..4700fbb13 100644
--- a/src/renderer/src/declaration.d.ts
+++ b/src/renderer/src/declaration.d.ts
@@ -30,6 +30,7 @@ import type {
   GameRunning,
   TorBoxUser,
   Theme,
+  Badge,
 } from "@types";
 import type { AxiosProgressEvent } from "axios";
 import type disk from "diskusage";
@@ -217,6 +218,7 @@ declare global {
     ) => Promise<Electron.OpenDialogReturnValue>;
     showItemInFolder: (path: string) => Promise<void>;
     getFeatures: () => Promise<string[]>;
+    getBadges: () => Promise<Badge[]>;
     platform: NodeJS.Platform;
 
     /* Auto update */
diff --git a/src/renderer/src/main.tsx b/src/renderer/src/main.tsx
index eb890d189..bb0a3e03d 100644
--- a/src/renderer/src/main.tsx
+++ b/src/renderer/src/main.tsx
@@ -11,6 +11,7 @@ import "@fontsource/noto-sans/500.css";
 import "@fontsource/noto-sans/700.css";
 
 import "react-loading-skeleton/dist/skeleton.css";
+import "react-tooltip/dist/react-tooltip.css";
 
 import { App } from "./app";
 
diff --git a/src/renderer/src/pages/achievements/achievements-content.scss b/src/renderer/src/pages/achievements/achievements-content.scss
index 8ee1773f3..a378f4902 100644
--- a/src/renderer/src/pages/achievements/achievements-content.scss
+++ b/src/renderer/src/pages/achievements/achievements-content.scss
@@ -31,7 +31,7 @@ $logo-max-width: 200px;
         display: flex;
         justify-content: center;
         width: 100%;
-        gap: globals.$spacing-unit / 2;
+        gap: calc(globals.$spacing-unit / 2);
         color: globals.$body-color;
         cursor: pointer;
 
diff --git a/src/renderer/src/pages/profile/profile-hero/profile-hero.scss b/src/renderer/src/pages/profile/profile-hero/profile-hero.scss
index fd21cf1d0..834cb0cfe 100644
--- a/src/renderer/src/pages/profile/profile-hero/profile-hero.scss
+++ b/src/renderer/src/pages/profile/profile-hero/profile-hero.scss
@@ -27,6 +27,11 @@
     }
   }
 
+  &__badges {
+    display: flex;
+    gap: calc(globals.$spacing-unit / 2);
+  }
+
   &__user-information {
     display: flex;
     padding: calc(globals.$spacing-unit * 7) calc(globals.$spacing-unit * 3);
@@ -82,12 +87,6 @@
     text-shadow: 0 0 5px rgb(0 0 0 / 40%);
   }
 
-  &__display-name-badges-container {
-    display: flex;
-    gap: globals.$spacing-unit;
-    align-items: center;
-  }
-
   &__current-game {
     &-wrapper {
       display: flex;
diff --git a/src/renderer/src/pages/profile/profile-hero/profile-hero.tsx b/src/renderer/src/pages/profile/profile-hero/profile-hero.tsx
index 66799c474..fc354d01f 100644
--- a/src/renderer/src/pages/profile/profile-hero/profile-hero.tsx
+++ b/src/renderer/src/pages/profile/profile-hero/profile-hero.tsx
@@ -24,8 +24,8 @@ import type { FriendRequestAction } from "@types";
 import { EditProfileModal } from "../edit-profile-modal/edit-profile-modal";
 import Skeleton from "react-loading-skeleton";
 import { UploadBackgroundImageButton } from "../upload-background-image-button/upload-background-image-button";
+import { Tooltip } from "react-tooltip";
 import "./profile-hero.scss";
-import { UserBadges } from "./user-badges";
 
 type FriendAction =
   | FriendRequestAction
@@ -35,8 +35,14 @@ export function ProfileHero() {
   const [showEditProfileModal, setShowEditProfileModal] = useState(false);
   const [isPerformingAction, setIsPerformingAction] = useState(false);
 
-  const { isMe, getUserProfile, userProfile, heroBackground, backgroundImage } =
-    useContext(userProfileContext);
+  const {
+    isMe,
+    badges,
+    getUserProfile,
+    userProfile,
+    heroBackground,
+    backgroundImage,
+  } = useContext(userProfileContext);
   const {
     signOut,
     updateFriendRequestState,
@@ -261,14 +267,6 @@ export function ProfileHero() {
 
   return (
     <>
-      {/* <ConfirmationModal
-        visible
-        title={t("sign_out_modal_title")}
-        descriptionText={t("sign_out_modal_text")}
-        confirmButtonLabel={t("sign_out")}
-        cancelButtonLabel={t("cancel")}
-      /> */}
-
       <EditProfileModal
         visible={showEditProfileModal}
         onClose={() => setShowEditProfileModal(false)}
@@ -312,7 +310,29 @@ export function ProfileHero() {
                   <h2 className="profile-hero__display-name">
                     {userProfile?.displayName}
                   </h2>
-                  <UserBadges />
+
+                  <div className="profile-hero__badges">
+                    {userProfile.badges.map((badgeName) => {
+                      const badge = badges.find((b) => b.name === badgeName);
+
+                      if (!badge) return null;
+
+                      return (
+                        <img
+                          key={badge.name}
+                          src={badge.badge.url}
+                          alt={badge.name}
+                          width={24}
+                          height={24}
+                          data-tooltip-place="top"
+                          data-tooltip-content={badge.description}
+                          data-tooltip-id="badge-name"
+                        />
+                      );
+                    })}
+
+                    <Tooltip id="badge-name" />
+                  </div>
                 </div>
               ) : (
                 <Skeleton width={150} height={28} />
diff --git a/src/renderer/src/pages/profile/profile-hero/user-badges.tsx b/src/renderer/src/pages/profile/profile-hero/user-badges.tsx
deleted file mode 100644
index ed5d05713..000000000
--- a/src/renderer/src/pages/profile/profile-hero/user-badges.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import BadgeThemeCreator from "@renderer/assets/icons/badge-theme-creator.svg?react";
-import "./profile-hero.scss";
-import { useContext } from "react";
-import { userProfileContext } from "@renderer/context";
-import { UserBadge } from "@types";
-import { useTranslation } from "react-i18next";
-
-export function UserBadges() {
-  const { t } = useTranslation("badge");
-  const { userProfile } = useContext(userProfileContext);
-
-  if (!userProfile?.badges?.length) return null;
-
-  const getBadgeIcon = (badge: UserBadge) => {
-    if (badge === "THEME_CREATOR") {
-      return <BadgeThemeCreator width={24} height={24} />;
-    }
-
-    return null;
-  };
-
-  return (
-    <div className="profile-hero__display-name-badges-container">
-      {userProfile.badges.map((badge) => {
-        const badgeIcon = getBadgeIcon(badge);
-
-        if (!badgeIcon) return null;
-        return (
-          <div
-            className={`badge__${badge.toLowerCase()}`}
-            key={badge}
-            title={t(`badge_description_${badge.toLowerCase()}`)}
-          >
-            {badgeIcon}
-          </div>
-        );
-      })}
-    </div>
-  );
-}
diff --git a/src/types/index.ts b/src/types/index.ts
index e17a694af..a8726b6b2 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -130,7 +130,13 @@ export interface UserProfileCurrentGame extends Omit<GameRunning, "objectId"> {
 
 export type ProfileVisibility = "PUBLIC" | "PRIVATE" | "FRIENDS";
 
-export type UserBadge = "THEME_CREATOR";
+export interface Badge {
+  name: string;
+  description: string;
+  badge: {
+    url: string;
+  };
+}
 
 export interface UserDetails {
   id: string;
@@ -166,7 +172,7 @@ export interface UserProfile {
   quirks: {
     backupsPerGameLimit: number;
   };
-  badges: UserBadge[];
+  badges: string[];
 }
 
 export interface UpdateProfileRequest {
diff --git a/yarn.lock b/yarn.lock
index 58d0cc9a7..22b895e02 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1663,6 +1663,14 @@
     "@floating-ui/core" "^1.6.0"
     "@floating-ui/utils" "^0.2.8"
 
+"@floating-ui/dom@^1.6.1":
+  version "1.6.13"
+  resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.13.tgz#a8a938532aea27a95121ec16e667a7cbe8c59e34"
+  integrity sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==
+  dependencies:
+    "@floating-ui/core" "^1.6.0"
+    "@floating-ui/utils" "^0.2.9"
+
 "@floating-ui/react-dom@^2.0.0":
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.1.2.tgz#a1349bbf6a0e5cb5ded55d023766f20a4d439a31"
@@ -1675,6 +1683,11 @@
   resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.8.tgz#21a907684723bbbaa5f0974cf7730bd797eb8e62"
   integrity sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==
 
+"@floating-ui/utils@^0.2.9":
+  version "0.2.9"
+  resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.9.tgz#50dea3616bc8191fb8e112283b49eaff03e78429"
+  integrity sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==
+
 "@fontsource/noto-sans@^5.1.0":
   version "5.1.0"
   resolved "https://registry.yarnpkg.com/@fontsource/noto-sans/-/noto-sans-5.1.0.tgz#54a5edd1b2b8c8e17bec6a85d4ee3a53b4b89c1f"
@@ -4279,7 +4292,7 @@ classic-level@^2.0.0:
     napi-macros "^2.2.2"
     node-gyp-build "^4.3.0"
 
-classnames@^2.2.1, classnames@^2.2.6, classnames@^2.5.1:
+classnames@^2.2.1, classnames@^2.2.6, classnames@^2.3.0, classnames@^2.5.1:
   version "2.5.1"
   resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b"
   integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==
@@ -8093,6 +8106,14 @@ react-style-singleton@^2.2.1:
     invariant "^2.2.4"
     tslib "^2.0.0"
 
+react-tooltip@^5.28.0:
+  version "5.28.0"
+  resolved "https://registry.yarnpkg.com/react-tooltip/-/react-tooltip-5.28.0.tgz#c7b5343ab2d740a428494a3d8315515af1f26f46"
+  integrity sha512-R5cO3JPPXk6FRbBHMO0rI9nkUG/JKfalBSQfZedZYzmqaZQgq7GLzF8vcCWx6IhUCKg0yPqJhXIzmIO5ff15xg==
+  dependencies:
+    "@floating-ui/dom" "^1.6.1"
+    classnames "^2.3.0"
+
 react@^18.2.0:
   version "18.3.1"
   resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891"


Reviews:
user: login: greptile-apps[bot]
body: 
## PR Summary

This PR implements dynamic badge functionality by replacing static badge translations with a server-driven system for managing user profile badges and their descriptions.

- Added new `getBadges` API endpoint in `src/main/events/misc/get-badges.ts` to fetch localized badge data based on user language
- Removed static badge translations from all locale files (en, ru, pt-BR) in favor of dynamic server-driven content
- Modified `UserProfile` interface in `types/index.ts` to support new `Badge` interface with name, description and badge URL properties
- Added React-Tooltip integration in profile hero component for displaying dynamic badge descriptions
- Improved avatar component layout handling by adding explicit width/height attributes and overflow control

<sub>17 file(s) reviewed, 2 comment(s)</sub>
<sub>[Edit PR Review Bot Settings](https://app.greptile.com/review/github) | [Greptile](https://greptile.com?utm_source=greptile_expert&utm_medium=github&utm_campaign=code_reviews&utm_content=hydra_1486)</sub>
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T19:49:45Z
user: login: zamitto
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-04T19:51:27Z

Review comments:
diff_hunk: @@ -18,7 +18,14 @@ export function Avatar({ size, alt, src, ...props }: AvatarProps) {
   return (
     <div className="profile-avatar" style={{ width: size, height: size }}>
       {src ? (
-        <img className="profile-avatar__image" alt={alt} src={src} {...props} />
+        <img
+          className="profile-avatar__image"
+          alt={alt}
+          src={src}
+          width={size}
+          height={size}
+          {...props}
path: src/renderer/src/components/avatar/avatar.tsx
user: login: greptile-apps[bot]
body: style: Consider moving {...props} before width/height to ensure explicit size attributes take precedence over any spread props

```suggestion
          {...props}
          width={size}
          height={size}
```
created_at: 2025-03-04T19:48:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -312,7 +310,29 @@ export function ProfileHero() {
                   <h2 className="profile-hero__display-name">
                     {userProfile?.displayName}
                   </h2>
-                  <UserBadges />
+
+                  <div className="profile-hero__badges">
+                    {userProfile.badges.map((badgeName) => {
+                      const badge = badges.find((b) => b.name === badgeName);
+
+                      if (!badge) return null;
+
+                      return (
+                        <img
+                          key={badge.name}
+                          src={badge.badge.url}
+                          alt={badge.name}
+                          width={24}
+                          height={24}
path: src/renderer/src/pages/profile/profile-hero/profile-hero.tsx
user: login: greptile-apps[bot]
body: style: Consider using CSS classes for badge dimensions instead of inline width/height attributes
created_at: 2025-03-04T19:48:40Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-03-04T20:37:58Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=hydralauncher_hydra&pullRequest=1486) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [1 New issue](https://sonarcloud.io/project/issues?id=hydralauncher_hydra&pullRequest=1486&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=hydralauncher_hydra&pullRequest=1486&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=hydralauncher_hydra&pullRequest=1486&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=hydralauncher_hydra&pullRequest=1486&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=hydralauncher_hydra&pullRequest=1486&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=hydralauncher_hydra&pullRequest=1486)


author_association: NONE