mirror of
https://github.com/Sheldan/abstracto.git
synced 2026-01-05 08:54:34 +00:00
[AB-xxx] initial experience leaderboard version
This commit is contained in:
10
.github/workflows/build.yml
vendored
10
.github/workflows/build.yml
vendored
@@ -25,8 +25,18 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
distribution: 'corretto'
|
distribution: 'corretto'
|
||||||
java-version: 17
|
java-version: 17
|
||||||
|
- name: Setup node
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: '21.x'
|
||||||
- name: Build with Maven
|
- name: Build with Maven
|
||||||
run: mvn -B install --file abstracto-application/pom.xml
|
run: mvn -B install --file abstracto-application/pom.xml
|
||||||
|
- name: Install node dependencies and build
|
||||||
|
working-directory: ./ui/experience-tracking
|
||||||
|
run: npm ci
|
||||||
|
- name: Build ui application
|
||||||
|
working-directory: ./ui/experience-tracking
|
||||||
|
run: npm run build
|
||||||
- uses: actions/setup-ruby@v1
|
- uses: actions/setup-ruby@v1
|
||||||
- name: Send Webhook Notification
|
- name: Send Webhook Notification
|
||||||
if: always()
|
if: always()
|
||||||
|
|||||||
12
.github/workflows/release_manual.yml
vendored
12
.github/workflows/release_manual.yml
vendored
@@ -15,6 +15,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
distribution: 'corretto'
|
distribution: 'corretto'
|
||||||
java-version: 17
|
java-version: 17
|
||||||
|
- name: Setup node
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: '21.x'
|
||||||
- name: Load current version
|
- name: Load current version
|
||||||
id: version
|
id: version
|
||||||
working-directory: ./abstracto-application
|
working-directory: ./abstracto-application
|
||||||
@@ -37,6 +41,14 @@ jobs:
|
|||||||
release-branch-name: master
|
release-branch-name: master
|
||||||
maven-args: "-Dmaven.javadoc.skip=true -s settings.xml -DskipTests"
|
maven-args: "-Dmaven.javadoc.skip=true -s settings.xml -DskipTests"
|
||||||
access-token: ${{ secrets.GITHUB_TOKEN }}
|
access-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Install node dependencies and build
|
||||||
|
working-directory: ./ui/experience-tracking
|
||||||
|
run: npm ci
|
||||||
|
- name: Build ui application
|
||||||
|
working-directory: ./ui/experience-tracking
|
||||||
|
run: npm run build
|
||||||
|
- name: Copy built UI
|
||||||
|
run: cp -R ui/experience-tracking/build/* python/components/experience-tracking/resources/templates/experience/leaderboards/
|
||||||
- name: Login to Harbor
|
- name: Login to Harbor
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -38,6 +38,16 @@
|
|||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.google.code.gson</groupId>
|
<groupId>com.google.code.gson</groupId>
|
||||||
<artifactId>gson</artifactId>
|
<artifactId>gson</artifactId>
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package dev.sheldan.abstracto.experience.api;
|
||||||
|
|
||||||
|
import dev.sheldan.abstracto.core.models.database.AServer;
|
||||||
|
import dev.sheldan.abstracto.core.models.frontend.RoleDisplay;
|
||||||
|
import dev.sheldan.abstracto.core.service.GuildService;
|
||||||
|
import dev.sheldan.abstracto.core.service.management.ServerManagementService;
|
||||||
|
import dev.sheldan.abstracto.experience.model.api.ExperienceConfig;
|
||||||
|
import dev.sheldan.abstracto.experience.model.api.ExperienceRoleDisplay;
|
||||||
|
import dev.sheldan.abstracto.experience.model.template.LevelRole;
|
||||||
|
import dev.sheldan.abstracto.experience.service.ExperienceRoleService;
|
||||||
|
import net.dv8tion.jda.api.entities.Guild;
|
||||||
|
import net.dv8tion.jda.api.entities.Role;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping(value = "/experience/v1/")
|
||||||
|
public class ExperienceConfigController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ServerManagementService serverManagementService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ExperienceRoleService experienceRoleService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private GuildService guildService;
|
||||||
|
|
||||||
|
@GetMapping(value = "/leaderboards/{serverId}/config", produces = "application/json")
|
||||||
|
public ExperienceConfig getLeaderboard(@PathVariable("serverId") Long serverId) {
|
||||||
|
AServer server = serverManagementService.loadServer(serverId);
|
||||||
|
List<LevelRole> levelRoles = experienceRoleService.loadLevelRoleConfigForServer(server);
|
||||||
|
levelRoles = levelRoles.stream().sorted(Comparator.comparingInt(LevelRole::getLevel).reversed()).toList();
|
||||||
|
Guild guild = guildService.getGuildById(serverId);
|
||||||
|
List<ExperienceRoleDisplay> roles = levelRoles
|
||||||
|
.stream()
|
||||||
|
.map(levelRole -> convertRole(levelRole, guild))
|
||||||
|
.toList();
|
||||||
|
return ExperienceConfig
|
||||||
|
.builder()
|
||||||
|
.roles(roles)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ExperienceRoleDisplay convertRole(LevelRole levelRole, Guild guild) {
|
||||||
|
Role guildRole = guild.getRoleById(levelRole.getRoleId());
|
||||||
|
RoleDisplay roleDisplay;
|
||||||
|
if(guildRole != null) {
|
||||||
|
roleDisplay = RoleDisplay.fromRole(guildRole);
|
||||||
|
} else {
|
||||||
|
roleDisplay = RoleDisplay
|
||||||
|
.builder()
|
||||||
|
.id(levelRole.getRoleId())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
return ExperienceRoleDisplay
|
||||||
|
.builder()
|
||||||
|
.level(levelRole.getLevel())
|
||||||
|
.role(roleDisplay)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package dev.sheldan.abstracto.experience.api;
|
||||||
|
|
||||||
|
import dev.sheldan.abstracto.core.models.database.AServer;
|
||||||
|
import dev.sheldan.abstracto.core.models.frontend.RoleDisplay;
|
||||||
|
import dev.sheldan.abstracto.core.models.frontend.UserDisplay;
|
||||||
|
import dev.sheldan.abstracto.core.service.GuildService;
|
||||||
|
import dev.sheldan.abstracto.core.service.management.ServerManagementService;
|
||||||
|
import dev.sheldan.abstracto.experience.model.api.UserExperienceDisplay;
|
||||||
|
import dev.sheldan.abstracto.experience.model.database.AExperienceRole;
|
||||||
|
import dev.sheldan.abstracto.experience.model.database.AUserExperience;
|
||||||
|
import dev.sheldan.abstracto.experience.service.management.UserExperienceManagementService;
|
||||||
|
import net.dv8tion.jda.api.entities.Guild;
|
||||||
|
import net.dv8tion.jda.api.entities.Member;
|
||||||
|
import net.dv8tion.jda.api.entities.Role;
|
||||||
|
import net.dv8tion.jda.api.entities.UserSnowflake;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.domain.Sort;
|
||||||
|
import org.springframework.data.web.PageableDefault;
|
||||||
|
import org.springframework.data.web.SortDefault;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping(value = "/experience/v1")
|
||||||
|
public class LeaderboardController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ServerManagementService serverManagementService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserExperienceManagementService userExperienceManagementService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private GuildService guildService;
|
||||||
|
|
||||||
|
@GetMapping(value = "/leaderboards/{serverId}", produces = "application/json")
|
||||||
|
public Page<UserExperienceDisplay> getLeaderboard(@PathVariable("serverId") Long serverId,
|
||||||
|
@PageableDefault(value = 25, page = 0)
|
||||||
|
@SortDefault(sort = "experience", direction = Sort.Direction.DESC)
|
||||||
|
Pageable pageable) {
|
||||||
|
AServer server = serverManagementService.loadServer(serverId);
|
||||||
|
Guild guild = guildService.getGuildById(serverId);
|
||||||
|
return userExperienceManagementService.loadAllUsersPaginated(server, pageable)
|
||||||
|
.map(userExperience -> convertFromUser(guild, userExperience));
|
||||||
|
}
|
||||||
|
|
||||||
|
private UserExperienceDisplay convertFromUser(Guild guild, AUserExperience aUserExperience) {
|
||||||
|
Long userId = aUserExperience.getUser().getUserReference().getId();
|
||||||
|
Member member = guild.getMember(UserSnowflake.fromId(userId));
|
||||||
|
AExperienceRole experienceRole = aUserExperience.getCurrentExperienceRole();
|
||||||
|
UserDisplay userDisplay = null;
|
||||||
|
RoleDisplay roleDisplay = null;
|
||||||
|
if(experienceRole != null) {
|
||||||
|
Role role = guild.getRoleById(experienceRole.getRole().getId());
|
||||||
|
if(role != null) {
|
||||||
|
roleDisplay = RoleDisplay.fromRole(role);
|
||||||
|
} else {
|
||||||
|
roleDisplay = RoleDisplay.fromARole(experienceRole.getRole());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(member != null) {
|
||||||
|
userDisplay = UserDisplay.fromMember(member);
|
||||||
|
}
|
||||||
|
return UserExperienceDisplay
|
||||||
|
.builder()
|
||||||
|
.id(userId)
|
||||||
|
.messages(aUserExperience.getMessageCount())
|
||||||
|
.level(aUserExperience.getLevelOrDefault())
|
||||||
|
.experience(aUserExperience.getExperience())
|
||||||
|
.role(roleDisplay)
|
||||||
|
.member(userDisplay)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package dev.sheldan.abstracto.experience.model.api;
|
||||||
|
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public class ExperienceConfig {
|
||||||
|
private List<ExperienceRoleDisplay> roles;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package dev.sheldan.abstracto.experience.model.api;
|
||||||
|
|
||||||
|
import dev.sheldan.abstracto.core.models.frontend.RoleDisplay;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Builder
|
||||||
|
@Getter
|
||||||
|
public class ExperienceRoleDisplay {
|
||||||
|
private RoleDisplay role;
|
||||||
|
private Integer level;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package dev.sheldan.abstracto.experience.model.api;
|
||||||
|
|
||||||
|
import dev.sheldan.abstracto.core.models.frontend.RoleDisplay;
|
||||||
|
import dev.sheldan.abstracto.core.models.frontend.UserDisplay;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public class UserExperienceDisplay {
|
||||||
|
private UserDisplay member;
|
||||||
|
private Long id;
|
||||||
|
private Integer level;
|
||||||
|
private Long experience;
|
||||||
|
private Long messages;
|
||||||
|
private RoleDisplay role;
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package dev.sheldan.abstracto.experience.repository;
|
|||||||
import dev.sheldan.abstracto.core.models.database.AServer;
|
import dev.sheldan.abstracto.core.models.database.AServer;
|
||||||
import dev.sheldan.abstracto.experience.model.database.AUserExperience;
|
import dev.sheldan.abstracto.experience.model.database.AUserExperience;
|
||||||
import dev.sheldan.abstracto.experience.model.database.LeaderBoardEntryResult;
|
import dev.sheldan.abstracto.experience.model.database.LeaderBoardEntryResult;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import org.springframework.data.jpa.repository.Modifying;
|
import org.springframework.data.jpa.repository.Modifying;
|
||||||
@@ -24,6 +25,7 @@ public interface UserExperienceRepository extends JpaRepository<AUserExperience
|
|||||||
* @return A complete list of {@link AUserExperience} of the given {@link AServer server}
|
* @return A complete list of {@link AUserExperience} of the given {@link AServer server}
|
||||||
*/
|
*/
|
||||||
List<AUserExperience> findByUser_ServerReference(AServer server);
|
List<AUserExperience> findByUser_ServerReference(AServer server);
|
||||||
|
Page<AUserExperience> findAllByServer(AServer server, Pageable pageable);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the {@link AUserExperience userExperience} ordered by experience, and applies the {@link Pageable pageable} to only filter out certain pages.
|
* Retrieves the {@link AUserExperience userExperience} ordered by experience, and applies the {@link Pageable pageable} to only filter out certain pages.
|
||||||
|
|||||||
@@ -166,7 +166,10 @@ public class ExperienceRoleServiceBean implements ExperienceRoleService {
|
|||||||
List<AExperienceRole> roles = experienceRoleManagementService.getExperienceRolesForServer(server);
|
List<AExperienceRole> roles = experienceRoleManagementService.getExperienceRolesForServer(server);
|
||||||
List<LevelRole> levelRoles = new ArrayList<>();
|
List<LevelRole> levelRoles = new ArrayList<>();
|
||||||
roles.forEach(aExperienceRole -> {
|
roles.forEach(aExperienceRole -> {
|
||||||
Role role = roleService.getRoleFromGuild(aExperienceRole.getRole());
|
Role role = null;
|
||||||
|
if(!aExperienceRole.getRole().getDeleted()) {
|
||||||
|
role = roleService.getRoleFromGuild(aExperienceRole.getRole());
|
||||||
|
}
|
||||||
LevelRole levelRole = LevelRole
|
LevelRole levelRole = LevelRole
|
||||||
.builder()
|
.builder()
|
||||||
.role(role)
|
.role(role)
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ import dev.sheldan.abstracto.experience.model.database.LeaderBoardEntryResult;
|
|||||||
import dev.sheldan.abstracto.experience.repository.UserExperienceRepository;
|
import dev.sheldan.abstracto.experience.repository.UserExperienceRepository;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.PageRequest;
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -70,6 +72,11 @@ public class UserExperienceManagementServiceBean implements UserExperienceManage
|
|||||||
return repository.findByUser_ServerReference(server);
|
return repository.findByUser_ServerReference(server);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Page<AUserExperience> loadAllUsersPaginated(AServer server, Pageable pageable) {
|
||||||
|
return repository.findAllByServer(server, pageable);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<AUserExperience> findLeaderBoardUsersPaginated(AServer aServer, Integer page, Integer size) {
|
public List<AUserExperience> findLeaderBoardUsersPaginated(AServer aServer, Integer page, Integer size) {
|
||||||
return repository.findTop10ByUser_ServerReferenceOrderByExperienceDesc(aServer, PageRequest.of(page, size));
|
return repository.findTop10ByUser_ServerReferenceOrderByExperienceDesc(aServer, PageRequest.of(page, size));
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import dev.sheldan.abstracto.core.models.database.AUserInAServer;
|
|||||||
import dev.sheldan.abstracto.experience.model.database.AExperienceRole;
|
import dev.sheldan.abstracto.experience.model.database.AExperienceRole;
|
||||||
import dev.sheldan.abstracto.experience.model.database.AUserExperience;
|
import dev.sheldan.abstracto.experience.model.database.AUserExperience;
|
||||||
import dev.sheldan.abstracto.experience.model.database.LeaderBoardEntryResult;
|
import dev.sheldan.abstracto.experience.model.database.LeaderBoardEntryResult;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@@ -50,6 +52,7 @@ public interface UserExperienceManagementService {
|
|||||||
* @return A list of {@link AUserExperience} objects associated with the given {@link AServer}
|
* @return A list of {@link AUserExperience} objects associated with the given {@link AServer}
|
||||||
*/
|
*/
|
||||||
List<AUserExperience> loadAllUsers(AServer server);
|
List<AUserExperience> loadAllUsers(AServer server);
|
||||||
|
Page<AUserExperience> loadAllUsersPaginated(AServer server, Pageable pageable);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves a list of {@link AUserExperience} ordered by {@link AUserExperience} experience and only returns the positions between {@code start} and @{code end}.
|
* Retrieves a list of {@link AUserExperience} ordered by {@link AUserExperience} experience and only returns the positions between {@code start} and @{code end}.
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package dev.sheldan.abstracto.core.api;
|
||||||
|
|
||||||
|
import dev.sheldan.abstracto.core.exception.GuildNotFoundException;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||||
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
|
||||||
|
@RestControllerAdvice
|
||||||
|
@Slf4j
|
||||||
|
public class ExceptionHandlerConfig {
|
||||||
|
|
||||||
|
@ResponseStatus(HttpStatus.NOT_FOUND)
|
||||||
|
@ExceptionHandler(GuildNotFoundException.class)
|
||||||
|
protected ResponseEntity<String> handleResourceNotFound(GuildNotFoundException ex){
|
||||||
|
log.warn("Server not found.", ex);
|
||||||
|
return ResponseEntity
|
||||||
|
.status(HttpStatus.NOT_FOUND)
|
||||||
|
.body("Server not found");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package dev.sheldan.abstracto.core.api;
|
||||||
|
|
||||||
|
import dev.sheldan.abstracto.core.models.api.GuildDisplay;
|
||||||
|
import dev.sheldan.abstracto.core.service.GuildService;
|
||||||
|
import dev.sheldan.abstracto.core.service.management.ServerManagementService;
|
||||||
|
import net.dv8tion.jda.api.entities.Guild;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping(value = "/servers/v1/")
|
||||||
|
public class ServerController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ServerManagementService serverManagementService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private GuildService guildService;
|
||||||
|
|
||||||
|
@GetMapping(value = "/{serverId}/info", produces = "application/json")
|
||||||
|
public GuildDisplay getLeaderboard(@PathVariable("serverId") Long serverId) {
|
||||||
|
serverManagementService.loadServer(serverId); // only used for verification if it exists in the db
|
||||||
|
Guild guild = guildService.getGuildById(serverId);
|
||||||
|
return GuildDisplay
|
||||||
|
.builder()
|
||||||
|
.name(guild.getName())
|
||||||
|
.id(guild.getIdLong())
|
||||||
|
.bannerUrl(guild.getBannerUrl())
|
||||||
|
.iconUrl(guild.getIconUrl())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package dev.sheldan.abstracto.core.models.api;
|
||||||
|
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public class GuildDisplay {
|
||||||
|
private Long id;
|
||||||
|
private String name;
|
||||||
|
private String iconUrl;
|
||||||
|
private String bannerUrl;
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package dev.sheldan.abstracto.core.models.frontend;
|
||||||
|
|
||||||
|
import dev.sheldan.abstracto.core.models.database.ARole;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import net.dv8tion.jda.api.entities.Role;
|
||||||
|
|
||||||
|
import java.awt.*;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public class RoleDisplay {
|
||||||
|
private Long id;
|
||||||
|
private String name;
|
||||||
|
private Integer r;
|
||||||
|
private Integer g;
|
||||||
|
private Integer b;
|
||||||
|
|
||||||
|
public static RoleDisplay fromRole(Role role) {
|
||||||
|
RoleDisplayBuilder builder = builder()
|
||||||
|
.name(role.getName());
|
||||||
|
Color roleColor = role.getColor();
|
||||||
|
if(roleColor != null) {
|
||||||
|
builder.r(roleColor.getRed()).
|
||||||
|
b(roleColor.getBlue())
|
||||||
|
.g(roleColor.getGreen());
|
||||||
|
}
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
public static RoleDisplay fromARole(ARole role) {
|
||||||
|
return builder()
|
||||||
|
.id(role.getId())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package dev.sheldan.abstracto.core.models.frontend;
|
||||||
|
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import net.dv8tion.jda.api.entities.Member;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public class UserDisplay {
|
||||||
|
private String avatarUrl;
|
||||||
|
private String name;
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
public static UserDisplay fromMember(Member member) {
|
||||||
|
return builder()
|
||||||
|
.avatarUrl(member.getEffectiveAvatarUrl())
|
||||||
|
.name(member.getEffectiveName())
|
||||||
|
.id(member.getIdLong())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,3 +17,13 @@ services:
|
|||||||
context: python/components/image-gen
|
context: python/components/image-gen
|
||||||
dockerfile: docker/Dockerfile
|
dockerfile: docker/Dockerfile
|
||||||
image: ${REGISTRY_PREFIX}abstracto-rest-api-image-gen:${VERSION:-latest}
|
image: ${REGISTRY_PREFIX}abstracto-rest-api-image-gen:${VERSION:-latest}
|
||||||
|
core_api:
|
||||||
|
build:
|
||||||
|
context: python/components/core
|
||||||
|
dockerfile: docker/Dockerfile
|
||||||
|
image: ${REGISTRY_PREFIX}abstracto-rest-api-core:${VERSION:-latest}
|
||||||
|
experience_api:
|
||||||
|
build:
|
||||||
|
context: python/components/experience-tracking
|
||||||
|
dockerfile: docker/Dockerfile
|
||||||
|
image: ${REGISTRY_PREFIX}abstracto-rest-api-experience:${VERSION:-latest}
|
||||||
3
python/components/core/docker/Dockerfile
Normal file
3
python/components/core/docker/Dockerfile
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
FROM alpine:3.19.0
|
||||||
|
ADD resources /python/resources
|
||||||
|
ADD python /python
|
||||||
16
python/components/core/python/endpoints/server.py
Normal file
16
python/components/core/python/endpoints/server.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
from __main__ import app
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
backend_host = os.getenv('BACKEND_HOST')
|
||||||
|
backend_port = os.getenv('BACKEND_PORT')
|
||||||
|
|
||||||
|
server_url = f'http://{backend_host}:{backend_port}/servers/v1'
|
||||||
|
|
||||||
|
@app.route('/servers/v1/<serverId>/info')
|
||||||
|
def get_server_info(serverId):
|
||||||
|
server = requests.get(f'{server_url}/{serverId}/info')
|
||||||
|
logging.info(f'returning server info')
|
||||||
|
return server.text, server.status_code
|
||||||
0
python/components/core/resources/.gitkeep
Normal file
0
python/components/core/resources/.gitkeep
Normal file
3
python/components/experience-tracking/docker/Dockerfile
Normal file
3
python/components/experience-tracking/docker/Dockerfile
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
FROM alpine:3.19.0
|
||||||
|
ADD resources /python/resources
|
||||||
|
ADD python /python
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
from __main__ import app
|
||||||
|
|
||||||
|
from flask import request, render_template
|
||||||
|
import requests
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
backend_host = os.getenv('BACKEND_HOST')
|
||||||
|
backend_port = os.getenv('BACKEND_PORT')
|
||||||
|
|
||||||
|
leaderboard_url = f'http://{backend_host}:{backend_port}/experience/v1/leaderboards'
|
||||||
|
|
||||||
|
@app.route('/experience/v1/leaderboards/<serverId>')
|
||||||
|
def get_leaderboard(serverId):
|
||||||
|
page = int(request.args.get('page', 0, type=int))
|
||||||
|
size = int(request.args.get('size', 25, type=int))
|
||||||
|
leaderboard = requests.get(f'{leaderboard_url}/{serverId}?page={page}&size={size}')
|
||||||
|
logging.info(f'returning leaderboard for server')
|
||||||
|
return leaderboard.text, leaderboard.status_code
|
||||||
|
|
||||||
|
@app.route('/experience/v1/leaderboards/<serverId>/config')
|
||||||
|
def get_experience_config(serverId):
|
||||||
|
leaderboard = requests.get(f'{leaderboard_url}/{serverId}/config')
|
||||||
|
logging.info(f'returning experience config for server')
|
||||||
|
return leaderboard.text, leaderboard.status_code
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/experience/leaderboards/<serverId>')
|
||||||
|
def render_index(serverId):
|
||||||
|
return render_template('experience/leaderboards/index.html', serverId=serverId)
|
||||||
1
python/components/experience-tracking/resources/templates/experience/leaderboards/.gitignore
vendored
Normal file
1
python/components/experience-tracking/resources/templates/experience/leaderboards/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
*
|
||||||
23
ui/experience-tracking/.gitignore
vendored
Normal file
23
ui/experience-tracking/.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
16654
ui/experience-tracking/package-lock.json
generated
Normal file
16654
ui/experience-tracking/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
48
ui/experience-tracking/package.json
Normal file
48
ui/experience-tracking/package.json
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"name": "experience-tracking",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"homepage": "/experience/leaderboards",
|
||||||
|
"dependencies": {
|
||||||
|
"@testing-library/jest-dom": "^5.17.0",
|
||||||
|
"@testing-library/react": "^13.4.0",
|
||||||
|
"@testing-library/user-event": "^13.5.0",
|
||||||
|
"@types/jest": "^27.5.2",
|
||||||
|
"@types/node": "^16.18.91",
|
||||||
|
"@types/react": "^18.2.69",
|
||||||
|
"@types/react-dom": "^18.2.22",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-router-dom": "^6.22.3",
|
||||||
|
"react-scripts": "5.0.1",
|
||||||
|
"typescript": "^4.9.5",
|
||||||
|
"web-vitals": "^2.1.4"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "react-scripts start",
|
||||||
|
"build": "react-scripts build",
|
||||||
|
"test": "react-scripts test",
|
||||||
|
"eject": "react-scripts eject"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": [
|
||||||
|
"react-app",
|
||||||
|
"react-app/jest"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"tailwindcss": "^3.4.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
19
ui/experience-tracking/public/index.html
Normal file
19
ui/experience-tracking/public/index.html
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Leaderboard for experience"
|
||||||
|
/>
|
||||||
|
<script>window.serverId={{ serverId }}n</script>
|
||||||
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
|
<title>Experience leaderboard</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
8
ui/experience-tracking/public/manifest.json
Normal file
8
ui/experience-tracking/public/manifest.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"short_name": "Experience leaderboard",
|
||||||
|
"name": "Leaderboard showing experience gathered",
|
||||||
|
"start_url": ".",
|
||||||
|
"display": "standalone",
|
||||||
|
"theme_color": "#000000",
|
||||||
|
"background_color": "#ffffff"
|
||||||
|
}
|
||||||
3
ui/experience-tracking/public/robots.txt
Normal file
3
ui/experience-tracking/public/robots.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
||||||
2
ui/experience-tracking/src/App.css
Normal file
2
ui/experience-tracking/src/App.css
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
|
||||||
17
ui/experience-tracking/src/App.tsx
Normal file
17
ui/experience-tracking/src/App.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import './App.css';
|
||||||
|
import {Leaderboard} from "./components/Leaderboard";
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
// @ts-ignore
|
||||||
|
const serverId: bigint = window.serverId
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="bg-slate-700 bg-cover min-h-screen">
|
||||||
|
<Leaderboard serverId={serverId}/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
8
ui/experience-tracking/src/components/ErrorDisplay.tsx
Normal file
8
ui/experience-tracking/src/components/ErrorDisplay.tsx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
export const ErrorDisplay = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1 className="mb-4 text-4xl font-extrabold leading-none tracking-tight md:text-5xl lg:text-6xl text-white">Failed to load leaderboard</h1>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import {ExperienceConfig, ExperienceRole} from "../data/leaderboard";
|
||||||
|
import {RoleDisplay} from "./RoleDisplay";
|
||||||
|
import {useEffect, useState} from "react";
|
||||||
|
|
||||||
|
export const ExperienceConfigDisplay = ({serverId}: { serverId: bigint }) => {
|
||||||
|
|
||||||
|
const [roles, setRoles] = useState<ExperienceRole[]>([])
|
||||||
|
const [hasError, setError] = useState(false)
|
||||||
|
async function loadConfig() {
|
||||||
|
try {
|
||||||
|
const configResponse = await fetch(`/experience/v1/leaderboards/${serverId}/config`)
|
||||||
|
let configObj: ExperienceConfig = await configResponse.json();
|
||||||
|
const roles = configObj.roles;
|
||||||
|
setRoles(roles)
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
setError(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(()=> {
|
||||||
|
loadConfig()
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
},[])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{!hasError ?
|
||||||
|
<div className="py-10">
|
||||||
|
<h2 className="text-4xl font-extrabold leading-none tracking-tight text-white">Role
|
||||||
|
config</h2>
|
||||||
|
<table className="w-full text-gray-400">
|
||||||
|
<thead
|
||||||
|
className="text-xs uppercase bg-gray-700 text-gray-400">
|
||||||
|
<tr>
|
||||||
|
<th className="px-6 py-3 w-1/2">Role</th>
|
||||||
|
<th className="px-6 py-3 w-1/8">Level</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{roles.map(role =>
|
||||||
|
<tr key={role.role.id} className="border-b bg-gray-800 border-gray-700">
|
||||||
|
<td className="px-6 py-4">
|
||||||
|
<RoleDisplay role={role.role}/>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 text-center">
|
||||||
|
{role.level}
|
||||||
|
</td>
|
||||||
|
</tr>)}
|
||||||
|
</tbody>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
{roles.length === 0 ?
|
||||||
|
<div className="w-full flex items-center justify-center">
|
||||||
|
<span className="text-gray-400">No roles</span>
|
||||||
|
</div> : ''}
|
||||||
|
</div>
|
||||||
|
: ''}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
112
ui/experience-tracking/src/components/Leaderboard.tsx
Normal file
112
ui/experience-tracking/src/components/Leaderboard.tsx
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import {LeaderboardEntry} from "./LeaderboardEntry";
|
||||||
|
import {useEffect, useState} from "react";
|
||||||
|
import {ExperienceMember, GuildInfo} from "../data/leaderboard";
|
||||||
|
import {ExperienceConfigDisplay} from "./ExperienceConfigDisplay";
|
||||||
|
import {ErrorDisplay} from "./ErrorDisplay";
|
||||||
|
|
||||||
|
export function Leaderboard({serverId}: { serverId: bigint }) {
|
||||||
|
|
||||||
|
const pageSize = 25;
|
||||||
|
|
||||||
|
const [members, setMembers] = useState<ExperienceMember[]>([])
|
||||||
|
const [memberCount, setMemberCount] = useState(0)
|
||||||
|
const [pageCount, setPageCount] = useState(0)
|
||||||
|
const [hasMore, setHasMore] = useState(true)
|
||||||
|
const [hasError, setError] = useState(false)
|
||||||
|
const [guildInfo, setGuildInfo] = useState<GuildInfo>({} as GuildInfo)
|
||||||
|
|
||||||
|
async function loadLeaderboard(page: number, size: number) {
|
||||||
|
try {
|
||||||
|
const leaderboardResponse = await fetch(`/experience/v1/leaderboards/${serverId}?page=${page}&size=${size}`)
|
||||||
|
const leaderboardJson = await leaderboardResponse.json()
|
||||||
|
const loadedMembers: Array<ExperienceMember> = leaderboardJson.content;
|
||||||
|
setMemberCount(memberCount + loadedMembers.length)
|
||||||
|
setHasMore(!leaderboardJson.last)
|
||||||
|
setPageCount(page)
|
||||||
|
setMembers(members.concat(loadedMembers))
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
setError(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadGuildInfo() {
|
||||||
|
try {
|
||||||
|
const guildInfoResponse = await fetch(`/servers/v1/${serverId}/info`)
|
||||||
|
const guildInfoJson: GuildInfo= await guildInfoResponse.json()
|
||||||
|
setGuildInfo(guildInfoJson)
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(()=> {
|
||||||
|
if(memberCount === 0) {
|
||||||
|
loadLeaderboard(0, pageSize)
|
||||||
|
}
|
||||||
|
loadGuildInfo()
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
},[])
|
||||||
|
|
||||||
|
function loadMore() {
|
||||||
|
loadLeaderboard(pageCount + 1, pageSize)
|
||||||
|
}
|
||||||
|
let loadMoreButton = <button className="w-full bg-gray-500 hover:bg-gray-700 text-white" onClick={loadMore}>Load more</button>;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{!hasError ?
|
||||||
|
<>
|
||||||
|
<div className="relative font-[sans-serif] before:absolute before:w-full before:h-full before:inset-0 before:bg-black before:opacity-50 before:z-10 h-48">
|
||||||
|
{guildInfo.bannerUrl !== null ? <img src={guildInfo.bannerUrl + "?size=4096"}
|
||||||
|
alt="Banner"
|
||||||
|
className="absolute inset-0 w-full h-full object-cover"/> : ''}
|
||||||
|
<div
|
||||||
|
className="min-h-[150px] relative z-50 h-full max-w-6xl mx-auto flex flex-row justify-center items-center text-center text-white p-6">
|
||||||
|
{guildInfo.iconUrl !== null ? <img
|
||||||
|
src={guildInfo.iconUrl + "?size=512"}
|
||||||
|
alt="Icon"
|
||||||
|
className="w-24"/>
|
||||||
|
: ''}
|
||||||
|
<h1 className="text-4xl font-extrabold leading-none tracking-tight md:text-5xl lg:text-6xl text-white">{'Leaderboard for ' + guildInfo.name}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
<div className="text-sm text-left w-3/4 ">
|
||||||
|
<table className="w-full text-gray-400">
|
||||||
|
<thead
|
||||||
|
className="text-xs uppercase bg-gray-700 text-gray-400">
|
||||||
|
<tr>
|
||||||
|
<th scope="col" className="px-6 py-3 w-1/3">
|
||||||
|
Member
|
||||||
|
</th>
|
||||||
|
<th scope="col" className="px-6 py-3 w-1/6 text-center">
|
||||||
|
Experience
|
||||||
|
</th>
|
||||||
|
<th scope="col" className="px-6 py-3 w-1/6 text-center">
|
||||||
|
Messages
|
||||||
|
</th>
|
||||||
|
<th scope="col" className="px-6 py-3 w-1/6 text-center">
|
||||||
|
Level
|
||||||
|
</th>
|
||||||
|
<th scope="col" className="px-6 py-3 w-1/3 text-center">
|
||||||
|
Role
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{members.map(member => <LeaderboardEntry key={member.id} member={member}/>)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{hasMore ? loadMoreButton : ''}
|
||||||
|
</div>
|
||||||
|
<div className="w-1/4 px-3">
|
||||||
|
<ExperienceConfigDisplay serverId={serverId}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
: <ErrorDisplay/>}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
37
ui/experience-tracking/src/components/LeaderboardEntry.tsx
Normal file
37
ui/experience-tracking/src/components/LeaderboardEntry.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import {ExperienceMember} from "../data/leaderboard";
|
||||||
|
import {RoleDisplay} from "./RoleDisplay";
|
||||||
|
import createStyle from "../utils/styleUtils";
|
||||||
|
|
||||||
|
export const LeaderboardEntry = ({member}: { member: ExperienceMember }) => {
|
||||||
|
const userHasRole = member.role !== null;
|
||||||
|
const memberExists = member.member !== null;
|
||||||
|
const nameColor = userHasRole ? createStyle(member.role!) : ''
|
||||||
|
let memberDisplay = memberExists ? <>
|
||||||
|
<img alt={member.member!.name} src={member.member!.avatarUrl}
|
||||||
|
className="object-contain h-16 w-16 rounded-full"/>
|
||||||
|
<span className="align-middle" style={{color: nameColor}}>{member.member!.name}</span>
|
||||||
|
</> : <>{member.id}</>;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<tr className="border-b bg-gray-800 border-gray-700">
|
||||||
|
<td
|
||||||
|
className="px-2 py-4 font-medium whitespace-nowrap text-white flex items-center gap-3">
|
||||||
|
{memberDisplay}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 text-center">
|
||||||
|
{member.experience.toLocaleString()}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 text-center">
|
||||||
|
{member.messages.toLocaleString()}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 text-center">
|
||||||
|
{member.level.toString()}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 text-center">
|
||||||
|
{userHasRole ? <RoleDisplay role={member.role!}/> : 'No role'}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
ui/experience-tracking/src/components/RoleDisplay.tsx
Normal file
12
ui/experience-tracking/src/components/RoleDisplay.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import {Role} from "../data/leaderboard";
|
||||||
|
import createStyle from "../utils/styleUtils";
|
||||||
|
|
||||||
|
export const RoleDisplay = ({role}: { role: Role | null }) => {
|
||||||
|
const roleColor = createStyle(role);
|
||||||
|
let roleDisplay = role !== null && role.name !== null ? <span style={{ color: roleColor}}>{role.name}</span> : <>Deleted role {role !== null ? role!.id : ''}</>
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{roleDisplay}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
38
ui/experience-tracking/src/data/leaderboard.tsx
Normal file
38
ui/experience-tracking/src/data/leaderboard.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
export interface ExperienceMember {
|
||||||
|
experience: bigint;
|
||||||
|
id: bigint;
|
||||||
|
level: number;
|
||||||
|
messages: bigint;
|
||||||
|
member: Member | null;
|
||||||
|
role: Role | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Member {
|
||||||
|
avatarUrl: string;
|
||||||
|
name: string;
|
||||||
|
id: bigint;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Role {
|
||||||
|
r: number | null;
|
||||||
|
g: number | null;
|
||||||
|
b: number | null;
|
||||||
|
name: string | null;
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExperienceRole {
|
||||||
|
role: Role;
|
||||||
|
level: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExperienceConfig {
|
||||||
|
roles: Array<ExperienceRole>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GuildInfo {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
iconUrl: string | null;
|
||||||
|
bannerUrl: string | null;
|
||||||
|
}
|
||||||
12
ui/experience-tracking/src/index.css
Normal file
12
ui/experience-tracking/src/index.css
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||||
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
19
ui/experience-tracking/src/index.tsx
Normal file
19
ui/experience-tracking/src/index.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import './index.css';
|
||||||
|
import App from './App';
|
||||||
|
import {BrowserRouter} from 'react-router-dom';
|
||||||
|
import reportWebVitals from './reportWebVitals';
|
||||||
|
|
||||||
|
const root = ReactDOM.createRoot(
|
||||||
|
document.getElementById('root') as HTMLElement
|
||||||
|
);
|
||||||
|
root.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<BrowserRouter basename={process.env.PUBLIC_URL}>
|
||||||
|
<App />
|
||||||
|
</BrowserRouter>
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
|
|
||||||
|
reportWebVitals();
|
||||||
1
ui/experience-tracking/src/react-app-env.d.ts
vendored
Normal file
1
ui/experience-tracking/src/react-app-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="react-scripts" />
|
||||||
15
ui/experience-tracking/src/reportWebVitals.ts
Normal file
15
ui/experience-tracking/src/reportWebVitals.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { ReportHandler } from 'web-vitals';
|
||||||
|
|
||||||
|
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
||||||
|
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||||
|
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||||
|
getCLS(onPerfEntry);
|
||||||
|
getFID(onPerfEntry);
|
||||||
|
getFCP(onPerfEntry);
|
||||||
|
getLCP(onPerfEntry);
|
||||||
|
getTTFB(onPerfEntry);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default reportWebVitals;
|
||||||
6
ui/experience-tracking/src/utils/styleUtils.ts
Normal file
6
ui/experience-tracking/src/utils/styleUtils.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default function createStyle(obj: any) {
|
||||||
|
if(obj == null || obj.r == null) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return `#${obj.r.toString(16).padStart(2, '0')}${obj.g.toString(16).padStart(2, '0')}${obj.b.toString(16).padStart(2, '0')}`
|
||||||
|
}
|
||||||
13
ui/experience-tracking/tailwind.config.js
Normal file
13
ui/experience-tracking/tailwind.config.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
mode: 'jit',
|
||||||
|
content: [
|
||||||
|
"./src/**/*.{js,jsx,ts,tsx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
26
ui/experience-tracking/tsconfig.json
Normal file
26
ui/experience-tracking/tsconfig.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx"
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user