Compare commits
19
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2be79c180a | ||
|
|
423d5e4a4c | ||
|
|
a0db55dede | ||
|
|
e0a09d303c | ||
|
|
29967d65b8 | ||
|
|
8b265514a6 | ||
|
|
e766fd1125 | ||
|
|
86a85049b3 | ||
|
|
cf73303218 | ||
|
|
d075464ded | ||
|
|
7be3b6f9d3 | ||
|
|
24e28015d3 | ||
|
|
754479eb98 | ||
|
|
5974ec1dba | ||
|
|
c5ed657d3e | ||
|
|
02adbb2522 | ||
|
|
4b466f314e | ||
|
|
6531526278 | ||
|
|
bc0739f707 |
+31
@@ -1,5 +1,7 @@
|
||||
package com.alttd.altitudeweb.controllers.data_from_auth;
|
||||
|
||||
import com.nimbusds.jwt.JWT;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.core.Authentication;
|
||||
@@ -8,8 +10,10 @@ import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class AuthenticatedUuid {
|
||||
@Value("${UNSECURED:#{false}}")
|
||||
@@ -25,6 +29,9 @@ public class AuthenticatedUuid {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
|
||||
if (authentication == null || !(authentication.getPrincipal() instanceof Jwt jwt)) {
|
||||
log.error("Authentication principal is null {} or not a JWT {}",
|
||||
authentication == null, authentication == null ?
|
||||
"null" : authentication.getPrincipal() instanceof JWT);
|
||||
if (unsecured) {
|
||||
return UUID.fromString("55e46bc3-2a29-4c53-850f-dbd944dc5c5f");
|
||||
}
|
||||
@@ -39,4 +46,28 @@ public class AuthenticatedUuid {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid UUID format");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the authenticated user's UUID from the JWT token.
|
||||
*
|
||||
* @return The UUID of the authenticated user
|
||||
*/
|
||||
public Optional<UUID> tryGetAuthenticatedUserUuid() {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
|
||||
if (authentication == null || !(authentication.getPrincipal() instanceof Jwt jwt)) {
|
||||
if (unsecured) {
|
||||
return Optional.of(UUID.fromString("55e46bc3-2a29-4c53-850f-dbd944dc5c5f"));
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
String stringUuid = jwt.getSubject();
|
||||
|
||||
try {
|
||||
return Optional.of(UUID.fromString(stringUuid));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-3
@@ -242,6 +242,8 @@ public class HistoryApiController implements HistoryApi {
|
||||
HistoryType historyTypeEnum = HistoryType.getHistoryType(type);
|
||||
CompletableFuture<PunishmentHistoryDto> result = new CompletableFuture<>();
|
||||
|
||||
final UUID actor = authenticatedUuid.getAuthenticatedUserUuid();
|
||||
|
||||
Connection.getConnection(Databases.LITE_BANS).runQuery(sqlSession -> {
|
||||
try {
|
||||
IdHistoryMapper idMapper = sqlSession.getMapper(IdHistoryMapper.class);
|
||||
@@ -253,7 +255,6 @@ public class HistoryApiController implements HistoryApi {
|
||||
}
|
||||
int changed = editMapper.setReason(historyTypeEnum, id, reason);
|
||||
HistoryRecord after = idMapper.getRecentHistory(historyTypeEnum, id);
|
||||
UUID actor = authenticatedUuid.getAuthenticatedUserUuid();
|
||||
log.info("[Punishment Edit] Actor={} Type={} Id={} Reason: '{}' -> '{}' (rows={})",
|
||||
actor, historyTypeEnum, id, before.getReason(), after != null ? after.getReason() : null, changed);
|
||||
result.complete(after != null ? mapPunishmentHistory(after) : null);
|
||||
@@ -275,6 +276,8 @@ public class HistoryApiController implements HistoryApi {
|
||||
HistoryType historyTypeEnum = HistoryType.getHistoryType(type);
|
||||
CompletableFuture<PunishmentHistoryDto> result = new CompletableFuture<>();
|
||||
|
||||
final UUID actor = authenticatedUuid.getAuthenticatedUserUuid();
|
||||
|
||||
Connection.getConnection(Databases.LITE_BANS).runQuery(sqlSession -> {
|
||||
try {
|
||||
IdHistoryMapper idMapper = sqlSession.getMapper(IdHistoryMapper.class);
|
||||
@@ -286,7 +289,6 @@ public class HistoryApiController implements HistoryApi {
|
||||
}
|
||||
int changed = editMapper.setUntil(historyTypeEnum, id, until);
|
||||
HistoryRecord after = idMapper.getRecentHistory(historyTypeEnum, id);
|
||||
UUID actor = authenticatedUuid.getAuthenticatedUserUuid();
|
||||
log.info("[Punishment Edit] Actor={} Type={} Id={} Until: '{}' -> '{}' (rows={})",
|
||||
actor, historyTypeEnum, id, before.getUntil(), after != null ? after.getUntil() : null, changed);
|
||||
result.complete(after != null ? mapPunishmentHistory(after) : null);
|
||||
@@ -311,6 +313,8 @@ public class HistoryApiController implements HistoryApi {
|
||||
HistoryType historyTypeEnum = HistoryType.getHistoryType(type);
|
||||
CompletableFuture<Boolean> result = new CompletableFuture<>();
|
||||
|
||||
final UUID actorUuid = authenticatedUuid.getAuthenticatedUserUuid();
|
||||
|
||||
Connection.getConnection(Databases.LITE_BANS).runQuery(sqlSession -> {
|
||||
try {
|
||||
IdHistoryMapper idMapper = sqlSession.getMapper(IdHistoryMapper.class);
|
||||
@@ -320,7 +324,6 @@ public class HistoryApiController implements HistoryApi {
|
||||
result.complete(false);
|
||||
return;
|
||||
}
|
||||
UUID actorUuid = authenticatedUuid.getAuthenticatedUserUuid();
|
||||
String actorName = sqlSession.getMapper(RecentNamesMapper.class).getUsername(actorUuid.toString());
|
||||
int changed = editMapper.remove(historyTypeEnum, id);
|
||||
log.info("[Punishment Remove] Actor={} ({}) Type={} Id={} Before(active={} removedBy={} reason='{}') (rows={})",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.alttd.altitudeweb.services.limits;
|
||||
|
||||
import com.alttd.altitudeweb.controllers.data_from_auth.AuthenticatedUuid;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -16,6 +17,8 @@ import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.time.Duration;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Aspect
|
||||
@Component
|
||||
@@ -24,6 +27,7 @@ import java.time.Duration;
|
||||
public class RateLimitAspect {
|
||||
|
||||
private final InMemoryRateLimiterService rateLimiterService;
|
||||
private final AuthenticatedUuid authenticatedUuid;
|
||||
|
||||
@Around("""
|
||||
@annotation(com.alttd.altitudeweb.services.limits.RateLimit)
|
||||
@@ -37,7 +41,6 @@ public class RateLimitAspect {
|
||||
HttpServletRequest request = requestAttributes.getRequest();
|
||||
HttpServletResponse response = requestAttributes.getResponse();
|
||||
|
||||
String clientIp = request.getRemoteAddr();
|
||||
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
||||
Method method = signature.getMethod();
|
||||
|
||||
@@ -54,7 +57,12 @@ public class RateLimitAspect {
|
||||
Duration duration = Duration.ofSeconds(rateLimit.timeUnit().toSeconds(rateLimit.timeValue()));
|
||||
String customKey = rateLimit.key();
|
||||
|
||||
String key = clientIp + "-" + (customKey.isEmpty() ? method.getName() : customKey);
|
||||
Optional<UUID> optionalUUID = authenticatedUuid.tryGetAuthenticatedUserUuid();
|
||||
if (optionalUUID.isEmpty()) {
|
||||
return joinPoint.proceed();
|
||||
}
|
||||
UUID uuid = optionalUUID.get();
|
||||
String key = uuid + "-" + (customKey.isEmpty() ? method.getName() : customKey);
|
||||
|
||||
boolean allowed = rateLimiterService.tryAcquire(key, limit, duration);
|
||||
|
||||
@@ -67,7 +75,7 @@ public class RateLimitAspect {
|
||||
|
||||
return joinPoint.proceed();
|
||||
} else {
|
||||
log.warn("Rate limit exceeded for IP: {}, endpoint: {}", clientIp, request.getRequestURI());
|
||||
log.warn("Rate limit exceeded for uuid: {}, endpoint: {}", uuid, request.getRequestURI());
|
||||
|
||||
Duration nextResetTime = rateLimiterService.getNextResetTime(key, duration);
|
||||
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ public interface TeamMemberMapper {
|
||||
SELECT players.username, players.uuid
|
||||
FROM luckperms_user_permissions AS permissions
|
||||
INNER JOIN luckperms_players AS players ON players.uuid = permissions.uuid
|
||||
WHERE permission = #{groupPermission}
|
||||
WHERE permission = #{groupPermission} AND server = 'global'
|
||||
AND world = 'global'
|
||||
""")
|
||||
List<Player> getTeamMembers(@Param("groupPermission") String groupPermission);
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ public interface VotingPluginUsersMapper {
|
||||
WeeklyTotal as totalVotesThisWeek,
|
||||
MonthTotal as totalVotesThisMonth,
|
||||
AllTimeTotal as totalVotesAllTime
|
||||
FROM votingplugin.votingplugin_users
|
||||
FROM votingplugin.VotingPlugin_Users
|
||||
WHERE uuid = #{uuid}
|
||||
""")
|
||||
Optional<VotingStatsRow> getStatsByUuid(@Param("uuid") UUID uuid);
|
||||
|
||||
@@ -154,6 +154,6 @@ export const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'nickgenerator',
|
||||
loadComponent: () => import('./pages/reference/nickgenerator/nickgenerator.component').then(m => m.NickgeneratorComponent)
|
||||
loadComponent: () => import('@pages/reference/nickgenerator/nick-generator.component').then(m => m.NickGeneratorComponent)
|
||||
},
|
||||
];
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
<ng-container>
|
||||
<app-header [current_page]="'nickgenerator'" height="460px" background_image="/public/img/backgrounds/trees.jpg"
|
||||
[overlay_gradient]="0.5">
|
||||
<div class="title" header-content>
|
||||
<h1>Nickname Generator</h1>
|
||||
<h2>Customize your in-game nickname</h2>
|
||||
<h3 style="font-family: 'minecraft-text', sans-serif; font-size: 0.8rem; margin-top: 10px;">Made by TheParm</h3>
|
||||
<!--TODO remove this message when everything works-->
|
||||
<p style="font-weight: bolder; color: red">NOTICE: This page is in the process of being updated to work on the new
|
||||
site.<br> This version is functional, but only barely. Expect updates in the coming days</p>
|
||||
</div>
|
||||
</app-header>
|
||||
|
||||
<main>
|
||||
<section class="containerNick">
|
||||
<div class="controls">
|
||||
|
||||
@for (part of parts; track $index; let i = $index) {
|
||||
<div class="part">
|
||||
<div class="row">
|
||||
<mat-form-field class="textField" appearance="outline">
|
||||
<mat-label>Text</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[value]="part.text"
|
||||
(input)="part.text = ($any($event.target).value || ''); onInputChanged()"
|
||||
maxlength="16"
|
||||
/>
|
||||
<mat-hint align="end">{{ part.text.length }} / 16</mat-hint>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-checkbox
|
||||
class="checkbox"
|
||||
[(ngModel)]="part.gradient"
|
||||
(change)="onGradientToggle(i)"
|
||||
>Gradient
|
||||
</mat-checkbox
|
||||
>
|
||||
|
||||
<mat-form-field
|
||||
class="colorField"
|
||||
appearance="outline"
|
||||
[style.visibility]="(part.continuation && i>0 && parts[i-1]?.gradient && part.gradient) ? 'hidden' : 'visible'">
|
||||
<mat-label>Color A</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="color"
|
||||
[value]="part.colorA"
|
||||
(input)="part.colorA = $any($event.target).value; onInputChanged()"
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field
|
||||
class="colorField"
|
||||
appearance="outline"
|
||||
[style.visibility]="part.gradient ? 'visible' : 'hidden'">
|
||||
<mat-label>Color B</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="color"
|
||||
[value]="part.colorB"
|
||||
(input)="part.colorB = $any($event.target).value; onInputChanged()"
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-checkbox
|
||||
class="checkbox"
|
||||
[(ngModel)]="part.continuation"
|
||||
(change)="onContinuationToggle(i)"
|
||||
[disabled]="i===0 || !part.gradient || !parts[i-1]?.gradient"
|
||||
>Continuation
|
||||
</mat-checkbox
|
||||
>
|
||||
</div>
|
||||
|
||||
@if (part.invalid) {
|
||||
<div class="invalid">(min 1 – max 16 chars{{ part.gradient ? '' : ' for non-empty text' }})</div>
|
||||
}
|
||||
<mat-divider></mat-divider>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="buttons">
|
||||
<button mat-raised-button (click)="addPart()">Add Part</button>
|
||||
<button mat-raised-button (click)="deletePart()">Remove Part</button>
|
||||
</div>
|
||||
|
||||
@if (showCommands) {
|
||||
<div class="commands">
|
||||
<div class="commandRow">
|
||||
<div class="command">{{ tryCmd }}</div>
|
||||
<button mat-stroked-button (click)="copy(tryCmd, 'try')">{{ tryCommandButtonContent }}</button>
|
||||
</div>
|
||||
<div class="commandRow">
|
||||
<div class="command">{{ requestCmd }}</div>
|
||||
<button mat-stroked-button (click)="copy(requestCmd, 'request')">{{ requestCommandButtonContent }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (showPreview) {
|
||||
<div class="preview" [innerHTML]="previewHtml"></div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,77 @@
|
||||
/* nick-generator.component.css */
|
||||
.containerNick {
|
||||
background-color: #292828;
|
||||
padding: 40px 5%;
|
||||
max-width: 1220px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.controls {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.part {
|
||||
padding: 8px 0 16px 0;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.textField {
|
||||
flex: 1 1 260px;
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.colorField {
|
||||
width: 110px;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
.invalid {
|
||||
color: #dd0000;
|
||||
font-size: 12px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin: 20px 0 32px 0;
|
||||
}
|
||||
|
||||
.commands {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.commandRow {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.command {
|
||||
background: #1e1e1e;
|
||||
color: #ffffff;
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
font-family: monospace;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.preview {
|
||||
background: #1e1e1e;
|
||||
padding: 14px 12px;
|
||||
border-radius: 6px;
|
||||
color: #ffffff;
|
||||
font-family: 'minecraft-text', monospace;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {DomSanitizer, SafeHtml} from '@angular/platform-browser';
|
||||
import {MatFormFieldModule} from '@angular/material/form-field';
|
||||
import {MatInputModule} from '@angular/material/input';
|
||||
import {HeaderComponent} from '@header/header.component';
|
||||
import {MatCheckboxModule} from '@angular/material/checkbox';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {MatDividerModule} from '@angular/material/divider';
|
||||
import {MatButtonModule} from '@angular/material/button';
|
||||
|
||||
interface Part {
|
||||
text: string;
|
||||
gradient: boolean;
|
||||
colorA: string;
|
||||
colorB: string;
|
||||
continuation: boolean;
|
||||
invalid?: boolean;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-nick-generator',
|
||||
templateUrl: './nick-generator.component.html',
|
||||
styleUrls: ['./nick-generator.component.scss'],
|
||||
imports: [
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
HeaderComponent,
|
||||
MatCheckboxModule,
|
||||
FormsModule,
|
||||
MatDividerModule,
|
||||
MatButtonModule,
|
||||
]
|
||||
})
|
||||
export class NickGeneratorComponent {
|
||||
parts: Part[] = [
|
||||
{text: '', gradient: false, colorA: '#ffffff', colorB: '#ffffff', continuation: false}
|
||||
];
|
||||
|
||||
tryCmd = '';
|
||||
requestCmd = '';
|
||||
previewHtml: SafeHtml = '';
|
||||
showPreview = false;
|
||||
showCommands = false;
|
||||
|
||||
constructor(private sanitizer: DomSanitizer) {
|
||||
}
|
||||
|
||||
addPart(): void {
|
||||
this.parts.push({text: '', gradient: false, colorA: '#ffffff', colorB: '#ffffff', continuation: false});
|
||||
this.onInputChanged();
|
||||
}
|
||||
|
||||
deletePart(): void {
|
||||
if (this.parts.length > 1) {
|
||||
this.parts.pop();
|
||||
// If last part was a gradient, unset continuation on new last part
|
||||
if (this.parts.length > 0) this.parts[this.parts.length - 1].continuation = false;
|
||||
this.onInputChanged();
|
||||
}
|
||||
}
|
||||
|
||||
onGradientToggle(i: number): void {
|
||||
// Toggling gradient affects availability of continuation for this & next part
|
||||
if (!this.parts[i].gradient) {
|
||||
// If gradient turned off, force continuation off for this index (not visible anymore)
|
||||
this.parts[i].continuation = false;
|
||||
}
|
||||
if (i + 1 < this.parts.length && !this.parts[i + 1].gradient) {
|
||||
this.parts[i + 1].continuation = false;
|
||||
}
|
||||
this.onInputChanged();
|
||||
}
|
||||
|
||||
onContinuationToggle(_: number): void {
|
||||
this.onInputChanged();
|
||||
}
|
||||
|
||||
onInputChanged(): void {
|
||||
let result = '';
|
||||
let preview = '';
|
||||
let valid = true;
|
||||
let nickLen = 0;
|
||||
let prevColorB = '#ffffff';
|
||||
|
||||
for (let i = 0; i < this.parts.length; i++) {
|
||||
const p = this.parts[i];
|
||||
const len = p.text.length;
|
||||
nickLen += len;
|
||||
|
||||
const partValid =
|
||||
(p.gradient && len >= 1 && len <= 16) ||
|
||||
(!p.gradient && len > 0);
|
||||
|
||||
p.invalid = !partValid;
|
||||
if (!partValid) {
|
||||
valid = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (p.gradient) {
|
||||
// Continuation allowed only if previous & current are gradient
|
||||
const contAllowed = i > 0 && this.parts[i - 1].gradient;
|
||||
const cont = p.continuation && contAllowed;
|
||||
|
||||
if (cont) {
|
||||
result += p.text;
|
||||
preview += this.generateGradient(p.text, prevColorB, p.colorB);
|
||||
} else {
|
||||
result += `{${p.colorA}>}${p.text}`;
|
||||
preview += this.generateGradient(p.text, p.colorA, p.colorB);
|
||||
}
|
||||
|
||||
// Add closing/continuation marker
|
||||
const nextContinuation = (i + 1 < this.parts.length) && this.parts[i + 1].continuation;
|
||||
if (i < this.parts.length - 1) {
|
||||
result += `{${p.colorB}<>}`;
|
||||
} else {
|
||||
result += `{${p.colorB}<}`;
|
||||
}
|
||||
prevColorB = p.colorB;
|
||||
} else {
|
||||
// Solid
|
||||
result += `{${p.colorA}}${p.text}`;
|
||||
preview += this.generateSolidColor(p.text, p.colorA);
|
||||
}
|
||||
}
|
||||
|
||||
this.tryCmd = '';
|
||||
this.requestCmd = '';
|
||||
this.showPreview = false;
|
||||
this.showCommands = false;
|
||||
|
||||
if (valid && result.length > 0 && nickLen >= 3 && nickLen <= 16) {
|
||||
this.tryCmd = `/nick try ${result}`;
|
||||
this.requestCmd = `/nick request ${result}`;
|
||||
this.previewHtml = this.sanitizer.bypassSecurityTrustHtml(
|
||||
this.generateSolidColor('Nickname preview: ', '#ffffff') + preview
|
||||
);
|
||||
this.showPreview = true;
|
||||
this.showCommands = true;
|
||||
} else {
|
||||
if (!valid && (this.parts.length > 1 || nickLen > 0)) {
|
||||
this.previewHtml = this.sanitizer.bypassSecurityTrustHtml(
|
||||
this.generateSolidColor('Invalid part(s) length', '#dd0000')
|
||||
);
|
||||
} else if (valid && (nickLen < 3 || nickLen > 16)) {
|
||||
this.previewHtml = this.sanitizer.bypassSecurityTrustHtml(
|
||||
this.generateSolidColor('Nickname needs to be 3–16 chars', '#dd0000')
|
||||
);
|
||||
} else {
|
||||
this.previewHtml = this.sanitizer.bypassSecurityTrustHtml('');
|
||||
}
|
||||
this.showPreview = nickLen > 0;
|
||||
}
|
||||
}
|
||||
|
||||
tryCommandButtonContent = 'Copy';
|
||||
requestCommandButtonContent = 'Copy';
|
||||
|
||||
copy(text: string, button: 'try' | 'request'): void {
|
||||
navigator.clipboard.writeText(text);
|
||||
if (button === 'try') {
|
||||
this.tryCommandButtonContent = 'Copied!';
|
||||
} else if (button === 'request') {
|
||||
this.requestCommandButtonContent = 'Copied!';
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (button === 'try') {
|
||||
this.tryCommandButtonContent = 'Copy';
|
||||
} else if (button === 'request') {
|
||||
this.requestCommandButtonContent = 'Copy';
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
generateSolidColor(text: string, color: string): string {
|
||||
return `<span style="color:${color}">${this.escape(text)}</span>`;
|
||||
}
|
||||
|
||||
generateGradient(text: string, colorA: string, colorB: string): string {
|
||||
const len = text.length;
|
||||
if (len === 0) return '';
|
||||
const a = this.hexToRgb(colorA);
|
||||
const b = this.hexToRgb(colorB);
|
||||
if (!a || !b) return this.generateSolidColor(text, colorA);
|
||||
|
||||
const stepR = len > 1 ? (b.r - a.r) / (len - 1) : 0;
|
||||
const stepG = len > 1 ? (b.g - a.g) / (len - 1) : 0;
|
||||
const stepB = len > 1 ? (b.b - a.b) / (len - 1) : 0;
|
||||
|
||||
let res = '';
|
||||
for (let i = 0; i < len; i++) {
|
||||
const r = a.r + stepR * i;
|
||||
const g = a.g + stepG * i;
|
||||
const bl = a.b + stepB * i;
|
||||
res += this.generateSolidColor(text[i], this.rgbToHex(r, g, bl));
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
hexToRgb(hex: string): { r: number; g: number; b: number } | null {
|
||||
const m = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
return m
|
||||
? {r: parseInt(m[1], 16), g: parseInt(m[2], 16), b: parseInt(m[3], 16)}
|
||||
: null;
|
||||
}
|
||||
|
||||
componentToHex(c: number): string {
|
||||
const x = Math.round(c);
|
||||
const h = x.toString(16);
|
||||
return h.length === 1 ? '0' + h : h;
|
||||
}
|
||||
|
||||
rgbToHex(r: number, g: number, b: number): string {
|
||||
return (
|
||||
'#' + this.componentToHex(r) + this.componentToHex(g) + this.componentToHex(b)
|
||||
);
|
||||
}
|
||||
|
||||
escape(s: string): string {
|
||||
return s
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
<ng-container>
|
||||
<app-header [current_page]="'nickgenerator'" height="460px" background_image="/public/img/backgrounds/trees.jpg"
|
||||
[overlay_gradient]="0.5">
|
||||
<div class="title" header-content>
|
||||
<h1>Nickname Generator</h1>
|
||||
<h2>Customize your in-game nickname</h2>
|
||||
<h3 style="font-family: 'minecraft-text', sans-serif; font-size: 0.8rem; margin-top: 10px;">Made by TheParm</h3>
|
||||
</div>
|
||||
</app-header>
|
||||
|
||||
<main>
|
||||
<!-- <section class="darkmodeSection">
|
||||
<div class="container containerNick">
|
||||
<div style="padding: 0 5% 0 5%;">
|
||||
<div id="parts" class="previewNickDiv">
|
||||
</div>
|
||||
<div class="previewNickDiv">
|
||||
<input type="button" class="button" value="Add Part" onclick="addPart()"/>
|
||||
<input type="button" class="button" value="Remove Part" onclick="deletePart()"/>
|
||||
</div>
|
||||
<br><br><br><br>
|
||||
<div id="commandTry" class="previewNickDiv">
|
||||
<div id="try" class="command darkBg"></div>
|
||||
<input type="button" class="button copy" value="Copy" onclick="copy(this)"/>
|
||||
</div>
|
||||
<div id="commandRequest" class="previewNickDiv">
|
||||
<div id="request" class="command darkBg"></div>
|
||||
<input type="button" class="button copy" value="Copy" onclick="copy(this)"/>
|
||||
</div>
|
||||
<div id="preview" class="preview darkBg previewNickDiv">
|
||||
</div>
|
||||
<div id="template" class='part' style="display: none">
|
||||
<p style="font-family: 'minecraft-text', sans-serif">
|
||||
Text: <input type="text" id="text" class="textPart" size=18 oninput="inputChanged()"/>
|
||||
Gradient: <input type="checkbox" id="grad" class="gradPart" oninput="onGradient(this)"/>
|
||||
<input id="colorA" type="text" class="coloris colorAPart color" value="#ffffff" oninput="inputChanged()"/>
|
||||
<input id="colorB" type="text" class="coloris colorBPart color" value="#ffffff" oninput="inputChanged()"/>
|
||||
Continuation: <input type="checkbox" id="cont" class="contPart" disabled oninput="onContinuation(this)"/>
|
||||
<span id="invalid" class="invalidPart" style="display: none">(min 1 - max 16 chars)</span>
|
||||
</p>
|
||||
</div>
|
||||
<div style="margin-top: 20px; text-align: center;">
|
||||
<p style="font-family: 'minecraft-text', sans-serif">
|
||||
Usage: Add as many parts as you wish, then apply the color and/or gradient, and copy/paste the command
|
||||
into the minecraft chat. The total length of the nickname should be between 3 and 16 characters. Use the
|
||||
continuation checkbox to continue the gradient from the last gradient color.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section> -->
|
||||
</main>
|
||||
</ng-container>
|
||||
@@ -1,23 +0,0 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { NickgeneratorComponent } from './nickgenerator.component';
|
||||
|
||||
describe('NickgeneratorComponent', () => {
|
||||
let component: NickgeneratorComponent;
|
||||
let fixture: ComponentFixture<NickgeneratorComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [NickgeneratorComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(NickgeneratorComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,14 +0,0 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {HeaderComponent} from "@header/header.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-nickgenerator',
|
||||
imports: [
|
||||
HeaderComponent
|
||||
],
|
||||
templateUrl: './nickgenerator.component.html',
|
||||
styleUrl: './nickgenerator.component.scss'
|
||||
})
|
||||
export class NickgeneratorComponent {
|
||||
|
||||
}
|
||||
@@ -32,7 +32,7 @@
|
||||
</div>
|
||||
<div class="voteDisclaimer">
|
||||
<h3 style="text-align: center;">Disclaimers & Info</h3>
|
||||
<p style="text-align: center;">You can only store 7 daily crate keys, 1 weekly crate key and 2 quest crate
|
||||
<p style="text-align: center;">You can only store 7 daily crate keys, 2 weekly crate key and 2 quest crate
|
||||
keys.</p><br>
|
||||
<p style="text-align: center;">Voting within 30 minutes of midnight UTC can cause your votes to glitch. Keys
|
||||
lost due to voting too close to midnight UTC will not be reimbursed.
|
||||
@@ -41,6 +41,12 @@
|
||||
</section>
|
||||
</section>
|
||||
<section class="voteSection">
|
||||
@if (voteStats) {
|
||||
<p style="text-align: center">You have voted {{ voteStats.voteStats.total }} times
|
||||
and {{ voteStats.voteStats.weekly }} this week. You
|
||||
are
|
||||
on a {{ voteStats.voteStreak.dailyStreak }} day vote streak!</p>
|
||||
}
|
||||
<div class="container voteContainer">
|
||||
@for (voteSite of Object.keys(voteSites); track voteSite) {
|
||||
<div class="vote">
|
||||
@@ -51,7 +57,9 @@
|
||||
[href]="voteSites[voteSite]">
|
||||
<div class=button-outer [class.not-available-button-outer]="!canVote(voteSite)"
|
||||
[class.available-button-outer]="canVote(voteSite)">
|
||||
<span class="button-inner">{{ getVoteText(voteSite) }}</span>
|
||||
<span class="button-inner">
|
||||
{{ getVoteText(voteSite) }}
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -43,9 +43,9 @@
|
||||
}
|
||||
|
||||
.available-button-outer {
|
||||
background-color: #4caf50 !important;
|
||||
border: 2px solid #4caf50;
|
||||
}
|
||||
|
||||
.not-available-button-outer {
|
||||
background-color: var(--white) !important;
|
||||
border: 2px solid #ffa433;
|
||||
}
|
||||
|
||||
@@ -6,13 +6,15 @@ import {SiteService, VoteData} from '@api';
|
||||
import {AuthService} from '@services/auth.service';
|
||||
import {interval, Subscription} from 'rxjs';
|
||||
import {TimeAgoPipe} from '@pipes/TimeAgoPipe';
|
||||
import {MatIconModule} from '@angular/material/icon';
|
||||
|
||||
@Component({
|
||||
selector: 'app-vote',
|
||||
standalone: true,
|
||||
imports: [
|
||||
HeaderComponent,
|
||||
TimeAgoPipe
|
||||
TimeAgoPipe,
|
||||
MatIconModule,
|
||||
],
|
||||
templateUrl: './vote.component.html',
|
||||
styleUrl: './vote.component.scss'
|
||||
@@ -48,7 +50,7 @@ export class VoteComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.refreshSubscription = interval(300000).subscribe(() => {
|
||||
this.refreshSubscription = interval(60000).subscribe(() => {
|
||||
this.loadVoteStats();
|
||||
});
|
||||
}
|
||||
@@ -94,9 +96,10 @@ export class VoteComponent implements OnInit, OnDestroy {
|
||||
return false;
|
||||
}
|
||||
const now: Date = new Date();
|
||||
return (
|
||||
this.voteStats.allVoteInfo.some(voteInfo => voteInfo.siteName === voteSite
|
||||
&& voteInfo.lastVoteTimestamp - now.getTime() < 86400000)
|
||||
)
|
||||
const voteInfo = this.voteStats.allVoteInfo.find(voteInfo => voteInfo.siteName === voteSite);
|
||||
if (!voteInfo) {
|
||||
return true;
|
||||
}
|
||||
return (now.getTime() - voteInfo.lastVoteTimestamp < 86400000)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,6 +155,15 @@ time, mark, audio, video {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.centered {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
/* flex end */
|
||||
|
||||
/* main css */
|
||||
|
||||
Reference in New Issue
Block a user