fix: перенёс уровни в бд и пофиксид их отображение на фронте
Backend CI / build-and-test (push) Successful in 52s
Frontend CI / build-and-check (push) Failing after 5m15s
🚀 Create and publish a Docker image / Detect changes in backend and frontend (push) Successful in 16s
🚀 Create and publish a Docker image / Build & publish backend image (push) Successful in 1m0s
🚀 Create and publish a Docker image / Build & publish frontend image (push) Successful in 32s
🚀 Create and publish a Docker image / Update stack on Portainer (push) Successful in 13s

This commit is contained in:
2026-05-18 02:28:05 +03:00
parent 302e01d705
commit 811b6ef51a
27 changed files with 1526 additions and 51 deletions
+2
View File
@@ -45,6 +45,8 @@ export function mapApiUser(user: UserAuthDto | UserDto, stats?: UserStatsDto): U
coins: stats?.coins ?? ('coins' in user ? user.coins : 0),
level: stats?.level ?? ('level' in user ? user.level : 1),
xp: stats?.xp ?? ('xp' in user ? user.xp : 0),
currentLevelXp: stats?.currentLevelXp ?? 0,
nextLevelXp: stats?.nextLevelXp,
lecturesAttended: stats?.attendedLectures ?? 0,
hoursLearned: stats ? Math.round(stats.attendedLectures * 1.5 * 10) / 10 : 0,
achievements: stats ? Array.from({ length: stats.achievementsCount }, (_, index) => String(index + 1)) : [],
+2
View File
@@ -60,6 +60,8 @@ export interface UserStatsDto {
coins: number
level: number
achievementsCount: number
currentLevelXp: number
nextLevelXp?: number | null
}
export interface LectureDto {
+3 -2
View File
@@ -4,6 +4,7 @@ defineProps<{
max?: number
label?: string
color?: string
text?: string
}>()
</script>
@@ -14,12 +15,12 @@ defineProps<{
<div
class="progress-fill"
:style="{
width: `${Math.min(100, (value / (max ?? 100)) * 100)}%`,
width: `${Math.max(0, Math.min(100, (value / (max && max > 0 ? max : 100)) * 100))}%`,
background: color ?? 'var(--gradient-progress-success)'
}"
/>
</div>
<div class="progress-text">{{ value }} / {{ max ?? 100 }}</div>
<div class="progress-text">{{ text ?? `${value} / ${max ?? 100}` }}</div>
</div>
</template>
+2
View File
@@ -36,6 +36,8 @@ export const useUserStore = defineStore('user', () => {
coins: stats.coins,
level: stats.level,
xp: stats.xp,
currentLevelXp: stats.currentLevelXp,
nextLevelXp: stats.nextLevelXp,
lecturesAttended: stats.attendedLectures,
hoursLearned: Math.round(stats.attendedLectures * 1.5 * 10) / 10,
achievements: Array.from({ length: stats.achievementsCount }, (_, index) => String(index + 1)),
+2
View File
@@ -14,6 +14,8 @@ export interface User {
coins: number
level: number
xp?: number
currentLevelXp?: number
nextLevelXp?: number | null
lecturesAttended?: number
hoursLearned?: number
achievements?: string[]
+22 -5
View File
@@ -33,8 +33,25 @@ const recommended = computed(() =>
)
const achievements = computed(() => userStore.achievements.filter(a => a.unlocked).slice(0, 3))
const reminders = computed(() => userStore.notifications.slice(0, 3))
const xpToNext = 200
const xpProgress = computed(() => user.value.xp ?? 120)
const currentLevelXp = computed(() => user.value.currentLevelXp ?? 0)
const nextLevelXp = computed(() => user.value.nextLevelXp)
const userXp = computed(() => user.value.xp ?? 0)
const hasLevelProgress = computed(() => nextLevelXp.value !== undefined)
const hasNextLevel = computed(() => typeof nextLevelXp.value === 'number' && nextLevelXp.value > currentLevelXp.value)
const levelProgressMax = computed(() => hasNextLevel.value ? nextLevelXp.value! - currentLevelXp.value : 1)
const levelProgress = computed(() => {
if (!hasLevelProgress.value) return 0
if (!hasNextLevel.value) return 1
return Math.min(Math.max(userXp.value - currentLevelXp.value, 0), levelProgressMax.value)
})
const levelProgressLabel = computed(() =>
!hasLevelProgress.value
? `Уровень ${user.value.level}`
: hasNextLevel.value ? `Прогресс до уровня ${user.value.level + 1}` : 'Максимальный уровень'
)
const levelProgressText = computed(() =>
hasNextLevel.value ? `${levelProgress.value} / ${levelProgressMax.value} XP` : `${userXp.value} XP`
)
onMounted(async () => {
await Promise.all([
@@ -99,10 +116,10 @@ onMounted(async () => {
<GlassCard>
<div class="xp-section">
<div class="xp-header">
<span class="xp-label">Прогресс до уровня {{ user.level + 1 }}</span>
<span class="xp-val">{{ xpProgress }} / {{ xpToNext }} XP</span>
<span class="xp-label">{{ levelProgressLabel }}</span>
<span class="xp-val">{{ levelProgressText }}</span>
</div>
<ProgressBar :value="xpProgress" :max="xpToNext" />
<ProgressBar :value="levelProgress" :max="levelProgressMax" :text="levelProgressText" />
</div>
</GlassCard>
+22 -3
View File
@@ -24,6 +24,25 @@ const userYearLine = computed(() => {
const year = user.value.year ?? 0
return Number.isFinite(year) && year > 0 ? `${year} курс` : ''
})
const currentLevelXp = computed(() => user.value.currentLevelXp ?? 0)
const nextLevelXp = computed(() => user.value.nextLevelXp)
const userXp = computed(() => user.value.xp ?? 0)
const hasLevelProgress = computed(() => nextLevelXp.value !== undefined)
const hasNextLevel = computed(() => typeof nextLevelXp.value === 'number' && nextLevelXp.value > currentLevelXp.value)
const levelProgressMax = computed(() => hasNextLevel.value ? nextLevelXp.value! - currentLevelXp.value : 1)
const levelProgress = computed(() => {
if (!hasLevelProgress.value) return 0
if (!hasNextLevel.value) return 1
return Math.min(Math.max(userXp.value - currentLevelXp.value, 0), levelProgressMax.value)
})
const levelProgressLabel = computed(() =>
!hasLevelProgress.value
? `Уровень ${user.value.level}`
: hasNextLevel.value ? `Уровень ${user.value.level}` : `Уровень ${user.value.level} · максимум`
)
const levelProgressText = computed(() =>
hasNextLevel.value ? `${levelProgress.value} / ${levelProgressMax.value} XP` : `${userXp.value} XP`
)
const interestTags = ref([
{ label: '#ML', active: true },
{ label: '#ИИ', active: true },
@@ -66,10 +85,10 @@ onMounted(() => {
</div>
<div class="level">
<div class="level-header">
<span>Уровень {{ user.level }}</span>
<span>{{ user.xp }} / 200 XP</span>
<span>{{ levelProgressLabel }}</span>
<span>{{ levelProgressText }}</span>
</div>
<ProgressBar :value="user.xp ?? 0" :max="200" />
<ProgressBar :value="levelProgress" :max="levelProgressMax" :text="levelProgressText" />
</div>
<div class="tags">
<div class="section-title">Интересы</div>