Compare commits

...

3 Commits

Author SHA1 Message Date
Peter d54a7e51ee Add MyPet and Warps components with HTML, SCSS
Implemented the MyPet and Warps components, including their HTML structure, styles, and unit tests. These components provide detailed information and features for MyPet and Player Warps systems for a Minecraft-inspired application.
2025-04-19 16:37:27 +02:00
Teriuihi 6be6944dea Update rate limit for HistoryApiController
Changed the rate limit from 30 requests per minute to 30 requests per 10 seconds. This ensures a smoother request flow and prevents excessive delays for frequent API users.
2025-04-19 05:00:41 +02:00
Teriuihi 3babde5513 Add punishment details and history retrieval functionality
This commit introduces a new `DetailsComponent` for displaying detailed punishment data and establishes a route to view punishment history by ID and type. It also updates the API to support fetching individual punishment records and refines database mappings for improved data handling.
2025-04-19 04:02:51 +02:00
30 changed files with 1109 additions and 166 deletions

View File

@ -3,11 +3,11 @@ package com.alttd.altitudeweb.controllers.history;
import com.alttd.altitudeweb.api.HistoryApi;
import com.alttd.altitudeweb.controllers.limits.RateLimit;
import com.alttd.altitudeweb.model.HistoryCountDto;
import com.alttd.altitudeweb.model.PunishmentHistoryListDto;
import com.alttd.altitudeweb.setup.Connection;
import com.alttd.altitudeweb.database.Databases;
import com.alttd.altitudeweb.database.litebans.*;
import com.alttd.altitudeweb.model.PunishmentHistoryDto;
import com.alttd.altitudeweb.model.PunishmentHistoryInnerDto;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RestController;
@ -19,19 +19,19 @@ import java.util.concurrent.TimeUnit;
@Slf4j
@RestController
@RateLimit(limit = 30, timeValue = 1, timeUnit = TimeUnit.MINUTES)
@RateLimit(limit = 30, timeValue = 10, timeUnit = TimeUnit.SECONDS)
public class HistoryApiController implements HistoryApi {
@Override
public ResponseEntity<PunishmentHistoryDto> getHistoryForAll(String userType, String type, Integer page) {
public ResponseEntity<PunishmentHistoryListDto> getHistoryForAll(String userType, String type, Integer page) {
return getHistoryForUsers(userType, type, "", page);
}
@Override
public ResponseEntity<PunishmentHistoryDto> getHistoryForUsers(String userType, String type, String user, Integer page) {
public ResponseEntity<PunishmentHistoryListDto> getHistoryForUsers(String userType, String type, String user, Integer page) {
UserType userTypeEnum = UserType.getUserType(userType);
HistoryType historyTypeEnum = HistoryType.getHistoryType(type);
PunishmentHistoryDto punishmentHistory = new PunishmentHistoryDto();
PunishmentHistoryListDto punishmentHistoryList = new PunishmentHistoryListDto();
CompletableFuture<List<HistoryRecord>> historyRecords = new CompletableFuture<>();
Connection.getConnection(Databases.LITE_BANS)
@ -46,14 +46,14 @@ public class HistoryApiController implements HistoryApi {
historyRecords.completeExceptionally(e);
}
});
return mapPunishmentHistory(punishmentHistory, historyRecords);
return mapPunishmentHistory(punishmentHistoryList, historyRecords);
}
@Override
public ResponseEntity<PunishmentHistoryDto> getHistoryForUuid(String userType, String type, String uuid, Integer page) {
public ResponseEntity<PunishmentHistoryListDto> getHistoryForUuid(String userType, String type, String uuid, Integer page) {
UserType userTypeEnum = UserType.getUserType(userType);
HistoryType historyTypeEnum = HistoryType.getHistoryType(type);
PunishmentHistoryDto punishmentHistory = new PunishmentHistoryDto();
PunishmentHistoryListDto punishmentHistoryList = new PunishmentHistoryListDto();
CompletableFuture<List<HistoryRecord>> historyRecords = new CompletableFuture<>();
Connection.getConnection(Databases.LITE_BANS)
@ -68,7 +68,7 @@ public class HistoryApiController implements HistoryApi {
historyRecords.completeExceptionally(e);
}
});
return mapPunishmentHistory(punishmentHistory, historyRecords);
return mapPunishmentHistory(punishmentHistoryList, historyRecords);
}
@Override
@ -153,6 +153,26 @@ public class HistoryApiController implements HistoryApi {
return ResponseEntity.ok().body(searchResultCountCompletableFuture.join());
}
@Override
public ResponseEntity<PunishmentHistoryDto> getHistoryById(String type, Integer id) {
HistoryType historyTypeEnum = HistoryType.getHistoryType(type);
CompletableFuture<HistoryRecord> historyRecordCompletableFuture = new CompletableFuture<>();
Connection.getConnection(Databases.LITE_BANS)
.runQuery(sqlSession -> {
log.debug("Loading history by id");
try {
HistoryRecord punishment = sqlSession.getMapper(IdHistoryMapper.class)
.getRecentHistory(historyTypeEnum, id);
historyRecordCompletableFuture.complete(punishment);
} catch (Exception e) {
log.error("Failed to load history count", e);
historyRecordCompletableFuture.completeExceptionally(e);
}
});
return ResponseEntity.ok().body(mapPunishmentHistory(historyRecordCompletableFuture.join()));
}
private ResponseEntity<HistoryCountDto> mapHistoryCount(HistoryCount historyCount) {
HistoryCountDto historyCountDto = new HistoryCountDto();
historyCountDto.setBans(historyCount.getBans());
@ -162,16 +182,23 @@ public class HistoryApiController implements HistoryApi {
return ResponseEntity.ok().body(historyCountDto);
}
private ResponseEntity<PunishmentHistoryDto> mapPunishmentHistory(PunishmentHistoryDto punishmentHistory, CompletableFuture<List<HistoryRecord>> historyRecords) {
private ResponseEntity<PunishmentHistoryListDto> mapPunishmentHistory(PunishmentHistoryListDto punishmentHistoryList, CompletableFuture<List<HistoryRecord>> historyRecords) {
historyRecords.join().forEach(historyRecord -> {
PunishmentHistoryInnerDto.TypeEnum type = switch (historyRecord.getType().toLowerCase()) {
case "ban" -> PunishmentHistoryInnerDto.TypeEnum.BAN;
case "mute" -> PunishmentHistoryInnerDto.TypeEnum.MUTE;
case "warn" -> PunishmentHistoryInnerDto.TypeEnum.WARN;
case "kick" -> PunishmentHistoryInnerDto.TypeEnum.KICK;
PunishmentHistoryDto innerDto = mapPunishmentHistory(historyRecord);
punishmentHistoryList.add(innerDto);
});
return ResponseEntity.ok().body(punishmentHistoryList);
}
private static PunishmentHistoryDto mapPunishmentHistory(HistoryRecord historyRecord) {
PunishmentHistoryDto.TypeEnum type = switch (historyRecord.getType().toLowerCase()) {
case "ban" -> PunishmentHistoryDto.TypeEnum.BAN;
case "mute" -> PunishmentHistoryDto.TypeEnum.MUTE;
case "warn" -> PunishmentHistoryDto.TypeEnum.WARN;
case "kick" -> PunishmentHistoryDto.TypeEnum.KICK;
default -> throw new IllegalStateException("Unexpected value: " + historyRecord.getType());
};
PunishmentHistoryInnerDto innerDto = new PunishmentHistoryInnerDto()
return new PunishmentHistoryDto()
.uuid(historyRecord.getUuid())
.username(historyRecord.getPunishedName())
.reason(historyRecord.getReason())
@ -181,9 +208,7 @@ public class HistoryApiController implements HistoryApi {
.punishmentTime(historyRecord.getTime())
.expiryTime(historyRecord.getUntil())
.removedReason(historyRecord.getRemovedByReason())
.type(type);
punishmentHistory.add(innerDto);
});
return ResponseEntity.ok().body(punishmentHistory);
.type(type)
.id(historyRecord.getId());
}
}

View File

@ -16,6 +16,7 @@ dependencies {
implementation("org.mybatis:mybatis:3.5.13")
compileOnly("org.slf4j:slf4j-api:2.0.17")
compileOnly("org.slf4j:slf4j-simple:2.0.17")
compileOnly("org.jetbrains:annotations:26.0.2")
}
tasks.test {

View File

@ -5,6 +5,7 @@ import lombok.Data;
@Data
public class HistoryRecord {
private String uuid;
private int id;
private String punishedName;
private String reason;
private String bannedByUuid;

View File

@ -0,0 +1,54 @@
package com.alttd.altitudeweb.database.litebans;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.Select;
public interface IdHistoryMapper {
@Results({
@Result(property = "id", column = "id"),
@Result(property = "uuid", column = "uuid"),
@Result(property = "punishedName", column = "punished_name"),
@Result(property = "reason", column = "reason"),
@Result(property = "bannedByUuid", column = "banned_by_uuid"),
@Result(property = "bannedByName", column = "banned_by_name"),
@Result(property = "removedByName", column = "removed_by_name"),
@Result(property = "time", column = "time"),
@Result(property = "until", column = "until"),
@Result(property = "removedByReason", column = "removed_by_reason")
})
@Select("""
SELECT punishment.id,
punishment.uuid,
user_lookup.name AS punished_name,
punishment.reason,
punishment.banned_by_uuid,
punishment.banned_by_name,
punishment.removed_by_name,
punishment.time,
punishment.until,
punishment.removed_by_reason
FROM (SELECT *
FROM ${table_name} AS punishment
WHERE punishment.id = #{id})
AS punishment
INNER JOIN user_lookup
ON user_lookup.uuid = punishment.uuid
""")
HistoryRecord getHistoryForId(@Param("table_name") String tableName,
@Param("type") String type,
@Param("id") int id);
default HistoryRecord getRecentHistory(HistoryType historyType, int id) {
HistoryRecord historyRecord = switch (historyType) {
case ALL -> throw new IllegalArgumentException("HistoryType.ALL is not supported");
case BAN -> getHistoryForId("litebans_bans", "ban", id);
case MUTE -> getHistoryForId("litebans_mutes", "mute", id);
case KICK -> getHistoryForId("litebans_kicks", "kick", id);
case WARN -> getHistoryForId("litebans_warnings", "warn", id);
};
historyRecord.setType(historyType.name().toLowerCase());
return historyRecord;
}
}

View File

@ -4,6 +4,7 @@ import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.Select;
import org.jetbrains.annotations.NotNull;
import java.util.List;
@ -24,6 +25,7 @@ public interface NameHistoryMapper {
* order
*/
@Results({
@Result(property = "id", column = "id"),
@Result(property = "uuid", column = "uuid"),
@Result(property = "punishedName", column = "punished_name"),
@Result(property = "reason", column = "reason"),
@ -36,7 +38,8 @@ public interface NameHistoryMapper {
@Result(property = "type", column = "type")
})
@Select("""
SELECT all_punishments.uuid,
SELECT id,
all_punishments.uuid,
user_lookup.name AS punished_name,
reason,
banned_by_uuid,
@ -62,7 +65,6 @@ public interface NameHistoryMapper {
* Retrieves a list of all types of recent punishment history records sorted
* in descending time order. This result does NOT contain kicks history
*
* @param nameColumn the column name in the database indicating the name to use for filtering
* @param limit the maximum number of records to fetch
* @param offset the starting offset position of the result set
*
@ -70,6 +72,7 @@ public interface NameHistoryMapper {
* order
*/
@Results({
@Result(property = "id", column = "id"),
@Result(property = "uuid", column = "uuid"),
@Result(property = "punishedName", column = "punished_name"),
@Result(property = "reason", column = "reason"),
@ -82,17 +85,19 @@ public interface NameHistoryMapper {
@Result(property = "type", column = "type")
})
@Select("""
SELECT punishments.uuid,
SELECT punishment.id,
punishment.uuid,
user_lookup.name AS punished_name,
punishments.reason,
punishments.banned_by_uuid,
punishments.banned_by_name,
punishments.removed_by_name,
punishments.time,
punishments.until,
punishments.removed_by_reason,
punishments.type
punishment.reason,
punishment.banned_by_uuid,
punishment.banned_by_name,
punishment.removed_by_name,
punishment.time,
punishment.until,
punishment.removed_by_reason,
punishment.type
FROM (SELECT uuid,
id,
reason,
banned_by_uuid,
banned_by_name,
@ -103,9 +108,9 @@ public interface NameHistoryMapper {
type
FROM all_punishments
ORDER BY time DESC
LIMIT #{limit} OFFSET #{offset}) AS punishments
LIMIT #{limit} OFFSET #{offset}) AS punishment
INNER JOIN user_lookup
ON user_lookup.uuid = punishments.uuid;
ON user_lookup.uuid = punishment.uuid;
""")
List<HistoryRecord> getRecentAllHistory(@Param("limit") int limit,
@Param("offset") int offset);
@ -124,6 +129,7 @@ public interface NameHistoryMapper {
* order
*/
@Results({
@Result(property = "id", column = "id"),
@Result(property = "uuid", column = "uuid"),
@Result(property = "punishedName", column = "punished_name"),
@Result(property = "reason", column = "reason"),
@ -135,7 +141,8 @@ public interface NameHistoryMapper {
@Result(property = "removedByReason", column = "removed_by_reason")
})
@Select("""
SELECT punishment.uuid,
SELECT punishment.id,
punishment.uuid,
user_lookup.name AS punished_name,
punishment.reason,
punishment.banned_by_uuid,
@ -162,8 +169,6 @@ public interface NameHistoryMapper {
* parameters. The records are sorted in descending order by time and limited to a specified range.
*
* @param tableName the name of the database table to query the history records from
* @param partialName a partial or complete name of the user to filter the records, case-insensitive
* @param nameColumn the column name in the database indicating the name to use for filtering
* @param limit the maximum number of records to fetch
* @param offset the starting offset position of the result set
*
@ -171,6 +176,7 @@ public interface NameHistoryMapper {
* order
*/
@Results({
@Result(property = "id", column = "id"),
@Result(property = "uuid", column = "uuid"),
@Result(property = "punishedName", column = "punished_name"),
@Result(property = "reason", column = "reason"),
@ -182,7 +188,8 @@ public interface NameHistoryMapper {
@Result(property = "removedByReason", column = "removed_by_reason")
})
@Select("""
SELECT punishment.uuid,
SELECT punishment.id,
punishment.uuid,
user_lookup.name AS punished_name,
punishment.reason,
punishment.banned_by_uuid,
@ -192,7 +199,8 @@ public interface NameHistoryMapper {
punishment.until,
punishment.removed_by_reason
FROM (
SELECT uuid,
SELECT id,
uuid,
reason,
banned_by_uuid,
banned_by_name,
@ -254,23 +262,23 @@ public interface NameHistoryMapper {
}
}
private List<HistoryRecord> getRecentBans(UserType userType, String partialName, int page) {
private @NotNull List<HistoryRecord> getRecentBans(UserType userType, String partialName, int page) {
return addType(getRecent("litebans_bans", userType, partialName, page), "ban");
}
private List<HistoryRecord> getRecentKicks(UserType userType, String partialName, int page) {
private @NotNull List<HistoryRecord> getRecentKicks(UserType userType, String partialName, int page) {
return addType(getRecent("litebans_kicks", userType, partialName, page), "kick");
}
private List<HistoryRecord> getRecentMutes(UserType userType, String partialName, int page) {
private @NotNull List<HistoryRecord> getRecentMutes(UserType userType, String partialName, int page) {
return addType(getRecent("litebans_mutes", userType, partialName, page), "mute");
}
private List<HistoryRecord> getRecentWarns(UserType userType, String partialName, int page) {
private @NotNull List<HistoryRecord> getRecentWarns(UserType userType, String partialName, int page) {
return addType(getRecent("litebans_warnings", userType, partialName, page), "warn");
}
private List<HistoryRecord> addType(List<HistoryRecord> historyRecords, String type) {
private @NotNull List<HistoryRecord> addType(@NotNull List<HistoryRecord> historyRecords, String type) {
historyRecords.forEach(historyRecord -> historyRecord.setType(type));
return historyRecords;
}

View File

@ -1,10 +1,11 @@
package com.alttd.altitudeweb.database.litebans;
import com.alttd.altitudeweb.type_handler.UUIDTypeHandler;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.Select;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.UUID;
@ -14,10 +15,11 @@ public interface UUIDHistoryMapper {
int PAGE_SIZE = 10;
@Results({
@Result(property = "uuid", column = "uuid", javaType = UUID.class, typeHandler = UUIDTypeHandler.class),
@Result(property = "id", column = "id"),
@Result(property = "uuid", column = "uuid"),
@Result(property = "punishedName", column = "punished_name"),
@Result(property = "reason", column = "reason"),
@Result(property = "bannedByUuid", column = "banned_by_uuid", javaType = UUID.class, typeHandler = UUIDTypeHandler.class),
@Result(property = "bannedByUuid", column = "banned_by_uuid"),
@Result(property = "bannedByName", column = "banned_by_name"),
@Result(property = "removedByName", column = "removed_by_name"),
@Result(property = "time", column = "time"),
@ -26,7 +28,7 @@ public interface UUIDHistoryMapper {
@Result(property = "type", column = "type")
})
@Select("""
SELECT all_punishments.uuid, user_lookup.name AS punished_name, reason, banned_by_uuid, banned_by_name,
SELECT id, all_punishments.uuid, user_lookup.name AS punished_name, reason, banned_by_uuid, banned_by_name,
removed_by_name, time, until, removed_by_reason, type
FROM all_punishments
INNER JOIN user_lookup
@ -41,10 +43,11 @@ public interface UUIDHistoryMapper {
@Param("offset") int offset);
@Results({
@Result(property = "uuid", column = "uuid", javaType = UUID.class, typeHandler = UUIDTypeHandler.class),
@Result(property = "id", column = "id"),
@Result(property = "uuid", column = "uuid"),
@Result(property = "punishedName", column = "punished_name"),
@Result(property = "reason", column = "reason"),
@Result(property = "bannedByUuid", column = "banned_by_uuid", javaType = UUID.class, typeHandler = UUIDTypeHandler.class),
@Result(property = "bannedByUuid", column = "banned_by_uuid"),
@Result(property = "bannedByName", column = "banned_by_name"),
@Result(property = "removedByName", column = "removed_by_name"),
@Result(property = "time", column = "time"),
@ -52,7 +55,7 @@ public interface UUIDHistoryMapper {
@Result(property = "removedByReason", column = "removed_by_reason")
})
@Select("""
SELECT punishment.uuid, user_lookup.name AS punished_name, reason, banned_by_uuid, banned_by_name,
SELECT punishment.id, punishment.uuid, user_lookup.name AS punished_name, reason, banned_by_uuid, banned_by_name,
removed_by_name, time, until, removed_by_reason
FROM ${tableName} AS punishment
INNER JOIN user_lookup ON user_lookup.uuid = punishment.uuid
@ -66,7 +69,8 @@ public interface UUIDHistoryMapper {
@Param("limit") int limit,
@Param("offset") int offset);
default List<HistoryRecord> getRecent(HistoryType historyType, UserType userType, UUID uuid, int page) {
default List<HistoryRecord> getRecent(@NotNull HistoryType historyType, @NotNull UserType userType,
@NotNull UUID uuid, int page) {
return switch (historyType) {
case ALL -> getRecentAll(userType, uuid, page);
case BAN -> getRecentBans(userType, uuid, page);
@ -76,7 +80,8 @@ public interface UUIDHistoryMapper {
};
}
private List<HistoryRecord> getRecent(String tableName, UserType userType, UUID uuid, int page) {
private List<HistoryRecord> getRecent(@NotNull String tableName, @NotNull UserType userType,
@NotNull UUID uuid, int page) {
int offset = page * PAGE_SIZE;
int limit = PAGE_SIZE;
return switch (userType) {
@ -85,7 +90,7 @@ public interface UUIDHistoryMapper {
};
}
private List<HistoryRecord> getRecentAll(UserType userType, UUID uuid, int page) {
private List<HistoryRecord> getRecentAll(@NotNull UserType userType, @NotNull UUID uuid, int page) {
int offset = page * PAGE_SIZE;
int limit = PAGE_SIZE;
return switch (userType) {
@ -96,23 +101,24 @@ public interface UUIDHistoryMapper {
};
}
private List<HistoryRecord> getRecentBans(UserType userType, UUID uuid, int page) {
private @NotNull List<HistoryRecord> getRecentBans(@NotNull UserType userType, @NotNull UUID uuid, int page) {
return addType(getRecent("litebans_bans", userType, uuid, page), "ban");
}
private List<HistoryRecord> getRecentKicks(UserType userType, UUID uuid, int page) {
private @NotNull List<HistoryRecord> getRecentKicks(@NotNull UserType userType, @NotNull UUID uuid, int page) {
return addType(getRecent("litebans_kicks", userType, uuid, page), "kick");
}
private List<HistoryRecord> getRecentMutes(UserType userType, UUID uuid, int page) {
private @NotNull List<HistoryRecord> getRecentMutes(@NotNull UserType userType, @NotNull UUID uuid, int page) {
return addType(getRecent("litebans_mutes", userType, uuid, page), "mute");
}
private List<HistoryRecord> getRecentWarns(UserType userType, UUID uuid, int page) {
private @NotNull List<HistoryRecord> getRecentWarns(@NotNull UserType userType, @NotNull UUID uuid, int page) {
return addType(getRecent("litebans_warnings", userType, uuid, page), "warn");
}
private List<HistoryRecord> addType(List<HistoryRecord> historyRecords, String type) {
@Contract("_, _ -> param1")
private @NotNull List<HistoryRecord> addType(@NotNull List<HistoryRecord> historyRecords, @NotNull String type) {
historyRecords.forEach(historyRecord -> historyRecord.setType(type));
return historyRecords;
}

View File

@ -18,6 +18,7 @@ public class InitializeLiteBans {
configuration.addMapper(NameHistoryMapper.class);
configuration.addMapper(UUIDHistoryMapper.class);
configuration.addMapper(HistoryCountMapper.class);
configuration.addMapper(IdHistoryMapper.class);
}).join()
.runQuery(sqlSession -> {
createAllPunishmentsView(sqlSession);
@ -29,15 +30,15 @@ public class InitializeLiteBans {
private static void createAllPunishmentsView(SqlSession sqlSession) {
String query = """
CREATE VIEW IF NOT EXISTS all_punishments AS
SELECT uuid, reason, banned_by_uuid, banned_by_name, removed_by_name, time, until, removed_by_reason,
SELECT id, uuid, reason, banned_by_uuid, banned_by_name, removed_by_name, time, until, removed_by_reason,
'ban' as type
FROM litebans_bans
UNION ALL
SELECT uuid, reason, banned_by_uuid, banned_by_name, removed_by_name, time, until, removed_by_reason,
SELECT id, uuid, reason, banned_by_uuid, banned_by_name, removed_by_name, time, until, removed_by_reason,
'mute' as type
FROM litebans_mutes
UNION ALL
SELECT uuid, reason, banned_by_uuid, banned_by_name, removed_by_name, time, until, removed_by_reason,
SELECT id, uuid, reason, banned_by_uuid, banned_by_name, removed_by_name, time, until, removed_by_reason,
'warn' as type
FROM litebans_warnings
ORDER BY time DESC;

View File

@ -45,6 +45,10 @@ export const routes: Routes = [
path: 'bans',
loadComponent: () => import('./bans/bans.component').then(m => m.BansComponent)
},
{
path: 'bans/:type/:id',
loadComponent: () => import('./bans/details/details.component').then(m => m.DetailsComponent)
},
{
path: 'economy',
loadComponent: () => import('./economy/economy.component').then(m => m.EconomyComponent)
@ -52,6 +56,14 @@ export const routes: Routes = [
{
path: 'claiming',
loadComponent: () => import('./claiming/claiming.component').then(m => m.ClaimingComponent)
},
{
path: 'mypet',
loadComponent: () => import('./mypet/mypet.component').then(m => m.MypetComponent)
},
{
path: 'warps',
loadComponent: () => import('./warps/warps.component').then(m => m.WarpsComponent)
}
];

View File

@ -57,7 +57,8 @@
</div>
<div class="historyTable">
<app-history [userType]="userType" [punishmentType]="punishmentType"
[page]="page" [searchTerm]="finalSearchTerm" (pageChange)="updatePageSize($event)">
[page]="page" [searchTerm]="finalSearchTerm" (pageChange)="updatePageSize($event)"
(selectItem)="setSearch($event)">
</app-history>
</div>
<div class="changePageButtons">

View File

@ -5,6 +5,7 @@ import {HistoryCount, HistoryService} from '../../api';
import {NgClass, NgForOf, NgIf} from '@angular/common';
import {FormsModule} from '@angular/forms';
import {catchError, map, Observable} from 'rxjs';
import {SearchParams} from './search-terms';
@Component({
selector: 'app-bans',
@ -210,4 +211,11 @@ export class BansComponent implements OnInit {
this.actualPage = this.page;
}
}
public setSearch(searchParams: SearchParams) {
this.userType = searchParams.userType
this.searchTerm = searchParams.searchTerm
this.punishmentType = searchParams.punishmentType
this.search();
}
}

View File

@ -0,0 +1,62 @@
<ng-container>
<app-header [current_page]="'bans'" height="200px" background_image="/public/img/backgrounds/staff.png"
[overlay_gradient]="0.5">>
<div class="title" header-content>
<h1>Minecraft Punishments</h1>
</div>
</app-header>
<ng-container *ngIf="punishment === undefined">
<p>Loading...</p>
</ng-container>
<ng-container *ngIf="punishment">
<table [cellSpacing]="0">
<div>
<p>type: {{ this.historyFormat.getType(punishment) }}</p>
<p>is active: {{ this.historyFormat.isActive(punishment) }}</p>
<tbody>
<tr>
<td>Player</td>
<td>
<div class="playerContainer">
<img class="avatar" [ngSrc]="this.historyFormat.getAvatarUrl(punishment.uuid)" width="25" height="25"
alt="{{punishment.username}}'s Minecraft skin">
<span class="username">{{ punishment.username }}</span>
</div>
</td>
</tr>
<tr>
<td>Moderator</td>
<td>
<div class="playerContainer">
<img class="avatar" [ngSrc]="this.historyFormat.getAvatarUrl(punishment.punishedByUuid)" width="25"
height="25"
alt="{{punishment.punishedBy}}'s Minecraft skin">
<span class="username">{{ punishment.punishedBy }}</span>
</div>
</td>
</tr>
<tr>
<td>Reason</td>
<td>{{ punishment.reason | removeTrailingPeriod }}</td>
</tr>
<tr>
<td>Date</td>
<td>{{ this.historyFormat.getPunishmentTime(punishment) }}</td>
</tr>
<tr>
<td>Expires</td>
<td>{{ this.historyFormat.getExpiredTime(punishment) }}</td>
</tr>
<ng-container *ngIf="punishment.removedBy !== undefined && punishment.removedBy.length > 0">
<tr>
<td>Un{{ this.historyFormat.getType(punishment).toLocaleLowerCase() }} reason</td>
<td>{{ punishment.removedReason == null ? 'No reason specified' : punishment.removedReason }}</td>
</tr>
</ng-container>
</tbody>
</div>
</table>
</ng-container>
</ng-container>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DetailsComponent } from './details.component';
describe('DetailsComponent', () => {
let component: DetailsComponent;
let fixture: ComponentFixture<DetailsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [DetailsComponent]
})
.compileComponents();
fixture = TestBed.createComponent(DetailsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,69 @@
import {Component, OnInit} from '@angular/core';
import {HistoryService, PunishmentHistory} from '../../../api';
import {NgIf, NgOptimizedImage} from '@angular/common';
import {RemoveTrailingPeriodPipe} from '../../util/RemoveTrailingPeriodPipe';
import {HistoryFormatService} from '../history-format.service';
import {ActivatedRoute} from '@angular/router';
import {catchError, map} from 'rxjs';
import {HeaderComponent} from '../../header/header.component';
@Component({
selector: 'app-details',
imports: [
NgIf,
NgOptimizedImage,
RemoveTrailingPeriodPipe,
HeaderComponent
],
templateUrl: './details.component.html',
styleUrl: './details.component.scss'
})
export class DetailsComponent implements OnInit {
type: 'all' | 'ban' | 'mute' | 'kick' | 'warn' = 'all';
id: number = -1;
punishment: PunishmentHistory | undefined;
constructor(private route: ActivatedRoute,
private historyApi: HistoryService,
public historyFormat: HistoryFormatService) {
}
ngOnInit(): void {
this.route.paramMap.subscribe(params => {
switch (params.get('type')) {
case 'ban':
this.type = 'ban';
break;
case 'mute':
this.type = 'mute';
break;
case 'kick':
this.type = 'kick';
break;
case 'warn':
this.type = 'warn';
break;
default:
throw new Error("Invalid type");
}
this.id = Number(params.get('id') || '-1');
this.loadPunishmentData();
});
}
private loadPunishmentData() {
if (!this.type || this.type === 'all' || this.id < 0 || isNaN(this.id)) {
console.error("Invalid type or id");
return;
}
this.historyApi.getHistoryById(this.type, this.id).pipe(
map(punishment => {
this.punishment = punishment;
}),
catchError(err => {
console.error(err);
return [];
})
).subscribe();
}
}

View File

@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { HistoryFormatService } from './history-format.service';
describe('HistoryFormatService', () => {
let service: HistoryFormatService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(HistoryFormatService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,58 @@
import {Injectable} from '@angular/core';
import {PunishmentHistory} from '../../api';
@Injectable({
providedIn: 'root'
})
export class HistoryFormatService {
constructor() {
}
public getPunishmentTime(entry: PunishmentHistory) {
const date = new Date(entry.punishmentTime);
return date.toLocaleString(navigator.language, {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: false
});
}
public isActive(entry: PunishmentHistory): boolean {
return entry.expiryTime > Date.now();
}
public getType(entry: PunishmentHistory): string {
return entry.type.charAt(0).toUpperCase() + entry.type.slice(1);
}
public getExpiredTime(entry: PunishmentHistory): string {
let suffix: string = '';
if (entry.removedBy !== null && entry.removedBy !== undefined) {
suffix = '(' + entry.removedBy + ')';
}
if (entry.expiryTime <= 0) {
return "Permanent " + this.getType(entry) + " " + suffix;
}
const date = new Date(entry.expiryTime);
return date.toLocaleString(navigator.language, {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: false
}) + " " + suffix;
}
public getAvatarUrl(entry: string): string {
let uuid = entry.replace('-', '');
if (uuid === 'C') {
uuid = "f78a4d8dd51b4b3998a3230f2de0c670"
}
return `https://crafatar.com/avatars/${uuid}?size=25&overlay`;
}
}

View File

@ -19,24 +19,32 @@
<div>
<tbody>
<tr class="historyPlayerRow" *ngFor="let entry of history">
<td class="historyType">{{ getType(entry) }}</td>
<td class="historyPlayer">
<td class="historyType" (click)="showDetailedPunishment(entry)">
{{ this.historyFormat.getType(entry) }}
</td>
<td class="historyPlayer" (click)="setSearch(entry.type, entry.username, 'player')">
<div class="playerContainer">
<img class="avatar" [ngSrc]="getAvatarUrl(entry.uuid)" width="25" height="25"
<img class="avatar" [ngSrc]="this.historyFormat.getAvatarUrl(entry.uuid)" width="25" height="25"
alt="{{entry.username}}'s Minecraft skin">
<span class="username">{{ entry.username }}</span>
</div>
</td>
<td class="historyPlayer">
<td class="historyPlayer" (click)="setSearch(entry.type, entry.punishedBy, 'staff')">
<div class="playerContainer">
<img class="avatar" [ngSrc]="getAvatarUrl(entry.punishedByUuid)" width="25" height="25"
<img class="avatar" [ngSrc]="this.historyFormat.getAvatarUrl(entry.punishedByUuid)" width="25" height="25"
alt="{{entry.punishedBy}}'s Minecraft skin">
<span>{{ entry.punishedBy }}</span>
</div>
</td>
<td class="historyReason">{{ entry.reason | removeTrailingPeriod }}</td>
<td class="historyDate">{{ getPunishmentTime(entry) }}</td>
<td class="historyDate">{{ getExpiredTime(entry) }}</td>
<td class="historyReason" (click)="showDetailedPunishment(entry)">
{{ entry.reason | removeTrailingPeriod }}
</td>
<td class="historyDate" (click)="showDetailedPunishment(entry)">
{{ this.historyFormat.getPunishmentTime(entry) }}
</td>
<td class="historyDate" (click)="showDetailedPunishment(entry)">
{{ this.historyFormat.getExpiredTime(entry) }}
</td>
</tr>
</tbody>
</div>

View File

@ -62,6 +62,7 @@ table tr td {
.historyPlayerRow:hover {
background-color: var(--history-table-row-color);
cursor: pointer;
}
.historyTableHead {

View File

@ -1,11 +1,14 @@
import {Component, EventEmitter, Input, OnChanges, OnInit, Output} from '@angular/core';
import {BASE_PATH, HistoryService, PunishmentHistoryInner} from '../../../api';
import {BASE_PATH, HistoryService, PunishmentHistory} from '../../../api';
import {catchError, map, Observable, shareReplay} from 'rxjs';
import {NgForOf, NgIf, NgOptimizedImage} from '@angular/common';
import {CookieService} from 'ngx-cookie-service';
import {RemoveTrailingPeriodPipe} from '../../util/RemoveTrailingPeriodPipe';
import {HttpErrorResponse} from '@angular/common/http';
import {environment} from '../../../environments/environment';
import {HistoryFormatService} from '../history-format.service';
import {SearchParams} from '../search-terms';
import {Router} from '@angular/router';
@Component({
selector: 'app-history',
@ -30,12 +33,13 @@ export class HistoryComponent implements OnInit, OnChanges {
@Input() searchTerm: string = '';
@Output() pageChange = new EventEmitter<number>();
@Output() selectItem = new EventEmitter<SearchParams>();
private uuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
public history: PunishmentHistoryInner[] = []
public history: PunishmentHistory[] = []
constructor(private historyApi: HistoryService) {
constructor(private historyApi: HistoryService, public historyFormat: HistoryFormatService, private router: Router) {
}
ngOnChanges(): void {
@ -47,7 +51,7 @@ export class HistoryComponent implements OnInit, OnChanges {
}
private reloadHistory(): void {
let historyObservable: Observable<PunishmentHistoryInner[]>;
let historyObservable: Observable<PunishmentHistory[]>;
if (this.searchTerm.length === 0) {
historyObservable = this.historyApi.getHistoryForAll(this.userType, this.punishmentType, this.page);
} else {
@ -89,42 +93,31 @@ export class HistoryComponent implements OnInit, OnChanges {
).subscribe();
}
public getPunishmentTime(entry: PunishmentHistoryInner) {
const date = new Date(entry.punishmentTime);
return date.toLocaleString(navigator.language, {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: false
});
setSearch(type: PunishmentHistory.TypeEnum, name: string, userType: 'player' | 'staff') {
let punishmentType: 'all' | 'ban' | 'mute' | 'kick' | 'warn' = 'all';
switch (type) {
case 'ban':
punishmentType = 'ban';
break;
case 'mute':
punishmentType = 'mute';
break;
case 'kick':
punishmentType = 'kick';
break;
case 'warn':
punishmentType = 'warn';
break;
}
let searchParams: SearchParams = {
userType: userType,
punishmentType: punishmentType,
searchTerm: name
}
this.selectItem.emit(searchParams);
}
public getType(entry: PunishmentHistoryInner) {
return entry.type.charAt(0).toUpperCase() + entry.type.slice(1);
}
public getExpiredTime(entry: PunishmentHistoryInner) {
if (entry.expiryTime <= 0) {
return "Permanent " + this.getType(entry);
}
const date = new Date(entry.expiryTime);
return date.toLocaleString(navigator.language, {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: false
});
}
public getAvatarUrl(entry: string): string {
let uuid = entry.replace('-', '');
if (uuid === 'C') {
uuid = "f78a4d8dd51b4b3998a3230f2de0c670"
}
return `https://crafatar.com/avatars/${uuid}?size=25&overlay`;
public showDetailedPunishment(entry: PunishmentHistory) {
this.router.navigate([`bans/${entry.type}/${entry.id}`]).then();
}
}

View File

@ -0,0 +1,5 @@
export interface SearchParams {
userType: 'player' | 'staff';
searchTerm: string;
punishmentType: 'all' | 'ban' | 'mute' | 'kick' | 'warn';
}

View File

@ -0,0 +1,221 @@
<ng-container>
<app-header [current_page]="'mypet'" height="460px" background_image="/public/img/backgrounds/babywither.png"
[overlay_gradient]="0.5">
<div class="title" header-content>
<h1>MyPet</h1>
<h2>Tame almost any mob in-game as your pet and train it to fight along your side, collect dropped items for you,
and more!</h2>
</div>
</app-header>
<main>
<section class="darkmodeSection">
<section class="columnSection">
<div class="columnContainer">
<div class="columnParagraph">
<h2>Claim a Pet</h2>
<p>To claim a pet you will need to find the mob you want in the survival world and <span
style="font-family: 'opensans-bold', sans-serif;">kill it with a lead in your hand</span>. You must have a
lead in your hand when you kill the mob or it will die instead of becoming your pet.</p>
<p>New players can have up to 3 pets in storage and 1 active pet at a time. More pets can be stored by
ranking up. To claim an additional pet you must first store your current pet with <span
style="font-family: 'opensans-bold', sans-serif;">/petstore</span>. Once you have stored your current
pet you can simply kill another mob to make it your pet. You will not be able to call it or interact with
your other pets while they are in storage. To interact with your MyPet you will need to make it your
active MyPet by doing <span style="font-family: 'opensans-bold', sans-serif;">/petswitch</span> and
selecting the one you want to use.</p>
<img ngSrc="/public/img/items/lead.png" alt="Minecraft lead/leash" style="width: 20%;" height="96"
width="96">
</div>
<div class="columnParagraph">
<h2>Skilltrees and Levels</h2>
<p>Pets can be assigned to a skilltree with <span
style="font-family: 'opensans-bold', sans-serif;">/pcst</span>, which allows it to level up and unlock new
skills. There are 8 skilltrees to choose from, but Mage, Tank, Marksman, and Warrior all require you to
start with "Fighter". Pets earn XP when they kill mobs, and also earn a small amount of XP any time you
kill a mob.</p>
<p><b>Note:</b> pets with inventories will drop it on player death.</p>
<ul>
<li><span style="font-family: 'opensans-bold', sans-serif;">Utility</span> - Specializes in mining with
pickup and inventory skills + a Haste II beacon at level 150. (150 levels)
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">Mount</span> - A rideable pet specializes in
speed and jump height + jump beacon at level 150, and inventory at level 200. (200 levels)
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">Defender</span> - Specializes in defending
you, it absorbs damage dealt towards you. High HP + regen beacon at level 200. (200 levels)
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">Fighter</span> - Starter rank for all other
fighter categories. Increases pet attack damage. (50 levels)
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">Mage</span> - Specializes in specialty attacks
like fireballs and lightning. No melee abilities. (100 levels)
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">Tank</span> - Specializes in defense with
lots of HP, redirecting damage, and sweeping attacks. Resistance beacon at level 100. (100 levels)
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">Marksman</span> - Specializes in ranged
attacks with arrow and poison damage, no melee abilities + invisibility beacon at level 50. (100 levels)
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">Warrior</span> - An all-around class with no
speciality, has some skills from every other class + a strength beacon at level 100. (100 levels)
</li>
</ul>
</div>
<div class="columnParagraph">
<h2>Equip Your Pet</h2>
<p>Some pets can hold items in their hands or wear armor. To equip a pet with something to hold or wear,
simply right click on your pet while shifting with the item in your hand. To remove the equipment right
click on your MyPet while shifting with shears in your hand.</p>
</div>
<div class="columnParagraph">
<h2>Naming Your Pet</h2>
<p>You can name your pet with <span
style="font-family: 'opensans-bold', sans-serif;">/petname &lt;name&gt;</span>. Please note that
inappropriate names (anything that wouldn't be allowed to be said in chat) are not allowed.</p>
<p>Duke members and above can change the color of their pet's name. To get a list of all the color codes
please do <span style="font-family: 'opensans-bold', sans-serif;">/colors</span> in-game.</p>
</div>
<div class="columnParagraph">
<h2>Pet Behavior</h2>
<p>You can change the way your pet attacks by changing its behavior. Different behaviors are unlocked as
your pet levels up. The available behaviors are listed below.</p>
<ul>
<li><span style="font-family: 'opensans-bold', sans-serif;">Friendly:</span> Won't fight, even if it's
attacked by anything
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">Normal:</span> Acts like a normal wolf</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">Aggressive:</span> Attacks everything within
15 blocks of the owner
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">Farm:</span> Attacks every hostile mob within
15 blocks of the owner
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">Raid:</span> Like normal, but the MyPet won't
attack players and their pets
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">Duel</span> Attacks other pets with active
duel behavior within a 5 block radius
</li>
</ul>
</div>
</div>
<div class="columnContainer">
<div class="columnParagraph">
<h2>Feed Your Pet</h2>
<p>You must feed your pet to keep its health up or it will die from hunger! If your pet is hungry it will
also be weaker and will deal less damage when attacking.</p>
<p>Each pet has a special food that it must be fed. You can see which food is right for your pet by doing
<span style="font-family: 'opensans-bold', sans-serif;">/petinfo</span>.</p>
<div class="inlineIcons">
<img ngSrc="/public/img/items/potato.png" alt="Minecraft potato" height="81" width="77">
<img ngSrc="/public/img/items/carrot.png" alt="Minecraft carrot" height="81" width="89">
<img ngSrc="/public/img/items/steak.png" alt="Minecraft steak" height="81" width="88">
</div>
</div>
<div class="columnParagraph">
<h2>How to Hide/Disable a MyPet</h2>
<p>There are two ways to disable a pet, and they work very differently. The first, <span
style="font-family: 'opensans-bold', sans-serif;">/petsendaway</span>, will hide your active MyPet until
you un-hide it with <span style="font-family: 'opensans-bold', sans-serif;">/petcall</span>. A pet that
has been hidden is still considered your "active MyPet", it just isn't visible right now. It also only
hides your pet during your current session.</p>
<p>The second way to disable a pet is to put it into storage with <span
style="font-family: 'opensans-bold', sans-serif;">/petstore</span>. Pets that are in storage are
completely deactivated until you use <span
style="font-family: 'opensans-bold', sans-serif;">/petswitch</span> to choose one you want to use. If
all of your pets are in storage, the server will tell you that you do not have a MyPet. This is also how
you can claim additional MyPets - by completely deactivating your current one first.</p>
</div>
<div class="columnParagraph">
<h2>Blocked Mob Types</h2>
<p>The following mobs are not allowed to be claimed as a pet: the enderdragon, villagers and villager-like
mobs, shulkers, ghasts, and elder guardians. If you kill any of these mobs it will die and will NOT become
your pet.</p>
<p>Furthermore, all horses (including donkeys and mules) are currently disabled as pets due to them being
buggy.</p>
<div class="inlineIcons">
<img ngSrc="/public/img/items/enderdragon.png"
style="padding: 25px 25px;"
alt="Minecraft enderdragon mob"
height="100" width="100">
<img ngSrc="/public/img/items/villager.png"
style="padding: 25px 25px;"
alt="Minecraft villager mob"
height="100" width="100">
<img ngSrc="/public/img/items/zombievillager.png"
style="padding: 25px 25px;"
alt="Minecraft zombie villager mob"
height="100" width="100">
<img ngSrc="/public/img/items/shulker.png"
style="padding: 25px 25px;"
alt="Minecraft shulker mob"
height="100" width="100">
<img ngSrc="/public/img/items/ghast.png"
style="padding: 25px 25px;"
alt="Minecraft ghast mob"
height="100" width="100">
<img ngSrc="/public/img/items/elderguardian.png"
style="padding: 25px 25px;"
alt="Minecraft elder guardian mob"
height="100" width="100">
</div>
</div>
<div class="columnParagraph">
<h2>Disclaimer</h2>
<p>When you die with your MyPet out, the MyPet will drop all of the items in its inventory. This means that
if your MyPet is in a dangerous area when you die, your items can be lost. And if you die in pvp the items
won't be protected. If your MyPet dies on its own, it will not drop its inventory.</p>
<p>Some skill trees require a base skill to be able to select them. The fighter skill tree is a base skill
and will not level beyond 50. When you reach level 50, you will need to switch to one of the secondary
skill trees with <span style="font-family: 'opensans-bold', sans-serif;">/pcst.</span></p>
</div>
<div class="columnParagraph">
<h2>Useful Commands</h2>
<ul>
<li><span style="font-family: 'opensans-bold', sans-serif;">/petcall -</span> Make your pet come to you if
it vanished
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/petsendaway -</span> Make your pet vanish
temporarily
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/petrelease &lt;name&gt;-</span> Release
your pet forever
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/petinfo -</span> Check your pet's health and
food as well as other stats
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/pcst -</span> Choose a skilltree for your
pet.
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/petskill -</span> Check your pet's skills and
strengths
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/petname &lt;name&gt;-</span> Change the name
of your pet (see Naming Your Pet above)
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/petinventory -</span> Open your pet's
inventory to see what it has picked up
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/petpickup -</span> Toggle whether your pet
will pick up items or not
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/petbehavior -</span> Change the behavior
class of your pet (see Pet Behavior above)
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/petbeacon -</span> Turn your pet into a
walking beacon to give you strength, speed, and other power-ups
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/petswitch -</span> Allows you to switch
between your MyPets
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/petstore -</span> Store your current pet to
claim another
</li>
</ul>
</div>
</div>
</section>
</section>
</main>
</ng-container>

View File

@ -0,0 +1,15 @@
main ul {
font-family: opensans, sans-serif;
text-align: left;
}
main li {
margin-left: 30px;
padding-bottom: 10px;
}
.inlineIcons {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
}

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MypetComponent } from './mypet.component';
describe('MypetComponent', () => {
let component: MypetComponent;
let fixture: ComponentFixture<MypetComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MypetComponent]
})
.compileComponents();
fixture = TestBed.createComponent(MypetComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,16 @@
import {Component} from '@angular/core';
import {HeaderComponent} from '../header/header.component';
import {NgOptimizedImage} from '@angular/common';
@Component({
selector: 'app-mypet',
imports: [
HeaderComponent,
NgOptimizedImage
],
templateUrl: './mypet.component.html',
styleUrl: './mypet.component.scss'
})
export class MypetComponent {
}

View File

@ -0,0 +1,223 @@
<ng-container>
<app-header [current_page]="'economy'" height="460px" background_image="/public/img/backgrounds/path.jpg"
[overlay_gradient]="0.5">
<div class="title" header-content>
<h1>Player Warps</h1>
<h2>Set up your own public warp to let everyone visit your town, shop, and more!</h2>
</div>
</app-header>
<main>
<section class="darkmodeSection">
<section class="columnSection">
<div class="columnContainer">
<div class="columnParagraph">
<h2>What are Warps?</h2>
<p>Warps allow everyone to teleport to a specific place within the world. They work just like homes
(/sethome), but everyone has access to the warps. We are very excited to have our own custom warp system
that allows players to easily apply for their own warp wherever they want and maintain their warp (the
name, description, and icon) on their own! This lets you easily promote your town, shop, farm, or just
about anything else.</p>
<p>All warps are separated into categories so it's easy to find the category of warp you're looking for.</p>
<img ngSrc="/public/img/random/warpgui.png"
alt="In-game warp GUI"
style="width: 80%; padding-bottom: 15px;"
height="232" width="356">
</div>
</div>
<div class="columnContainer">
<div class="columnParagraph">
<h2>Applying for a Warp</h2>
<p>Once you've built something that you want to share, and made sure that it meets the "Warp Requirements",
you can apply for a warp in-game! Applying is easy, just do <span
style="font-family: 'opensans-bold', sans-serif;">/warps apply</span> and follow the prompts.</p>
<p>The application process will ask you for a name, description, icon, category, and location for your warp.
Before beginning the application process, be sure you are standing where you want the warp to be! Make
sure you are also facing the direction you want players to face when they teleport to your warp. Don't
look at the ground! Your warp name must be under 30 characters and your description must be under 120. Do
not use any color codes. When it asks for the item you want to have as an icon, simply hold the desired
item in your hand.</p>
<p><span style="font-family: 'opensans-bold', sans-serif;">Attention</span>: Warps cost $25000. If your
application is denied, you will be refunded $15000. To continue, type continue, or to cancel, type cancel.
However, if your warp is approved, but you fail to maintain it, your warp will be deleted without a
refund!</p>
</div>
</div>
</section>
<section class="columnSection" style="padding-top: 0;">
<div class="columnParagraph" style="padding-left: 15px;">
<h2>Warp Requirements</h2>
<p>You need to be the owner of the claim your warp is placed in. It should look good, and be as finished as
your warp type allows you to have it. Safety is an important aspect as well, visitors should not be
accidently dying by falling in dangerous areas or having mobs spawn on them. Phantoms are an exception to
this rule as you can't easily prevent those spawns. Your warp should be in one claim*, and divided with
subclaims if you need to allow different levels of trust in different areas (house plots in towns, shop
plots in malls, etc).</p>
<p><sub>*If a warp cannot be done in one claim due to surrounding claims, you are allowed to use up to 3
separate claims to claim everything. If you can reduce the total size of your claim by at least roughly 30%
by using an extra claim, you may do so for up to a total of 3 claims.</sub></p>
<p>It should be easy for players to find where they want to go. You can accomplish this through signs or by
designing it in such a way that players can see their destination when they warp in. Your warp's claim
border should be 500 blocks away from any other warp's claim border, and two warps can not lead to the same
area. We encourage making it possible for players to leave the warp, either by walking or elytra use, so
that they can use it as a starting point for exploring as well.</p>
<p>If you have any farms in the area of your warp that players don't benefit from through your warp it should
not be possible for players to load those farms during regular use. We don't want players being used as
chunkloaders.</p>
<p>It's important to make sure your warp is as lag friendly as possible. Our rules on lag can be found here on
the <a [routerLink]="['/lag']">lag</a> page. If we find your warp creates more lag than necessary for its
function
we will deny the application.</p>
</div>
<div class="columnContainer">
<div class="columnParagraph">
<h3>Towns</h3>
<span
style="font-family: 'opensans-bold', sans-serif;">Any warp that is designed to house other players</span>
<ul>
<li>It should be clear how to navigate the town and how to get a plot</li>
<li>You need to be active enough to regularly assign players new plots</li>
<li>Your town must have a community that actively participates in your town by living/building there or
using your towns features both before applying and while it's a warp (Minimum 3 active players other
than you)
</li>
<li>Players should be able to leave the town area in order to use the warp as a travel hub</li>
</ul>
</div>
<div class="columnParagraph">
<h3>Shops</h3>
<span style="font-family: 'opensans-bold', sans-serif;">Any warp that sells/trades/buys items using shops. This also includes malls.</span>
<ul>
<li>It should be clear where to go to find the items that are being sold from the warp spawn point</li>
<li>Your shops should sell items players want and arent readily available elsewhere</li>
<li>Your prices should be competitive with spawn and other warps</li>
<li>You are responsible for keeping stock up. Staff will regularly check random shops within your warp, if
they find understocked shops they will issue warnings for it and eventually remove it. This counts for
mall owners as well!
</li>
<li>Players should be able to leave the area in order to use the warp as a travel hub</li>
</ul>
<span style="font-family: 'opensans-bold', sans-serif;">General stock requirements</span>
<ul>
<li>Easily obtainable items such as concrete: full chest</li>
<li>Hard to obtain, common items such as diamonds: 1 stack</li>
<li>Hard to obtain, uncommon items such as netherite/beacons: 16</li>
<li>Maps: 16 for 1x1, 8 for anything over 1x1 up to 2x2, and 4 for anything over 2x2</li>
</ul>
</div>
<div class="columnParagraph">
<h2>Warp Notes</h2>
<p><span style="font-family: 'opensans-bold', sans-serif;">-</span> If a new warp application has
improvements or additional, useful features that a current warp lacks, the current warp may be replaced
with the new one. For example, if there already is a single-spawner spider farm that offers drops at a
certain price, and a warp application is submitted for a spider farm with multiple spawners and offers
free drops, the new warp is likely to be favored.</p>
<p><span style="font-family: 'opensans-bold', sans-serif;">-</span> Warps that are malls (primarily focused
on having many shop plots for many players) are limited to 2.</p>
<p><span style="font-family: 'opensans-bold', sans-serif;">-</span> Smaller shop warps are limited to 1 shop
per type. For example, only 1 warp for redstone-related shops.</p>
<p><span style="font-family: 'opensans-bold', sans-serif;">-</span> In order to always keep our warps fair
and updated, we will automatically remove a warp if the warp owner has been offline for 30+ days. We need
to be able to contact our warp owners in a timely manner incase any issues with the warp happen to pop up.
If we notice that a player is fairly inactive and another warp application comes in for a similar warp,
the application will be more likely to be accepted if the warp meets all of our requirements. If you plan
on being gone for an extended period of time, 14+ days, you can let a staff member know and they will note
it down and try to work with you.</p>
<p><span style="font-family: 'opensans-bold', sans-serif;">-</span> The staff team does weekly warp checks
to make sure our warps continue to stay up to meet our requirements. They will mail a warp owner if they
notice an issue (low stock, sudden lighting issue, farm isn't working, etc.), and we expect all issues to
be fixed within a week of receiving that mail. If an issue goes unfixed, we will remove the warp due to
the lack of upkeep. If the issue is something you need help fixing or you're unable to work on it during
the week, you can reach out to any member of the staff team and they will work with you!</p>
</div>
<div class="columnParagraph">
<h2>Useful Commands</h2>
<ul>
<li><span style="font-family: 'opensans-bold', sans-serif;">/warps -</span> Open a GUI showing all warps
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/warps apply -</span> Apply for your own warp
</li>
</ul>
</div>
</div>
<div class="columnContainer">
<div class="columnParagraph">
<h3>Farms</h3>
<span style="font-family: 'opensans-bold', sans-serif;">Any warps that give XP</span>
<ul>
<li>It should be easy to find where to go and how to use the farm</li>
<li>Players should have access to all of the drops either through shops with competitive prices, or for
free
</li>
<li>XP farms should not attempt to bypass our anti lag systems</li>
<li>Kill chambers should be 1x1 or 1x2, exceptions to this rule are listed below</li>
<li>Amount of farms allowed as warps:
<ul>
<li>Skeleton/spider - 1 each</li>
<li>Zombie/drowned - 1</li>
<li>Bad Omen - 1</li>
<li>Raid - 2</li>
<li>Guardian - 2</li>
<li>Endermen - 2</li>
<li>Creeper - 2</li>
<li>Shulker - 2</li>
<li>Wither skeleton - 2</li>
</ul>
</li>
<p>Exceptions can be made for this rule if the farms have different, unique designs (such as a one-player
vs two-player farm). Head Staff will approve these exceptions on a case-by-case basis</p>
</ul>
<span style="font-family: 'opensans-bold', sans-serif;">Kill chamber size exceptions</span>
<ul>
<li>Magma kill chamber may be up to 3x3</li>
<li>Hoglin kill chamber may be up to 2x2</li>
<li>Ravagers during pillager raids must be killed automatically and the kill chamber should still be 2x1
for the rest of the mobs
</li>
<li>Enderman kill chamber may be up to 3x3</li>
</ul>
</div>
<div class="columnParagraph">
<h3>Other</h3>
<span style="font-family: 'opensans-bold', sans-serif;">Any warp that doesn't fit in the other categories can go in here</span>
<ul>
<li>Warps designed for player events need to look aesthetically pleasing and accommodate a large amount of
players
</li>
<li>Casino warps should, at a minimum, display or list the prizes that can be won from each game
<ul>
<li style="padding-top: 2px;">If the game allows you to win crate items, listing the rarity of prizes
that can be won is acceptable
</li>
</ul>
</li>
<li>Casino warps should have a disclaimer posted visibly that tells players that they can lose money and
should expect to lose money playing any gambling machines
</li>
<li>Portal warps need to be the only one of its kind</li>
<li>Villager trading areas have their villagers on no AI blocks and be named public for public
accessibility
</li>
</ul>
</div>
<div class="columnParagraph">
<h2>Maintaining a Warp</h2>
<p>We've made it easy for you to maintain your warp on your own. As the warp owner you can change the name,
description, and icon anytime. There is no charge for modifying a warp. To access the GUI to manage your
warp, just do <span style="font-family: 'opensans-bold', sans-serif;">/warps</span> and click on the chest
labeled "<span style="font-family: 'opensans-bold', sans-serif;">My Warps</span>" in the bottom left
corner.</p>
<img ngSrc="/public/img/random/editwarpgui.png"
alt="In-game warp edit GUI"
style="width: 80%;"
width="384" height="170">
<p>Maintaining a warp also involves making sure it looks nice, shops are well stocked, and, if it's a town,
open plots are always available for residents to move in. Make sure you keep up on maintaining your warp
or it could be deleted! If a warp is deleted by a staff member you will not receive a refund for the
creation cost.</p>
</div>
</div>
</section>
</section>
</main>
</ng-container>

View File

@ -0,0 +1,14 @@
main ul {
font-family: opensans, sans-serif;
text-align: left;
}
main li {
margin-left: 30px;
padding-bottom: 10px;
}
.columnParagraph > span {
color: var(--font-color);
transition: 0.5s ease;
}

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { WarpsComponent } from './warps.component';
describe('WarpsComponent', () => {
let component: WarpsComponent;
let fixture: ComponentFixture<WarpsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [WarpsComponent]
})
.compileComponents();
fixture = TestBed.createComponent(WarpsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,18 @@
import {Component} from '@angular/core';
import {HeaderComponent} from '../header/header.component';
import {NgOptimizedImage} from '@angular/common';
import {RouterLink} from '@angular/router';
@Component({
selector: 'app-warps',
imports: [
HeaderComponent,
NgOptimizedImage,
RouterLink
],
templateUrl: './warps.component.html',
styleUrl: './warps.component.scss'
})
export class WarpsComponent {
}

View File

@ -26,6 +26,8 @@ paths:
$ref: './schemas/bans/bans.yml#/getTotalResultsForUuidSearch'
/history/{userType}/search-results/user/{type}/{user}:
$ref: './schemas/bans/bans.yml#/getTotalResultsForUserSearch'
/history/single/{type}/{id}:
$ref: './schemas/bans/bans.yml#/getHistoryById'
/history/total:
$ref: './schemas/bans/bans.yml#/getTotalPunishments'
/appeal/update-mail:

View File

@ -44,7 +44,7 @@ getHistoryForUsers:
content:
application/json:
schema:
$ref: '#/components/schemas/PunishmentHistory'
$ref: '#/components/schemas/PunishmentHistoryList'
default:
description: Unexpected error
content:
@ -70,7 +70,7 @@ getHistoryForAll:
content:
application/json:
schema:
$ref: '#/components/schemas/PunishmentHistory'
$ref: '#/components/schemas/PunishmentHistoryList'
default:
description: Unexpected error
content:
@ -97,7 +97,7 @@ getHistoryForUuid:
content:
application/json:
schema:
$ref: '#/components/schemas/PunishmentHistory'
$ref: '#/components/schemas/PunishmentHistoryList'
default:
description: Unexpected error
content:
@ -172,6 +172,29 @@ getTotalResultsForUuidSearch:
application/json:
schema:
$ref: '../generic/errors.yml#/components/schemas/ApiError'
getHistoryById:
get:
tags:
- history
summary: Gets history for specified id
description: Retrieves a specific history record
operationId: getHistoryById
parameters:
- $ref: '#/components/parameters/HistoryType'
- $ref: '#/components/parameters/Id'
responses:
'200':
description: Successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/PunishmentHistory'
default:
description: Unexpected error
content:
application/json:
schema:
$ref: '../generic/errors.yml#/components/schemas/ApiError'
components:
parameters:
HistoryType:
@ -211,13 +234,22 @@ components:
schema:
type: integer
description: The page that should be retrieved
Id:
name: id
in: path
required: true
schema:
type: integer
description: The id of the punishment that should be retrieved
schemas:
SearchResults:
type: integer
description: A number representing the total count of results for the search query
PunishmentHistory:
PunishmentHistoryList:
type: array
items:
$ref: '#/components/schemas/PunishmentHistory'
PunishmentHistory:
type: object
required:
- username
@ -228,6 +260,7 @@ components:
- expiryTime
- punishedBy
- punishedByUuid
- id
properties:
username:
type: string
@ -262,6 +295,9 @@ components:
removedReason:
type: string
description: The reason why the punishment was removed
id:
type: integer
description: Id of the punishment
Player:
type: object
properties: