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
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:
@@ -38,6 +38,3 @@ EMAIL_SMTP_USERNAME=
|
|||||||
EMAIL_SMTP_PASSWORD=
|
EMAIL_SMTP_PASSWORD=
|
||||||
EMAIL_SMTP_FROM_ADDRESS=no-reply@universe.local
|
EMAIL_SMTP_FROM_ADDRESS=no-reply@universe.local
|
||||||
EMAIL_SMTP_FROM_NAME=UniVerse
|
EMAIL_SMTP_FROM_NAME=UniVerse
|
||||||
|
|
||||||
# Gamification
|
|
||||||
GAMIFICATION_XP_THRESHOLDS=[0, 100, 300, 600, 1000, 1500, 2500, 4000]
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using UniVerse.Application.DTOs.Notifications;
|
using UniVerse.Application.DTOs.Notifications;
|
||||||
@@ -18,6 +17,7 @@ public class GamificationServiceTests
|
|||||||
public async Task CheckAndAwardAchievementsAsync_AwardsModernConditionsOnce()
|
public async Task CheckAndAwardAchievementsAsync_AwardsModernConditionsOnce()
|
||||||
{
|
{
|
||||||
await using var db = CreateDbContext();
|
await using var db = CreateDbContext();
|
||||||
|
SeedLevelThresholds(db);
|
||||||
var service = CreateService(db);
|
var service = CreateService(db);
|
||||||
|
|
||||||
db.Users.Add(new User
|
db.Users.Add(new User
|
||||||
@@ -78,6 +78,7 @@ public class GamificationServiceTests
|
|||||||
public async Task CheckAndAwardAchievementsAsync_CountsConsecutiveIsoWeeksAcrossYears()
|
public async Task CheckAndAwardAchievementsAsync_CountsConsecutiveIsoWeeksAcrossYears()
|
||||||
{
|
{
|
||||||
await using var db = CreateDbContext();
|
await using var db = CreateDbContext();
|
||||||
|
SeedLevelThresholds(db);
|
||||||
var service = CreateService(db);
|
var service = CreateService(db);
|
||||||
|
|
||||||
db.Users.Add(new User { Id = 1, Email = "student@test.local" });
|
db.Users.Add(new User { Id = 1, Email = "student@test.local" });
|
||||||
@@ -98,6 +99,38 @@ public class GamificationServiceTests
|
|||||||
Assert.True(await db.UserAchievements.AnyAsync(ua => ua.UserId == 1 && ua.AchievementId == 1001));
|
Assert.True(await db.UserAchievements.AnyAsync(ua => ua.UserId == 1 && ua.AchievementId == 1001));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0, 1)]
|
||||||
|
[InlineData(99, 1)]
|
||||||
|
[InlineData(100, 2)]
|
||||||
|
[InlineData(299, 2)]
|
||||||
|
[InlineData(300, 3)]
|
||||||
|
public async Task CalculateLevelAsync_UsesDatabaseThresholds(int xp, int expectedLevel)
|
||||||
|
{
|
||||||
|
await using var db = CreateDbContext();
|
||||||
|
SeedLevelThresholds(db);
|
||||||
|
var service = CreateService(db);
|
||||||
|
|
||||||
|
var level = await service.CalculateLevelAsync(xp);
|
||||||
|
|
||||||
|
Assert.Equal(expectedLevel, level);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(120, 100, 300)]
|
||||||
|
[InlineData(350, 300, null)]
|
||||||
|
public async Task GetLevelProgressAsync_ReturnsCurrentAndNextThresholds(int xp, int currentLevelXp, int? nextLevelXp)
|
||||||
|
{
|
||||||
|
await using var db = CreateDbContext();
|
||||||
|
SeedLevelThresholds(db);
|
||||||
|
var service = CreateService(db);
|
||||||
|
|
||||||
|
var progress = await service.GetLevelProgressAsync(xp);
|
||||||
|
|
||||||
|
Assert.Equal(currentLevelXp, progress.CurrentLevelXp);
|
||||||
|
Assert.Equal(nextLevelXp, progress.NextLevelXp);
|
||||||
|
}
|
||||||
|
|
||||||
private static AppDbContext CreateDbContext()
|
private static AppDbContext CreateDbContext()
|
||||||
{
|
{
|
||||||
var options = new DbContextOptionsBuilder<AppDbContext>()
|
var options = new DbContextOptionsBuilder<AppDbContext>()
|
||||||
@@ -119,16 +152,16 @@ public class GamificationServiceTests
|
|||||||
notifications.SendAsync(Arg.Any<NotificationMessage>(), Arg.Any<CancellationToken>())
|
notifications.SendAsync(Arg.Any<NotificationMessage>(), Arg.Any<CancellationToken>())
|
||||||
.Returns(Task.CompletedTask);
|
.Returns(Task.CompletedTask);
|
||||||
|
|
||||||
var configuration = new ConfigurationBuilder()
|
return new GamificationService(db, notifications, NullLogger<GamificationService>.Instance);
|
||||||
.AddInMemoryCollection(new Dictionary<string, string?>
|
}
|
||||||
{
|
|
||||||
["Gamification:XpThresholds:0"] = "0",
|
|
||||||
["Gamification:XpThresholds:1"] = "100",
|
|
||||||
["Gamification:XpThresholds:2"] = "300"
|
|
||||||
})
|
|
||||||
.Build();
|
|
||||||
|
|
||||||
return new GamificationService(db, configuration, notifications, NullLogger<GamificationService>.Instance);
|
private static void SeedLevelThresholds(AppDbContext db)
|
||||||
|
{
|
||||||
|
db.LevelThresholds.AddRange(
|
||||||
|
new LevelThreshold { Level = 1, RequiredXp = 0 },
|
||||||
|
new LevelThreshold { Level = 2, RequiredXp = 100 },
|
||||||
|
new LevelThreshold { Level = 3, RequiredXp = 300 });
|
||||||
|
db.SaveChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Achievement Achievement(int id, string name, string condition, int coinReward) => new()
|
private static Achievement Achievement(int id, string name, string condition, int coinReward) => new()
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ public class ApiWebApplicationFactory : WebApplicationFactory<Program>
|
|||||||
|
|
||||||
stub.GetByIdAsync(Arg.Any<int>()).Returns(userDto);
|
stub.GetByIdAsync(Arg.Any<int>()).Returns(userDto);
|
||||||
stub.UpdateProfileAsync(Arg.Any<int>(), Arg.Any<UpdateUserRequest>()).Returns(userDto);
|
stub.UpdateProfileAsync(Arg.Any<int>(), Arg.Any<UpdateUserRequest>()).Returns(userDto);
|
||||||
stub.GetStatsAsync(Arg.Any<int>()).Returns(new UserStatsDto(0, 0, 0, 0, 0, 1, 0));
|
stub.GetStatsAsync(Arg.Any<int>()).Returns(new UserStatsDto(0, 0, 0, 0, 0, 1, 0, 0, 100));
|
||||||
stub.GetAllAsync(Arg.Any<UserFilterRequest>()).Returns(pagedUsers);
|
stub.GetAllAsync(Arg.Any<UserFilterRequest>()).Returns(pagedUsers);
|
||||||
stub.SetRolesAsync(Arg.Any<int>(), Arg.Any<IReadOnlyCollection<UserRole>>()).Returns(Task.CompletedTask);
|
stub.SetRolesAsync(Arg.Any<int>(), Arg.Any<IReadOnlyCollection<UserRole>>()).Returns(Task.CompletedTask);
|
||||||
stub.SetActiveAsync(Arg.Any<int>(), Arg.Any<bool>()).Returns(Task.CompletedTask);
|
stub.SetActiveAsync(Arg.Any<int>(), Arg.Any<bool>()).Returns(Task.CompletedTask);
|
||||||
@@ -274,7 +274,8 @@ public class ApiWebApplicationFactory : WebApplicationFactory<Program>
|
|||||||
stub.AwardCoinsAsync(Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CoinTransactionType>(),
|
stub.AwardCoinsAsync(Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CoinTransactionType>(),
|
||||||
Arg.Any<int?>(), Arg.Any<int?>(), Arg.Any<string?>()).Returns(Task.CompletedTask);
|
Arg.Any<int?>(), Arg.Any<int?>(), Arg.Any<string?>()).Returns(Task.CompletedTask);
|
||||||
stub.CheckAndAwardAchievementsAsync(Arg.Any<int>()).Returns(Task.CompletedTask);
|
stub.CheckAndAwardAchievementsAsync(Arg.Any<int>()).Returns(Task.CompletedTask);
|
||||||
stub.CalculateLevel(Arg.Any<int>()).Returns(1);
|
stub.CalculateLevelAsync(Arg.Any<int>()).Returns(Task.FromResult(1));
|
||||||
|
stub.GetLevelProgressAsync(Arg.Any<int>()).Returns(Task.FromResult(new LevelProgressDto(0, 100)));
|
||||||
return stub;
|
return stub;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using NSubstitute;
|
||||||
|
using UniVerse.Application.DTOs.Notifications;
|
||||||
|
using UniVerse.Application.Interfaces;
|
||||||
|
using UniVerse.Domain.Entities;
|
||||||
|
using UniVerse.Infrastructure.Data;
|
||||||
|
using UniVerse.Infrastructure.Services;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace UniVerse.Api.Tests.Users;
|
||||||
|
|
||||||
|
public class UserServiceTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task GetStatsAsync_ReturnsLevelProgressThresholds()
|
||||||
|
{
|
||||||
|
await using var db = CreateDbContext();
|
||||||
|
SeedLevelThresholds(db);
|
||||||
|
db.Users.Add(new User { Id = 1, Email = "student@test.local", Xp = 120 });
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
var service = CreateService(db);
|
||||||
|
|
||||||
|
var stats = await service.GetStatsAsync(1);
|
||||||
|
|
||||||
|
Assert.Equal(2, stats.Level);
|
||||||
|
Assert.Equal(100, stats.CurrentLevelXp);
|
||||||
|
Assert.Equal(300, stats.NextLevelXp);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetStatsAsync_ReturnsNullNextLevelAtMaxConfiguredLevel()
|
||||||
|
{
|
||||||
|
await using var db = CreateDbContext();
|
||||||
|
SeedLevelThresholds(db);
|
||||||
|
db.Users.Add(new User { Id = 1, Email = "student@test.local", Xp = 350 });
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
var service = CreateService(db);
|
||||||
|
|
||||||
|
var stats = await service.GetStatsAsync(1);
|
||||||
|
|
||||||
|
Assert.Equal(3, stats.Level);
|
||||||
|
Assert.Equal(300, stats.CurrentLevelXp);
|
||||||
|
Assert.Null(stats.NextLevelXp);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AppDbContext CreateDbContext()
|
||||||
|
{
|
||||||
|
var options = new DbContextOptionsBuilder<AppDbContext>()
|
||||||
|
.UseInMemoryDatabase($"UserServiceTests_{Guid.NewGuid()}")
|
||||||
|
.Options;
|
||||||
|
return new AppDbContext(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static UserService CreateService(AppDbContext db)
|
||||||
|
{
|
||||||
|
var notifications = Substitute.For<INotificationService>();
|
||||||
|
notifications.CreateUserNotificationAsync(
|
||||||
|
Arg.Any<int>(),
|
||||||
|
Arg.Any<string>(),
|
||||||
|
Arg.Any<string>(),
|
||||||
|
Arg.Any<string>(),
|
||||||
|
Arg.Any<CancellationToken>())
|
||||||
|
.Returns(callInfo => new UserNotificationDto(1, callInfo.ArgAt<string>(1), callInfo.ArgAt<string>(2), callInfo.ArgAt<string>(3), false, DateTime.UtcNow));
|
||||||
|
notifications.SendAsync(Arg.Any<NotificationMessage>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
|
||||||
|
var gamification = new GamificationService(db, notifications, NullLogger<GamificationService>.Instance);
|
||||||
|
return new UserService(db, gamification);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SeedLevelThresholds(AppDbContext db)
|
||||||
|
{
|
||||||
|
db.LevelThresholds.AddRange(
|
||||||
|
new LevelThreshold { Level = 1, RequiredXp = 0 },
|
||||||
|
new LevelThreshold { Level = 2, RequiredXp = 100 },
|
||||||
|
new LevelThreshold { Level = 3, RequiredXp = 300 });
|
||||||
|
db.SaveChanges();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,9 +22,6 @@
|
|||||||
"BaseUrl": "https://schedule.rdcenter.ru",
|
"BaseUrl": "https://schedule.rdcenter.ru",
|
||||||
"ApiKey": ""
|
"ApiKey": ""
|
||||||
},
|
},
|
||||||
"Gamification": {
|
|
||||||
"XpThresholds": [0, 100, 300, 600, 1000, 1500, 2500, 4000]
|
|
||||||
},
|
|
||||||
"Serilog": {
|
"Serilog": {
|
||||||
"MinimumLevel": {
|
"MinimumLevel": {
|
||||||
"Default": "Information",
|
"Default": "Information",
|
||||||
|
|||||||
@@ -5624,6 +5624,15 @@
|
|||||||
"achievementsCount": {
|
"achievementsCount": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "int32"
|
"format": "int32"
|
||||||
|
},
|
||||||
|
"currentLevelXp": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
},
|
||||||
|
"nextLevelXp": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32",
|
||||||
|
"nullable": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
|||||||
@@ -11,3 +11,8 @@ public record CoinTransactionDto(
|
|||||||
string? Description,
|
string? Description,
|
||||||
DateTime CreatedAt
|
DateTime CreatedAt
|
||||||
);
|
);
|
||||||
|
|
||||||
|
public record LevelProgressDto(
|
||||||
|
int CurrentLevelXp,
|
||||||
|
int? NextLevelXp
|
||||||
|
);
|
||||||
|
|||||||
@@ -22,7 +22,9 @@ public record UserStatsDto(
|
|||||||
int Xp,
|
int Xp,
|
||||||
int Coins,
|
int Coins,
|
||||||
int Level,
|
int Level,
|
||||||
int AchievementsCount
|
int AchievementsCount,
|
||||||
|
int CurrentLevelXp,
|
||||||
|
int? NextLevelXp
|
||||||
);
|
);
|
||||||
|
|
||||||
public record UpdateUserRequest(
|
public record UpdateUserRequest(
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ public interface IGamificationService
|
|||||||
Task AwardCoinsAsync(int userId, int amount, CoinTransactionType type,
|
Task AwardCoinsAsync(int userId, int amount, CoinTransactionType type,
|
||||||
int? reviewId = null, int? achievementId = null, string? description = null);
|
int? reviewId = null, int? achievementId = null, string? description = null);
|
||||||
Task CheckAndAwardAchievementsAsync(int userId);
|
Task CheckAndAwardAchievementsAsync(int userId);
|
||||||
int CalculateLevel(int xp);
|
Task<int> CalculateLevelAsync(int xp);
|
||||||
|
Task<LevelProgressDto> GetLevelProgressAsync(int xp);
|
||||||
Task<List<UserAchievementDto>> GetUserAchievementsAsync(int userId);
|
Task<List<UserAchievementDto>> GetUserAchievementsAsync(int userId);
|
||||||
Task<PagedResult<CoinTransactionDto>> GetTransactionsAsync(int userId, PaginationRequest pagination);
|
Task<PagedResult<CoinTransactionDto>> GetTransactionsAsync(int userId, PaginationRequest pagination);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace UniVerse.Domain.Entities;
|
||||||
|
|
||||||
|
public class LevelThreshold
|
||||||
|
{
|
||||||
|
public int Level { get; set; }
|
||||||
|
public int RequiredXp { get; set; }
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ public class AppDbContext : DbContext
|
|||||||
public DbSet<Achievement> Achievements { get; set; } = null!;
|
public DbSet<Achievement> Achievements { get; set; } = null!;
|
||||||
public DbSet<UserAchievement> UserAchievements { get; set; } = null!;
|
public DbSet<UserAchievement> UserAchievements { get; set; } = null!;
|
||||||
public DbSet<CoinTransaction> CoinTransactions { get; set; } = null!;
|
public DbSet<CoinTransaction> CoinTransactions { get; set; } = null!;
|
||||||
|
public DbSet<LevelThreshold> LevelThresholds { get; set; } = null!;
|
||||||
public DbSet<UserNotification> UserNotifications { get; set; } = null!;
|
public DbSet<UserNotification> UserNotifications { get; set; } = null!;
|
||||||
public DbSet<RefreshToken> RefreshTokens { get; set; } = null!;
|
public DbSet<RefreshToken> RefreshTokens { get; set; } = null!;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
using UniVerse.Domain.Entities;
|
||||||
|
|
||||||
|
namespace UniVerse.Infrastructure.Data.Configurations;
|
||||||
|
|
||||||
|
public class LevelThresholdConfiguration : IEntityTypeConfiguration<LevelThreshold>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<LevelThreshold> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("level_thresholds", table =>
|
||||||
|
{
|
||||||
|
table.HasCheckConstraint("CK_level_thresholds_level_positive", "level > 0");
|
||||||
|
table.HasCheckConstraint("CK_level_thresholds_required_xp_non_negative", "required_xp >= 0");
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.HasKey(t => t.Level);
|
||||||
|
builder.Property(t => t.Level).HasColumnName("level").ValueGeneratedNever();
|
||||||
|
builder.Property(t => t.RequiredXp).HasColumnName("required_xp").IsRequired();
|
||||||
|
builder.HasIndex(t => t.RequiredXp).IsUnique();
|
||||||
|
|
||||||
|
builder.HasData(
|
||||||
|
new LevelThreshold { Level = 1, RequiredXp = 0 },
|
||||||
|
new LevelThreshold { Level = 2, RequiredXp = 100 },
|
||||||
|
new LevelThreshold { Level = 3, RequiredXp = 300 },
|
||||||
|
new LevelThreshold { Level = 4, RequiredXp = 600 },
|
||||||
|
new LevelThreshold { Level = 5, RequiredXp = 1000 },
|
||||||
|
new LevelThreshold { Level = 6, RequiredXp = 1500 },
|
||||||
|
new LevelThreshold { Level = 7, RequiredXp = 2500 },
|
||||||
|
new LevelThreshold { Level = 8, RequiredXp = 4000 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+1107
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,58 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
|
||||||
|
|
||||||
|
namespace UniVerse.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class LevelThresholds : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "level_thresholds",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
level = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
required_xp = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_level_thresholds", x => x.level);
|
||||||
|
table.CheckConstraint("CK_level_thresholds_level_positive", "level > 0");
|
||||||
|
table.CheckConstraint("CK_level_thresholds_required_xp_non_negative", "required_xp >= 0");
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.InsertData(
|
||||||
|
table: "level_thresholds",
|
||||||
|
columns: new[] { "level", "required_xp" },
|
||||||
|
values: new object[,]
|
||||||
|
{
|
||||||
|
{ 1, 0 },
|
||||||
|
{ 2, 100 },
|
||||||
|
{ 3, 300 },
|
||||||
|
{ 4, 600 },
|
||||||
|
{ 5, 1000 },
|
||||||
|
{ 6, 1500 },
|
||||||
|
{ 7, 2500 },
|
||||||
|
{ 8, 4000 }
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_level_thresholds_required_xp",
|
||||||
|
table: "level_thresholds",
|
||||||
|
column: "required_xp",
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "level_thresholds");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -334,6 +334,71 @@ namespace UniVerse.Infrastructure.Migrations
|
|||||||
b.ToTable("lecture_enrollments", (string)null);
|
b.ToTable("lecture_enrollments", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.LevelThreshold", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Level")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("level");
|
||||||
|
|
||||||
|
b.Property<int>("RequiredXp")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("required_xp");
|
||||||
|
|
||||||
|
b.HasKey("Level");
|
||||||
|
|
||||||
|
b.HasIndex("RequiredXp")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("level_thresholds", null, t =>
|
||||||
|
{
|
||||||
|
t.HasCheckConstraint("CK_level_thresholds_level_positive", "level > 0");
|
||||||
|
|
||||||
|
t.HasCheckConstraint("CK_level_thresholds_required_xp_non_negative", "required_xp >= 0");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.HasData(
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Level = 1,
|
||||||
|
RequiredXp = 0
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Level = 2,
|
||||||
|
RequiredXp = 100
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Level = 3,
|
||||||
|
RequiredXp = 300
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Level = 4,
|
||||||
|
RequiredXp = 600
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Level = 5,
|
||||||
|
RequiredXp = 1000
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Level = 6,
|
||||||
|
RequiredXp = 1500
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Level = 7,
|
||||||
|
RequiredXp = 2500
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Level = 8,
|
||||||
|
RequiredXp = 4000
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("UniVerse.Domain.Entities.Location", b =>
|
modelBuilder.Entity("UniVerse.Domain.Entities.Location", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ public class AuthService : IAuthService
|
|||||||
.Include(u => u.Roles)
|
.Include(u => u.Roles)
|
||||||
.FirstOrDefaultAsync(u => u.Id == userId)
|
.FirstOrDefaultAsync(u => u.Id == userId)
|
||||||
?? throw new NotFoundException("User", userId);
|
?? throw new NotFoundException("User", userId);
|
||||||
return user.ToDto(_gamification.CalculateLevel(user.Xp));
|
return user.ToDto(await _gamification.CalculateLevelAsync(user.Xp));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task TrySendLoginNotificationAsync(User user, string? ipAddress)
|
private async Task TrySendLoginNotificationAsync(User user, string? ipAddress)
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using UniVerse.Application.DTOs.Achievements;
|
using UniVerse.Application.DTOs.Achievements;
|
||||||
@@ -17,17 +16,16 @@ namespace UniVerse.Infrastructure.Services;
|
|||||||
public class GamificationService : IGamificationService
|
public class GamificationService : IGamificationService
|
||||||
{
|
{
|
||||||
private readonly AppDbContext _db;
|
private readonly AppDbContext _db;
|
||||||
private readonly IConfiguration _config;
|
|
||||||
private readonly INotificationService _notifications;
|
private readonly INotificationService _notifications;
|
||||||
private readonly ILogger<GamificationService> _logger;
|
private readonly ILogger<GamificationService> _logger;
|
||||||
|
private List<LevelThreshold>? _levelThresholds;
|
||||||
|
|
||||||
public GamificationService(
|
public GamificationService(
|
||||||
AppDbContext db,
|
AppDbContext db,
|
||||||
IConfiguration config,
|
|
||||||
INotificationService notifications,
|
INotificationService notifications,
|
||||||
ILogger<GamificationService> logger)
|
ILogger<GamificationService> logger)
|
||||||
{
|
{
|
||||||
_db = db; _config = config; _notifications = notifications; _logger = logger;
|
_db = db; _notifications = notifications; _logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task AwardCoinsAsync(int userId, int amount, CoinTransactionType type,
|
public async Task AwardCoinsAsync(int userId, int amount, CoinTransactionType type,
|
||||||
@@ -83,7 +81,7 @@ public class GamificationService : IGamificationService
|
|||||||
"attendance_streak_weeks" => attendanceStreakWeeks >= value,
|
"attendance_streak_weeks" => attendanceStreakWeeks >= value,
|
||||||
"attended_registered" => attended >= value,
|
"attended_registered" => attended >= value,
|
||||||
"coins_earned" => earnedCoins >= value,
|
"coins_earned" => earnedCoins >= value,
|
||||||
"level_reached" => CalculateLevel(user.Xp) >= value,
|
"level_reached" => await CalculateLevelAsync(user.Xp) >= value,
|
||||||
"profile_completed" => profileCompleted && value <= 1,
|
"profile_completed" => profileCompleted && value <= 1,
|
||||||
"first_activity" => firstActivity && value <= 1,
|
"first_activity" => firstActivity && value <= 1,
|
||||||
_ => false
|
_ => false
|
||||||
@@ -184,13 +182,47 @@ public class GamificationService : IGamificationService
|
|||||||
return DateOnly.FromDateTime(ISOWeek.ToDateTime(isoYear, isoWeek, DayOfWeek.Monday));
|
return DateOnly.FromDateTime(ISOWeek.ToDateTime(isoYear, isoWeek, DayOfWeek.Monday));
|
||||||
}
|
}
|
||||||
|
|
||||||
public int CalculateLevel(int xp)
|
public async Task<int> CalculateLevelAsync(int xp)
|
||||||
{
|
{
|
||||||
var thresholds = _config.GetSection("Gamification:XpThresholds").Get<int[]>()
|
var thresholds = await GetLevelThresholdsAsync();
|
||||||
?? [0, 100, 300, 600, 1000, 1500, 2500, 4000];
|
return thresholds
|
||||||
for (int i = thresholds.Length - 1; i >= 0; i--)
|
.Where(t => xp >= t.RequiredXp)
|
||||||
if (xp >= thresholds[i]) return i + 1;
|
.OrderBy(t => t.RequiredXp)
|
||||||
return 1;
|
.ThenBy(t => t.Level)
|
||||||
|
.LastOrDefault()?.Level ?? thresholds[0].Level;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<LevelProgressDto> GetLevelProgressAsync(int xp)
|
||||||
|
{
|
||||||
|
var thresholds = await GetLevelThresholdsAsync();
|
||||||
|
var current = thresholds
|
||||||
|
.Where(t => xp >= t.RequiredXp)
|
||||||
|
.OrderBy(t => t.RequiredXp)
|
||||||
|
.ThenBy(t => t.Level)
|
||||||
|
.LastOrDefault() ?? thresholds[0];
|
||||||
|
var next = thresholds
|
||||||
|
.Where(t => t.RequiredXp > current.RequiredXp)
|
||||||
|
.OrderBy(t => t.RequiredXp)
|
||||||
|
.ThenBy(t => t.Level)
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
return new LevelProgressDto(current.RequiredXp, next?.RequiredXp);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<LevelThreshold>> GetLevelThresholdsAsync()
|
||||||
|
{
|
||||||
|
if (_levelThresholds is { Count: > 0 }) return _levelThresholds;
|
||||||
|
|
||||||
|
_levelThresholds = await _db.LevelThresholds
|
||||||
|
.AsNoTracking()
|
||||||
|
.OrderBy(t => t.RequiredXp)
|
||||||
|
.ThenBy(t => t.Level)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
if (_levelThresholds.Count == 0)
|
||||||
|
_levelThresholds.Add(new LevelThreshold { Level = 1, RequiredXp = 0 });
|
||||||
|
|
||||||
|
return _levelThresholds;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<UserAchievementDto>> GetUserAchievementsAsync(int userId) =>
|
public async Task<List<UserAchievementDto>> GetUserAchievementsAsync(int userId) =>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ public class UserService : IUserService
|
|||||||
.Include(u => u.Roles)
|
.Include(u => u.Roles)
|
||||||
.FirstOrDefaultAsync(u => u.Id == id)
|
.FirstOrDefaultAsync(u => u.Id == id)
|
||||||
?? throw new NotFoundException("User", id);
|
?? throw new NotFoundException("User", id);
|
||||||
return user.ToDto(_gamification.CalculateLevel(user.Xp));
|
return user.ToDto(await _gamification.CalculateLevelAsync(user.Xp));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<UserDto> UpdateProfileAsync(int id, UpdateUserRequest request)
|
public async Task<UserDto> UpdateProfileAsync(int id, UpdateUserRequest request)
|
||||||
@@ -42,7 +42,7 @@ public class UserService : IUserService
|
|||||||
|
|
||||||
await _db.SaveChangesAsync();
|
await _db.SaveChangesAsync();
|
||||||
await _gamification.CheckAndAwardAchievementsAsync(id);
|
await _gamification.CheckAndAwardAchievementsAsync(id);
|
||||||
return user.ToDto(_gamification.CalculateLevel(user.Xp));
|
return user.ToDto(await _gamification.CalculateLevelAsync(user.Xp));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<UserStatsDto> GetStatsAsync(int id)
|
public async Task<UserStatsDto> GetStatsAsync(int id)
|
||||||
@@ -55,9 +55,13 @@ public class UserService : IUserService
|
|||||||
var reviews = await _db.Reviews.CountAsync(r => r.UserId == id);
|
var reviews = await _db.Reviews.CountAsync(r => r.UserId == id);
|
||||||
var achievements = await _db.UserAchievements.CountAsync(ua => ua.UserId == id);
|
var achievements = await _db.UserAchievements.CountAsync(ua => ua.UserId == id);
|
||||||
|
|
||||||
|
var level = await _gamification.CalculateLevelAsync(user.Xp);
|
||||||
|
var levelProgress = await _gamification.GetLevelProgressAsync(user.Xp);
|
||||||
|
|
||||||
return new UserStatsDto(
|
return new UserStatsDto(
|
||||||
totalLectures, attended, reviews,
|
totalLectures, attended, reviews,
|
||||||
user.Xp, user.Coins, _gamification.CalculateLevel(user.Xp), achievements
|
user.Xp, user.Coins, level, achievements,
|
||||||
|
levelProgress.CurrentLevelXp, levelProgress.NextLevelXp
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +98,10 @@ public class UserService : IUserService
|
|||||||
.Take(filter.PageSize)
|
.Take(filter.PageSize)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
var items = users.Select(u => u.ToDto(_gamification.CalculateLevel(u.Xp))).ToList();
|
var items = new List<UserDto>(users.Count);
|
||||||
|
foreach (var user in users)
|
||||||
|
items.Add(user.ToDto(await _gamification.CalculateLevelAsync(user.Xp)));
|
||||||
|
|
||||||
return PagedResult<UserDto>.Create(items, total, filter.Page, filter.PageSize);
|
return PagedResult<UserDto>.Create(items, total, filter.Page, filter.PageSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,8 +41,6 @@ services:
|
|||||||
- Email:Smtp:FromAddress=${EMAIL_SMTP_FROM_ADDRESS:-no-reply@universe.local}
|
- Email:Smtp:FromAddress=${EMAIL_SMTP_FROM_ADDRESS:-no-reply@universe.local}
|
||||||
- Email:Smtp:FromName=${EMAIL_SMTP_FROM_NAME:-UniVerse}
|
- Email:Smtp:FromName=${EMAIL_SMTP_FROM_NAME:-UniVerse}
|
||||||
|
|
||||||
- Gamification:XpThresholds=${GAMIFICATION_XP_THRESHOLDS:-[0, 100, 300, 600, 1000, 1500, 2500, 4000]}
|
|
||||||
|
|
||||||
- ConnectionStrings:DefaultConnection=Host=db;Port=5432;Database=${POSTGRES_DATABASE:-universe};Username=${POSTGRES_USER};Password=${POSTGRES_PASSWORD}
|
- ConnectionStrings:DefaultConnection=Host=db;Port=5432;Database=${POSTGRES_DATABASE:-universe};Username=${POSTGRES_USER};Password=${POSTGRES_PASSWORD}
|
||||||
networks:
|
networks:
|
||||||
- backend
|
- backend
|
||||||
|
|||||||
@@ -42,9 +42,6 @@ services:
|
|||||||
- Email:Smtp:Password=${EMAIL_SMTP_PASSWORD}
|
- Email:Smtp:Password=${EMAIL_SMTP_PASSWORD}
|
||||||
- Email:Smtp:FromAddress=${EMAIL_SMTP_FROM_ADDRESS:-no-reply@universe.local}
|
- Email:Smtp:FromAddress=${EMAIL_SMTP_FROM_ADDRESS:-no-reply@universe.local}
|
||||||
- Email:Smtp:FromName=${EMAIL_SMTP_FROM_NAME:-UniVerse}
|
- Email:Smtp:FromName=${EMAIL_SMTP_FROM_NAME:-UniVerse}
|
||||||
|
|
||||||
- Gamification:XpThresholds=${GAMIFICATION_XP_THRESHOLDS:-[0, 100, 300, 600, 1000, 1500, 2500, 4000]}
|
|
||||||
|
|
||||||
- ConnectionStrings:DefaultConnection=Host=db;Port=5432;Database=${POSTGRES_DATABASE};Username=${POSTGRES_USER};Password=${POSTGRES_PASSWORD}
|
- ConnectionStrings:DefaultConnection=Host=db;Port=5432;Database=${POSTGRES_DATABASE};Username=${POSTGRES_USER};Password=${POSTGRES_PASSWORD}
|
||||||
|
|
||||||
db:
|
db:
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ export function mapApiUser(user: UserAuthDto | UserDto, stats?: UserStatsDto): U
|
|||||||
coins: stats?.coins ?? ('coins' in user ? user.coins : 0),
|
coins: stats?.coins ?? ('coins' in user ? user.coins : 0),
|
||||||
level: stats?.level ?? ('level' in user ? user.level : 1),
|
level: stats?.level ?? ('level' in user ? user.level : 1),
|
||||||
xp: stats?.xp ?? ('xp' in user ? user.xp : 0),
|
xp: stats?.xp ?? ('xp' in user ? user.xp : 0),
|
||||||
|
currentLevelXp: stats?.currentLevelXp ?? 0,
|
||||||
|
nextLevelXp: stats?.nextLevelXp,
|
||||||
lecturesAttended: stats?.attendedLectures ?? 0,
|
lecturesAttended: stats?.attendedLectures ?? 0,
|
||||||
hoursLearned: stats ? Math.round(stats.attendedLectures * 1.5 * 10) / 10 : 0,
|
hoursLearned: stats ? Math.round(stats.attendedLectures * 1.5 * 10) / 10 : 0,
|
||||||
achievements: stats ? Array.from({ length: stats.achievementsCount }, (_, index) => String(index + 1)) : [],
|
achievements: stats ? Array.from({ length: stats.achievementsCount }, (_, index) => String(index + 1)) : [],
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ export interface UserStatsDto {
|
|||||||
coins: number
|
coins: number
|
||||||
level: number
|
level: number
|
||||||
achievementsCount: number
|
achievementsCount: number
|
||||||
|
currentLevelXp: number
|
||||||
|
nextLevelXp?: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LectureDto {
|
export interface LectureDto {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ defineProps<{
|
|||||||
max?: number
|
max?: number
|
||||||
label?: string
|
label?: string
|
||||||
color?: string
|
color?: string
|
||||||
|
text?: string
|
||||||
}>()
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -14,12 +15,12 @@ defineProps<{
|
|||||||
<div
|
<div
|
||||||
class="progress-fill"
|
class="progress-fill"
|
||||||
:style="{
|
: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)'
|
background: color ?? 'var(--gradient-progress-success)'
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="progress-text">{{ value }} / {{ max ?? 100 }}</div>
|
<div class="progress-text">{{ text ?? `${value} / ${max ?? 100}` }}</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
coins: stats.coins,
|
coins: stats.coins,
|
||||||
level: stats.level,
|
level: stats.level,
|
||||||
xp: stats.xp,
|
xp: stats.xp,
|
||||||
|
currentLevelXp: stats.currentLevelXp,
|
||||||
|
nextLevelXp: stats.nextLevelXp,
|
||||||
lecturesAttended: stats.attendedLectures,
|
lecturesAttended: stats.attendedLectures,
|
||||||
hoursLearned: Math.round(stats.attendedLectures * 1.5 * 10) / 10,
|
hoursLearned: Math.round(stats.attendedLectures * 1.5 * 10) / 10,
|
||||||
achievements: Array.from({ length: stats.achievementsCount }, (_, index) => String(index + 1)),
|
achievements: Array.from({ length: stats.achievementsCount }, (_, index) => String(index + 1)),
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ export interface User {
|
|||||||
coins: number
|
coins: number
|
||||||
level: number
|
level: number
|
||||||
xp?: number
|
xp?: number
|
||||||
|
currentLevelXp?: number
|
||||||
|
nextLevelXp?: number | null
|
||||||
lecturesAttended?: number
|
lecturesAttended?: number
|
||||||
hoursLearned?: number
|
hoursLearned?: number
|
||||||
achievements?: string[]
|
achievements?: string[]
|
||||||
|
|||||||
@@ -33,8 +33,25 @@ const recommended = computed(() =>
|
|||||||
)
|
)
|
||||||
const achievements = computed(() => userStore.achievements.filter(a => a.unlocked).slice(0, 3))
|
const achievements = computed(() => userStore.achievements.filter(a => a.unlocked).slice(0, 3))
|
||||||
const reminders = computed(() => userStore.notifications.slice(0, 3))
|
const reminders = computed(() => userStore.notifications.slice(0, 3))
|
||||||
const xpToNext = 200
|
const currentLevelXp = computed(() => user.value.currentLevelXp ?? 0)
|
||||||
const xpProgress = computed(() => user.value.xp ?? 120)
|
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 () => {
|
onMounted(async () => {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
@@ -99,10 +116,10 @@ onMounted(async () => {
|
|||||||
<GlassCard>
|
<GlassCard>
|
||||||
<div class="xp-section">
|
<div class="xp-section">
|
||||||
<div class="xp-header">
|
<div class="xp-header">
|
||||||
<span class="xp-label">Прогресс до уровня {{ user.level + 1 }}</span>
|
<span class="xp-label">{{ levelProgressLabel }}</span>
|
||||||
<span class="xp-val">{{ xpProgress }} / {{ xpToNext }} XP</span>
|
<span class="xp-val">{{ levelProgressText }}</span>
|
||||||
</div>
|
</div>
|
||||||
<ProgressBar :value="xpProgress" :max="xpToNext" />
|
<ProgressBar :value="levelProgress" :max="levelProgressMax" :text="levelProgressText" />
|
||||||
</div>
|
</div>
|
||||||
</GlassCard>
|
</GlassCard>
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,25 @@ const userYearLine = computed(() => {
|
|||||||
const year = user.value.year ?? 0
|
const year = user.value.year ?? 0
|
||||||
return Number.isFinite(year) && year > 0 ? `${year} курс` : ''
|
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([
|
const interestTags = ref([
|
||||||
{ label: '#ML', active: true },
|
{ label: '#ML', active: true },
|
||||||
{ label: '#ИИ', active: true },
|
{ label: '#ИИ', active: true },
|
||||||
@@ -66,10 +85,10 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="level">
|
<div class="level">
|
||||||
<div class="level-header">
|
<div class="level-header">
|
||||||
<span>Уровень {{ user.level }}</span>
|
<span>{{ levelProgressLabel }}</span>
|
||||||
<span>{{ user.xp }} / 200 XP</span>
|
<span>{{ levelProgressText }}</span>
|
||||||
</div>
|
</div>
|
||||||
<ProgressBar :value="user.xp ?? 0" :max="200" />
|
<ProgressBar :value="levelProgress" :max="levelProgressMax" :text="levelProgressText" />
|
||||||
</div>
|
</div>
|
||||||
<div class="tags">
|
<div class="tags">
|
||||||
<div class="section-title">Интересы</div>
|
<div class="section-title">Интересы</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user