Files
service-finder/frontend_admin/.nuxt/dist/server/_nuxt/leaderboard-CqTgptys.js.map.json
2026-07-01 02:27:38 +00:00

1 line
25 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"file":"leaderboard-CqTgptys.js","mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAsPA,UAAM,EAAE,EAAA,IAAM,QAAA;AAyBd,UAAM,EAAE,cAAc,eAAA,IAAmB,aAAA;AAEzC,UAAM,UAAU,IAAI,KAAK;AACzB,UAAM,QAAQ,IAAI,KAAK;AACvB,UAAM,UAAU,IAAwB,EAAE;AAC1C,UAAM,aAAa,IAAI,CAAC;AACxB,UAAM,SAAS,IAAI,CAAC;AACpB,UAAM,QAAQ,IAAI,EAAE;AACpB,UAAM,SAAS,IAAI,UAAU;AAC7B,UAAM,YAAY,IAAI,MAAM;AAE5B,UAAM,UAAU,IAAI;AAAA,MAClB,QAAQ;AAAA,MACR,OAAO;AAAA,MACP,QAAQ;AAAA,IAAA,CACT;AAED,aAAS,iBAAiB,OAAuB;AAC/C,cAAQ,OAAA;AAAA,QACN,KAAK;AAAG,iBAAO,EAAE,2CAA2C;AAAA,QAC5D,KAAK;AAAG,iBAAO,EAAE,+CAA+C;AAAA,QAChE,KAAK;AAAG,iBAAO,EAAE,6CAA6C;AAAA,QAC9D;AAAS,iBAAO,GAAG,KAAK,KAAK,EAAE,4CAA4C,CAAC;AAAA,MAAA;AAAA,IAEhF;AAEA,aAAS,YAAY,MAAsB;AACzC,aAAO,KACJ,MAAM,GAAG,EACT,IAAI,CAAA,SAAQ,KAAK,OAAO,CAAC,EAAE,YAAA,CAAa,EACxC,MAAM,GAAG,CAAC,EACV,KAAK,EAAE;AAAA,IACZ;AAEA,aAAS,kBAAkB,MAAsB;AAC/C,UAAI,SAAS,EAAG,QAAO;AACvB,UAAI,SAAS,EAAG,QAAO;AACvB,UAAI,SAAS,EAAG,QAAO;AACvB,aAAO;AAAA,IACT;AAEA,aAAS,YAAY,MAAsB;AACzC,UAAI,SAAS,EAAG,QAAO;AACvB,UAAI,SAAS,EAAG,QAAO;AACvB,UAAI,SAAS,EAAG,QAAO;AACvB,aAAO;AAAA,IACT;;;wHAzTmDA,MAAA,CAAA,EAAC,gCAAA,CAAA,uDACZA,MAAA,CAAA,EAAC,mCAAA,CAAA,kQAQ4DA,MAAA,CAAA,EAAC,iCAAA,CAAA,wCAG/EC,cAAA,SAAAD,MAAA,OAAA,EAAQ,MAAM,gBAEtBC,cAAA,eAAaD,MAAA,CAAA,EAAC,6CAAA,CAAA,uiBAYwEA,MAAA,CAAA,EAAC,gCAAA,CAAA,4LAC3EE,sBAAA,MAAA,QAAAF,MAAA,OAAA,EAAQ,KAAK,IAAbG,gBAAAH,MAAA,OAAA,EAAQ,OAAK,EAAA,IAAbI,cAAAJ,MAAA,OAAA,EAAQ,OAAK,EAAA,CAAA,IAAA,cAAA,qBACRA,MAAA,CAAA,EAAC,qCAAA,CAAA;AACCK,oBAAA,KAAP,QAAG;wBAAoBJ,cAAA,SAAO,GAAG,IAFjCC,sBAAA,MAAA,QAAAF,MAAA,OAAA,EAAQ,KAAK,oBAAbA,MAAA,OAAA,EAAQ,OAEsB,GAAG,kBAFjCA,MAAA,OAAA,EAAQ,OAEsB,GAAG,CAAA,IAAA,cAAA,qBAAK,GAAG,qBAAQA,MAAA,CAAA,EAAC,uCAAA,CAAA;;kJAMwBA,MAAA,CAAA,EAAC,iCAAA,CAAA,kBACrEC,cAAA,SAAAD,MAAA,OAAA,EAAQ,MAAM,qTAKsDA,MAAA,CAAA,EAAC,kCAAA,CAAA,wOAC3EA,MAAA,MAAA,CAAM,oBAANA,MAAA,MAAA,GAAM,UAAA,kBAANA,MAAA,MAAA,GAAM,UAAA,CAAA,IAAA,cAAA,qBACOA,MAAA,CAAA,EAAC,kCAAA,CAAA,6EADdA,MAAA,MAAA,CAAM,oBAANA,MAAA,MAAA,GAAM,cAAA,kBAANA,MAAA,MAAA,GAAM,cAAA,CAAA,IAAA,cAAA,qBAEWA,MAAA,CAAA,EAAC,sCAAA,CAAA,8EAFlBA,MAAA,MAAA,CAAM,oBAANA,MAAA,MAAA,GAAM,eAAA,kBAANA,MAAA,MAAA,GAAM,eAAA,CAAA,IAAA,cAAA,qBAGYA,MAAA,CAAA,EAAC,sCAAA,CAAA,8EAHnBA,MAAA,MAAA,CAAM,oBAANA,MAAA,MAAA,GAAM,eAAA,kBAANA,MAAA,MAAA,GAAM,eAAA,CAAA,IAAA,cAAA,qBAIYA,MAAA,CAAA,EAAC,qCAAA,CAAA,8OAQjCA,MAAA,CAAA,EAAC,iCAAA,CAAA,+IAGDA,MAAA,CAAA,EAAC,gCAAA,CAAA;UAIUA,MAAA,SAAA,MAAS,QAAA;;;;;qCAGpBA,MAAA,SAAA,MAAS,SAAcA,MAAA,CAAA,EAAC,+BAAA,IAAoCA,MAAA,CAAA,EAAC,8BAAA,CAAA,yEAElBA,MAAA,UAAA,CAAU,oBAAOA,MAAA,CAAA,EAAC,sCAAA,CAAA;UAK3DA,MAAA,OAAA,GAAO;yaAKLA,MAAA,CAAA,EAAC,kCAAA,CAAA,CAAA,qBAAA;AAAA,iBAKEA,MAAA,KAAA,GAAK;mIACSA,MAAA,CAAA,EAAC,qCAAA,CAAA,gGACyEA,MAAA,CAAA,EAAC,gCAAA,CAAA;MAIzF,WAAAA,MAAA,OAAA,EAAQ,WAAM,GAAA;6IACmBA,MAAA,CAAA,EAAC,gCAAA,CAAA,CAAA,YAAA;AAAA;gZAUuDA,MAAA,CAAA,EAAC,mCAAA,CAAA,oHACDA,MAAA,CAAA,EAAC,oCAAA,CAAA,oHACDA,MAAA,CAAA,EAAC,iCAAA,CAAA,oHACDA,MAAA,CAAA,EAAC,oCAAA,CAAA,oHACDA,MAAA,CAAA,EAAC,qCAAA,CAAA,oHACDA,MAAA,CAAA,EAAC,qCAAA,CAAA,oHACDA,MAAA,CAAA,EAAC,sCAAA,CAAA,oHACDA,MAAA,CAAA,EAAC,0CAAA,CAAA,oHACDA,MAAA,CAAA,EAAC,0CAAA,CAAA,oHACDA,MAAA,CAAA,EAAC,uCAAA,CAAA,oHACDA,MAAA,CAAA,EAAC,sCAAA,CAAA,qHACAA,MAAA,CAAA,EAAC,sCAAA,CAAA;AAKjFK,sBAAAL,MAAA,OAAA,IAAT,UAAK;8CAGJ,YAAY,MAAM,IAAI,GAAA,kCAAA,CAAA,yDAIyE,kBAAkB,MAAM,IAAI,GAAA,gFAAA,CAAA,MAC5HM,eAAA,MAAM,IAAI,wPAQR,YAAY,MAAM,SAAS,CAAA,uDAEgBA,eAAA,MAAM,SAAS,6EAKTA,eAAA,MAAM,cAAU,GAAA,+kBAQpBN,MAAA,YAAA,EAAa,MAAM,QAAQ,CAAA,+LAO1EM,eAAA,MAAM,aAAa,4EAKsBN,MAAA,YAAA,EAAa,MAAM,YAAY,CAAA,qEAG/BA,MAAA,YAAA,EAAa,MAAM,aAAa,CAAA;AAIlE,cAAA,MAAM,iBAAc,GAAA;AAC1B,kBAAA,kJAAAM,eAAA,MAAM,cAAc,CAAA,SAAA;AAAA;;;;AAOd,cAAA,MAAM,oBAAiB,GAAA;oLAC9B,iBAAiB,MAAM,iBAAiB,CAAA,CAAA,SAAA;AAAA;;;qEAMCA,eAAA,MAAM,iBAAiB,sDAGvBA,eAAA,MAAM,kBAAkB,sDAGxBA,eAAA,MAAM,aAAaN,sBAAe,MAAM,UAAU,IAAA,GAAA;;YAIrF,IAAE,uBAAyB,MAAM,OAAO;AAAA,YAAI,OAAM;AAAA,UAAA;6BAA7D,CAKW,GAAAO,QAAAC,UAAA,aAAA;;yCAJNR,MAAA,CAAA,EAAC,kCAAA,CAAA;;;kBAADS,gBAAAC,gBAAAV,MAAA,CAAA,yCAAwC,KAC3C,CAAA;AAAA,gCAAAW,YAEM,OAAA;AAAA,oBAFD,OAAM;AAAA,oBAAc,MAAK;AAAA,oBAAO,QAAO;AAAA,oBAAe,SAAQ;AAAA,kBAAA;oBACjEC,YAAyF,QAAA;AAAA,sBAAnF,kBAAe;AAAA,sBAAQ,mBAAgB;AAAA,sBAAQ,gBAAa;AAAA,sBAAI,GAAE;AAAA,oBAAA;;;;;;;;;oMAY5CZ,MAAA,MAAA,MAAM,CAAA,IAAA,cAAA,6BAAoEA,MAAA,MAAA,MAAM,IAAA,sCAAA,qCAAA,6CAAA,CAAA,4OAKjFA,MAAA,MAAA,MAAM,CAAA,IAAA,cAAA,6BAAkEA,MAAA,MAAA,MAAM,IAAA,sCAAA,qCAAA,2CAAA,CAAA,qBAC9GA,MAAA,CAAA,EAAC,+BAAA,CAAA,4GAIkCA,MAAA,MAAA,IAAM,CAAA,KAAWM,eAAA,KAAK,IAAIN,gBAASA,MAAA,KAAA,GAAOA,MAAA,UAAA,CAAU,CAAA,sBAAUA,MAAA,UAAA,CAAU,iLAErGC,cAAA,SAAO,EAAE,wCADID,MAAA,KAAA,CAAK,IAALG,gBAAAH,MAAA,KAAA,GACN,EAAE,IADII,cAAAJ,MAAA,KAAA,GACN,EAAE,CAAA,IAAA,cAAA,0BAAUA,MAAA,CAAA,EAAC,+BAAA,CAAA,oBACpBC,cAAA,SAAO,EAAE,wCAFID,MAAA,KAAA,CAAK,IAALG,gBAAAH,MAAA,KAAA,GAEN,EAAE,IAFII,cAAAJ,MAAA,KAAA,GAEN,EAAE,CAAA,IAAA,cAAA,0BAAUA,MAAA,CAAA,EAAC,+BAAA,CAAA,oBACpBC,cAAA,SAAO,GAAG,wCAHGD,MAAA,KAAA,CAAK,IAALG,gBAAAH,MAAA,KAAA,GAGN,GAAG,IAHGI,cAAAJ,MAAA,KAAA,GAGN,GAAG,CAAA,IAAA,cAAA,2BAAWA,MAAA,CAAA,EAAC,+BAAA,CAAA,oBACtBC,cAAA,SAAO,GAAG,wCAJGD,MAAA,KAAA,CAAK,IAALG,gBAAAH,MAAA,KAAA,GAIN,GAAG,IAJGI,cAAAJ,MAAA,KAAA,GAIN,GAAG,CAAA,IAAA,cAAA,2BAAWA,MAAA,CAAA,EAAC,+BAAA,CAAA,8FAIIA,MAAA,MAAA,IAASA,MAAA,KAAA,KAASA,MAAA,UAAA,CAAU,IAAA,cAAA,6BAA4DA,MAAA,MAAA,IAASA,MAAA,KAAA,KAASA,MAAA,UAAA,IAAU,sCAAA,qCAAA,2CAAA,CAAA,qBACpJA,MAAA,CAAA,EAAC,+BAAA,CAAA,0CAE+BA,MAAA,MAAA,IAASA,MAAA,KAAA,KAASA,MAAA,UAAA,CAAU,IAAA,cAAA,6BAA8DA,MAAA,MAAA,IAASA,MAAA,KAAA,KAASA,MAAA,UAAA,IAAU,sCAAA,qCAAA,6CAAA,CAAA;;;;;;;;;;;;","names":["_unref","_ssrRenderAttr","_ssrIncludeBooleanAttr","_ssrLooseContain","_ssrLooseEqual","_ssrRenderList","_ssrInterpolate","_push","_parent","_createTextVNode","_toDisplayString","_createBlock","_createVNode"],"sources":["../../../../pages/gamification/leaderboard.vue"],"sourcesContent":["<template>\n <div>\n <!-- Page Header -->\n <div class=\"mb-8\">\n <h1 class=\"text-2xl font-bold text-white\">{{ t('gamification.leaderboard.title') }}</h1>\n <p class=\"text-slate-400 mt-1\">{{ t('gamification.leaderboard.subtitle') }}</p>\n </div>\n\n <!-- Filters -->\n <div class=\"bg-slate-800 rounded-xl border border-slate-700 p-4 mb-6\">\n <div class=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4\">\n <!-- Search -->\n <div>\n <label class=\"block text-xs font-medium text-slate-400 mb-1.5 uppercase tracking-wider\">{{ t('gamification.leaderboard.search') }}</label>\n <div class=\"relative\">\n <input\n v-model=\"filters.search\"\n type=\"text\"\n :placeholder=\"t('gamification.leaderboard.search_placeholder')\"\n class=\"w-full bg-slate-700 border border-slate-600 rounded-lg pl-9 pr-3 py-2 text-sm text-white placeholder-slate-500 focus:ring-2 focus:ring-indigo-500 focus:border-transparent\"\n @keyup.enter=\"applyFilters\"\n />\n <svg class=\"absolute left-2.5 top-2.5 w-4 h-4 text-slate-500\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z\" />\n </svg>\n </div>\n </div>\n\n <!-- Level Filter -->\n <div>\n <label class=\"block text-xs font-medium text-slate-400 mb-1.5 uppercase tracking-wider\">{{ t('gamification.leaderboard.level') }}</label>\n <select v-model=\"filters.level\" class=\"w-full bg-slate-700 border border-slate-600 rounded-lg px-3 py-2 text-sm text-white focus:ring-2 focus:ring-indigo-500 focus:border-transparent\">\n <option value=\"\">{{ t('gamification.leaderboard.all_levels') }}</option>\n <option v-for=\"lvl in 20\" :key=\"lvl\" :value=\"lvl\">{{ lvl }}. {{ t('gamification.leaderboard.level_suffix') }}</option>\n </select>\n </div>\n\n <!-- Min XP -->\n <div>\n <label class=\"block text-xs font-medium text-slate-400 mb-1.5 uppercase tracking-wider\">{{ t('gamification.leaderboard.min_xp') }}</label>\n <input v-model.number=\"filters.min_xp\" type=\"number\" min=\"0\" placeholder=\"0\" class=\"w-full bg-slate-700 border border-slate-600 rounded-lg px-3 py-2 text-sm text-white focus:ring-2 focus:ring-indigo-500 focus:border-transparent\" />\n </div>\n\n <!-- Sort By -->\n <div>\n <label class=\"block text-xs font-medium text-slate-400 mb-1.5 uppercase tracking-wider\">{{ t('gamification.leaderboard.sort_by') }}</label>\n <select v-model=\"sortBy\" class=\"w-full bg-slate-700 border border-slate-600 rounded-lg px-3 py-2 text-sm text-white focus:ring-2 focus:ring-indigo-500 focus:border-transparent\">\n <option value=\"total_xp\">{{ t('gamification.leaderboard.sort_xp') }}</option>\n <option value=\"total_points\">{{ t('gamification.leaderboard.sort_points') }}</option>\n <option value=\"social_points\">{{ t('gamification.leaderboard.sort_social') }}</option>\n <option value=\"current_level\">{{ t('gamification.leaderboard.sort_level') }}</option>\n </select>\n </div>\n </div>\n\n <!-- Action Buttons -->\n <div class=\"flex items-center gap-3 mt-4 pt-4 border-t border-slate-700/50\">\n <button @click=\"applyFilters\" class=\"px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-white text-sm font-medium rounded-lg transition\">\n {{ t('gamification.leaderboard.filter') }}\n </button>\n <button @click=\"resetFilters\" class=\"px-4 py-2 bg-slate-700 hover:bg-slate-600 text-slate-300 text-sm font-medium rounded-lg transition\">\n {{ t('gamification.leaderboard.reset') }}\n </button>\n <button @click=\"toggleSortOrder\" class=\"px-3 py-2 bg-slate-700 hover:bg-slate-600 text-slate-300 text-sm font-medium rounded-lg transition flex items-center gap-1.5\">\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path v-if=\"sortOrder === 'desc'\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M3 4h13M3 8h9m-9 4h6m4 0l4-4m0 0l4 4m-4-4v12\" />\n <path v-else stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M3 4h13M3 8h9m-9 4h9m5-4v12m0 0l-4-4m4 4l4-4\" />\n </svg>\n {{ sortOrder === 'desc' ? t('gamification.leaderboard.desc') : t('gamification.leaderboard.asc') }}\n </button>\n <span class=\"text-xs text-slate-500 ml-auto\">{{ totalCount }} {{ t('gamification.leaderboard.users_count') }}</span>\n </div>\n </div>\n\n <!-- Loading State -->\n <div v-if=\"loading\" class=\"flex items-center justify-center py-20\">\n <div class=\"text-slate-400 flex items-center gap-3\">\n <svg class=\"w-5 h-5 animate-spin\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15\" />\n </svg>\n <span>{{ t('gamification.leaderboard.loading') }}</span>\n </div>\n </div>\n\n <!-- Error State -->\n <div v-else-if=\"error\" class=\"bg-rose-500/10 border border-rose-500/30 rounded-xl p-6 mb-8\">\n <p class=\"text-rose-400\">{{ t('gamification.leaderboard.load_error') }}</p>\n <button @click=\"fetchLeaderboard\" class=\"mt-3 text-sm text-rose-300 hover:text-rose-200 underline\">{{ t('gamification.leaderboard.retry') }}</button>\n </div>\n\n <!-- Empty State -->\n <div v-else-if=\"entries.length === 0\" class=\"bg-slate-800 rounded-xl border border-slate-700 p-6\">\n <p class=\"text-slate-400 text-center py-12\">{{ t('gamification.leaderboard.empty') }}</p>\n </div>\n\n <!-- Leaderboard Table -->\n <div v-else class=\"bg-slate-800 rounded-xl border border-slate-700 overflow-hidden\">\n <div class=\"overflow-x-auto\">\n <table class=\"w-full\">\n <thead>\n <tr class=\"border-b border-slate-700\">\n <th class=\"text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider w-16\">#</th>\n <th class=\"text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider\">{{ t('gamification.leaderboard.col_user') }}</th>\n <th class=\"text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider\">{{ t('gamification.leaderboard.col_email') }}</th>\n <th class=\"text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider\">{{ t('gamification.leaderboard.col_xp') }}</th>\n <th class=\"text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider\">{{ t('gamification.leaderboard.col_level') }}</th>\n <th class=\"text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider\">{{ t('gamification.leaderboard.col_points') }}</th>\n <th class=\"text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider\">{{ t('gamification.leaderboard.col_social') }}</th>\n <th class=\"text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider\">{{ t('gamification.leaderboard.col_penalty') }}</th>\n <th class=\"text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider\">{{ t('gamification.leaderboard.col_restriction') }}</th>\n <th class=\"text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider\">{{ t('gamification.leaderboard.col_discoveries') }}</th>\n <th class=\"text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider\">{{ t('gamification.leaderboard.col_services') }}</th>\n <th class=\"text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider\">{{ t('gamification.leaderboard.col_updated') }}</th>\n <th class=\"text-right px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider\">{{ t('gamification.leaderboard.col_actions') }}</th>\n </tr>\n </thead>\n <tbody class=\"divide-y divide-slate-700/50\">\n <tr\n v-for=\"entry in entries\"\n :key=\"entry.user_id\"\n class=\"hover:bg-slate-700/30 transition\"\n :class=\"getRowClass(entry.rank)\"\n >\n <!-- Rank -->\n <td class=\"px-4 py-3\">\n <span class=\"inline-flex items-center justify-center w-8 h-8 rounded-full text-sm font-bold\" :class=\"getRankBadgeClass(entry.rank)\">\n {{ entry.rank }}\n </span>\n </td>\n\n <!-- User Name -->\n <td class=\"px-4 py-3\">\n <div class=\"flex items-center gap-2\">\n <div class=\"w-8 h-8 rounded-full bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center text-xs font-bold text-white flex-shrink-0\">\n {{ getInitials(entry.user_name) }}\n </div>\n <span class=\"text-sm font-medium text-white\">{{ entry.user_name }}</span>\n </div>\n </td>\n\n <!-- Email -->\n <td class=\"px-4 py-3 text-sm text-slate-400 font-mono\">{{ entry.user_email || '—' }}</td>\n\n <!-- XP -->\n <td class=\"px-4 py-3\">\n <div class=\"flex items-center gap-1.5\">\n <svg class=\"w-4 h-4 text-amber-400\" fill=\"currentColor\" viewBox=\"0 0 20 20\">\n <path d=\"M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z\" />\n </svg>\n <span class=\"text-sm font-semibold text-white\">{{ formatNumber(entry.total_xp) }}</span>\n </div>\n </td>\n\n <!-- Level -->\n <td class=\"px-4 py-3\">\n <span class=\"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-indigo-500/10 text-indigo-400 border border-indigo-500/20\">\n {{ entry.current_level }}\n </span>\n </td>\n\n <!-- Points -->\n <td class=\"px-4 py-3 text-sm text-slate-300\">{{ formatNumber(entry.total_points) }}</td>\n\n <!-- Social Points -->\n <td class=\"px-4 py-3 text-sm text-slate-300\">{{ formatNumber(entry.social_points) }}</td>\n\n <!-- Penalty -->\n <td class=\"px-4 py-3\">\n <span v-if=\"entry.penalty_points > 0\" class=\"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-rose-500/10 text-rose-400 border border-rose-500/20\">\n -{{ entry.penalty_points }}\n </span>\n <span v-else class=\"text-sm text-slate-500\">—</span>\n </td>\n\n <!-- Restriction -->\n <td class=\"px-4 py-3\">\n <span v-if=\"entry.restriction_level > 0\" class=\"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-amber-500/10 text-amber-400 border border-amber-500/20\">\n {{ restrictionLabel(entry.restriction_level) }}\n </span>\n <span v-else class=\"text-sm text-slate-500\">—</span>\n </td>\n\n <!-- Discoveries -->\n <td class=\"px-4 py-3 text-sm text-slate-300\">{{ entry.places_discovered }}</td>\n\n <!-- Services -->\n <td class=\"px-4 py-3 text-sm text-slate-300\">{{ entry.services_submitted }}</td>\n\n <!-- Updated At -->\n <td class=\"px-4 py-3 text-sm text-slate-500\">{{ entry.updated_at ? formatDateTime(entry.updated_at) : '—' }}</td>\n\n <!-- Actions -->\n <td class=\"px-4 py-3 text-right\">\n <NuxtLink :to=\"`/gamification/users/${entry.user_id}`\" class=\"inline-flex items-center gap-1.5 px-3 py-1.5 bg-indigo-600/10 hover:bg-indigo-600/20 text-indigo-400 text-xs font-medium rounded-lg transition\">\n {{ t('gamification.leaderboard.details') }}\n <svg class=\"w-3.5 h-3.5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9 5l7 7-7 7\" />\n </svg>\n </NuxtLink>\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n\n <!-- Pagination -->\n <div class=\"flex items-center justify-between px-4 py-3 border-t border-slate-700\">\n <div class=\"flex items-center gap-2\">\n <button @click=\"firstPage\" :disabled=\"offset === 0\" class=\"px-2.5 py-1.5 text-sm rounded-lg transition\" :class=\"offset === 0 ? 'text-slate-600 cursor-not-allowed' : 'text-slate-300 hover:bg-slate-700'\">\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M11 19l-7-7 7-7m8 14l-7-7 7-7\" />\n </svg>\n </button>\n <button @click=\"prevPage\" :disabled=\"offset === 0\" class=\"px-3 py-1.5 text-sm rounded-lg transition\" :class=\"offset === 0 ? 'text-slate-600 cursor-not-allowed' : 'text-slate-300 hover:bg-slate-700'\">\n {{ t('gamification.leaderboard.prev') }}\n </button>\n </div>\n <div class=\"flex items-center gap-3\">\n <span class=\"text-sm text-slate-500\">{{ offset + 1 }}{{ Math.min(offset + limit, totalCount) }} / {{ totalCount }}</span>\n <select v-model.number=\"limit\" @change=\"onLimitChange\" class=\"bg-slate-700 border border-slate-600 rounded-lg px-2 py-1.5 text-xs text-slate-300 focus:ring-2 focus:ring-indigo-500 focus:border-transparent\">\n <option :value=\"25\">25 / {{ t('gamification.leaderboard.page') }}</option>\n <option :value=\"50\">50 / {{ t('gamification.leaderboard.page') }}</option>\n <option :value=\"100\">100 / {{ t('gamification.leaderboard.page') }}</option>\n <option :value=\"250\">250 / {{ t('gamification.leaderboard.page') }}</option>\n </select>\n </div>\n <div class=\"flex items-center gap-2\">\n <button @click=\"nextPage\" :disabled=\"offset + limit >= totalCount\" class=\"px-3 py-1.5 text-sm rounded-lg transition\" :class=\"offset + limit >= totalCount ? 'text-slate-600 cursor-not-allowed' : 'text-slate-300 hover:bg-slate-700'\">\n {{ t('gamification.leaderboard.next') }}\n </button>\n <button @click=\"lastPage\" :disabled=\"offset + limit >= totalCount\" class=\"px-2.5 py-1.5 text-sm rounded-lg transition\" :class=\"offset + limit >= totalCount ? 'text-slate-600 cursor-not-allowed' : 'text-slate-300 hover:bg-slate-700'\">\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M13 5l7 7-7 7M5 5l7 7-7 7\" />\n </svg>\n </button>\n </div>\n </div>\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\ndefinePageMeta({\n middleware: 'auth',\n})\n\nconst { t } = useI18n()\n\ninterface LeaderboardEntry {\n rank: number\n user_id: number\n user_name: string\n user_email: string | null\n total_xp: number\n total_points: number\n social_points: number\n current_level: number\n penalty_points: number\n restriction_level: number\n places_discovered: number\n services_submitted: number\n updated_at: string | null\n}\n\ninterface LeaderboardResponse {\n total: number\n offset: number\n limit: number\n entries: LeaderboardEntry[]\n}\n\nconst { formatNumber, formatDateTime } = useFormatter()\n\nconst loading = ref(false)\nconst error = ref(false)\nconst entries = ref<LeaderboardEntry[]>([])\nconst totalCount = ref(0)\nconst offset = ref(0)\nconst limit = ref(50)\nconst sortBy = ref('total_xp')\nconst sortOrder = ref('desc')\n\nconst filters = ref({\n search: '',\n level: '' as number | string,\n min_xp: null as number | null,\n})\n\nfunction restrictionLabel(level: number): string {\n switch (level) {\n case 1: return t('gamification.leaderboard.restriction_mild')\n case 2: return t('gamification.leaderboard.restriction_moderate')\n case 3: return t('gamification.leaderboard.restriction_severe')\n default: return `${level}. ${t('gamification.leaderboard.restriction_level')}`\n }\n}\n\nfunction getInitials(name: string): string {\n return name\n .split(' ')\n .map(part => part.charAt(0).toUpperCase())\n .slice(0, 2)\n .join('')\n}\n\nfunction getRankBadgeClass(rank: number): string {\n if (rank === 1) return 'bg-amber-500/20 text-amber-400 border border-amber-500/30'\n if (rank === 2) return 'bg-slate-300/10 text-slate-300 border border-slate-400/20'\n if (rank === 3) return 'bg-amber-700/20 text-amber-600 border border-amber-700/30'\n return 'bg-slate-700/50 text-slate-400 border border-slate-600/30'\n}\n\nfunction getRowClass(rank: number): string {\n if (rank === 1) return 'bg-amber-500/5'\n if (rank === 2) return 'bg-slate-300/5'\n if (rank === 3) return 'bg-amber-700/5'\n return ''\n}\n\nfunction toggleSortOrder() {\n sortOrder.value = sortOrder.value === 'desc' ? 'asc' : 'desc'\n applyFilters()\n}\n\nfunction buildQueryParams(): string {\n const params = new URLSearchParams()\n params.set('limit', String(limit.value))\n params.set('offset', String(offset.value))\n params.set('sort_by', sortBy.value)\n params.set('sort_order', sortOrder.value)\n if (filters.value.level !== '') params.set('level', String(filters.value.level))\n if (filters.value.min_xp !== null && filters.value.min_xp > 0) params.set('min_xp', String(filters.value.min_xp))\n if (filters.value.search && filters.value.search.length >= 2) params.set('search', filters.value.search.trim())\n return params.toString()\n}\n\nasync function fetchLeaderboard() {\n loading.value = true\n error.value = false\n try {\n const tokenCookie = useCookie('access_token')\n const token = tokenCookie.value\n const headers = token ? { Authorization: `Bearer ${token}` } : {}\n\n const data = await $fetch<LeaderboardResponse>(`/api/v1/admin/gamification/leaderboard?${buildQueryParams()}`, { headers })\n entries.value = data.entries || []\n totalCount.value = data.total || 0\n } catch (e) {\n console.error('Failed to load leaderboard:', e)\n error.value = true\n } finally {\n loading.value = false\n }\n}\n\nfunction applyFilters() {\n offset.value = 0\n fetchLeaderboard()\n}\n\nfunction resetFilters() {\n filters.value = { search: '', level: '', min_xp: null }\n sortBy.value = 'total_xp'\n sortOrder.value = 'desc'\n offset.value = 0\n fetchLeaderboard()\n}\n\nfunction onLimitChange() {\n offset.value = 0\n fetchLeaderboard()\n}\n\nfunction firstPage() {\n offset.value = 0\n fetchLeaderboard()\n}\n\nfunction prevPage() {\n if (offset.value > 0) {\n offset.value = Math.max(0, offset.value - limit.value)\n fetchLeaderboard()\n }\n}\n\nfunction nextPage() {\n offset.value += limit.value\n fetchLeaderboard()\n}\n\nfunction lastPage() {\n const lastOffset = Math.max(0, Math.floor((totalCount.value - 1) / limit.value) * limit.value)\n offset.value = lastOffset\n fetchLeaderboard()\n}\n\nonMounted(() => {\n fetchLeaderboard()\n})\n</script>\n"],"version":3}