1 line
25 KiB
JSON
1 line
25 KiB
JSON
{"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} |