Author SHA1 Message Date
stijn 2be79c180a Refactor Nickname Generator component with Angular Material, update logic for fields and commands, and improve styling. 2025-10-29 21:39:39 +01:00
stijn 423d5e4a4c Fix getTeamMembers query to scope permissions to the global server 2025-10-26 01:45:19 +02:00
stijn a0db55dede Fix getTeamMembers query to scope permissions to the global server 2025-10-26 01:43:00 +02:00
stijn e0a09d303c Update vote eligibility logic, add MatIconModule, and refine button styles for improved clarity. 2025-10-24 22:43:38 +02:00
stijn 29967d65b8 Improve vote eligibility check by adding find to handle cases where voteSite is not found. 2025-10-24 22:28:36 +02:00
stijn 8b265514a6 Refactor RateLimitAspect to use authenticated UUID instead of client IP for rate limiting. Enhance AuthenticatedUuid with optional UUID retrieval method. 2025-10-24 22:27:04 +02:00
stijn e766fd1125 Fix vote eligibility check logic by correcting timestamp comparison direction. 2025-10-24 22:22:32 +02:00
stijn 86a85049b3 center p 2025-10-24 22:20:50 +02:00
stijn cf73303218 center div 2025-10-24 22:18:19 +02:00
stijn d075464ded center div 2025-10-24 22:16:58 +02:00
stijn 7be3b6f9d3 Wrap vote statistics message in a centered <div> for improved alignment and readability. 2025-10-24 22:15:30 +02:00
stijn 24e28015d3 Adjust vote refresh interval to 1 minute and update vote disclaimer text. Add vote statistics display. 2025-10-24 22:13:44 +02:00
stijn 754479eb98 Refactor: move actor and actorUuid UUID retrieval to method to thread with auth 2025-10-24 22:01:12 +02:00
stijn 5974ec1dba Revert "Refactor getAuthenticatedUserUuid - extract getAuthentication method for improved null handling and clarity"
This reverts commit 4b466f314e.
2025-10-24 21:59:57 +02:00
stijn c5ed657d3e Revert "Enhance AuthenticatedUuid to improve UUID extraction by adding support for decoding tokens from the Authorization header. Add logging, refactor for better null handling, and introduce @RequiredArgsConstructor."
This reverts commit 02adbb2522.
2025-10-24 21:59:57 +02:00
stijn 02adbb2522 Enhance AuthenticatedUuid to improve UUID extraction by adding support for decoding tokens from the Authorization header. Add logging, refactor for better null handling, and introduce @RequiredArgsConstructor. 2025-10-24 21:58:07 +02:00
stijn 4b466f314e Refactor getAuthenticatedUserUuid - extract getAuthentication method for improved null handling and clarity 2025-10-24 21:46:07 +02:00
stijn 6531526278 Add logging for invalid authentication principal in AuthenticatedUuid and annotate with @Slf4j 2025-10-24 21:43:50 +02:00
stijn bc0739f707 Fix table name casing in VotingPluginUsersMapper query 2025-10-24 21:34:54 +02:00
17 changed files with 492 additions and 109 deletions
@@ -1,5 +1,7 @@
package com.alttd.altitudeweb.controllers.data_from_auth; 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.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication; 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.stereotype.Service;
import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ResponseStatusException;
import java.util.Optional;
import java.util.UUID; import java.util.UUID;
@Slf4j
@Service @Service
public class AuthenticatedUuid { public class AuthenticatedUuid {
@Value("${UNSECURED:#{false}}") @Value("${UNSECURED:#{false}}")
@@ -25,6 +29,9 @@ public class AuthenticatedUuid {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !(authentication.getPrincipal() instanceof Jwt jwt)) { 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) { if (unsecured) {
return UUID.fromString("55e46bc3-2a29-4c53-850f-dbd944dc5c5f"); return UUID.fromString("55e46bc3-2a29-4c53-850f-dbd944dc5c5f");
} }
@@ -39,4 +46,28 @@ public class AuthenticatedUuid {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid UUID format"); 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();
}
}
} }
@@ -242,6 +242,8 @@ public class HistoryApiController implements HistoryApi {
HistoryType historyTypeEnum = HistoryType.getHistoryType(type); HistoryType historyTypeEnum = HistoryType.getHistoryType(type);
CompletableFuture<PunishmentHistoryDto> result = new CompletableFuture<>(); CompletableFuture<PunishmentHistoryDto> result = new CompletableFuture<>();
final UUID actor = authenticatedUuid.getAuthenticatedUserUuid();
Connection.getConnection(Databases.LITE_BANS).runQuery(sqlSession -> { Connection.getConnection(Databases.LITE_BANS).runQuery(sqlSession -> {
try { try {
IdHistoryMapper idMapper = sqlSession.getMapper(IdHistoryMapper.class); IdHistoryMapper idMapper = sqlSession.getMapper(IdHistoryMapper.class);
@@ -253,7 +255,6 @@ public class HistoryApiController implements HistoryApi {
} }
int changed = editMapper.setReason(historyTypeEnum, id, reason); int changed = editMapper.setReason(historyTypeEnum, id, reason);
HistoryRecord after = idMapper.getRecentHistory(historyTypeEnum, id); HistoryRecord after = idMapper.getRecentHistory(historyTypeEnum, id);
UUID actor = authenticatedUuid.getAuthenticatedUserUuid();
log.info("[Punishment Edit] Actor={} Type={} Id={} Reason: '{}' -> '{}' (rows={})", log.info("[Punishment Edit] Actor={} Type={} Id={} Reason: '{}' -> '{}' (rows={})",
actor, historyTypeEnum, id, before.getReason(), after != null ? after.getReason() : null, changed); actor, historyTypeEnum, id, before.getReason(), after != null ? after.getReason() : null, changed);
result.complete(after != null ? mapPunishmentHistory(after) : null); result.complete(after != null ? mapPunishmentHistory(after) : null);
@@ -275,6 +276,8 @@ public class HistoryApiController implements HistoryApi {
HistoryType historyTypeEnum = HistoryType.getHistoryType(type); HistoryType historyTypeEnum = HistoryType.getHistoryType(type);
CompletableFuture<PunishmentHistoryDto> result = new CompletableFuture<>(); CompletableFuture<PunishmentHistoryDto> result = new CompletableFuture<>();
final UUID actor = authenticatedUuid.getAuthenticatedUserUuid();
Connection.getConnection(Databases.LITE_BANS).runQuery(sqlSession -> { Connection.getConnection(Databases.LITE_BANS).runQuery(sqlSession -> {
try { try {
IdHistoryMapper idMapper = sqlSession.getMapper(IdHistoryMapper.class); IdHistoryMapper idMapper = sqlSession.getMapper(IdHistoryMapper.class);
@@ -286,7 +289,6 @@ public class HistoryApiController implements HistoryApi {
} }
int changed = editMapper.setUntil(historyTypeEnum, id, until); int changed = editMapper.setUntil(historyTypeEnum, id, until);
HistoryRecord after = idMapper.getRecentHistory(historyTypeEnum, id); HistoryRecord after = idMapper.getRecentHistory(historyTypeEnum, id);
UUID actor = authenticatedUuid.getAuthenticatedUserUuid();
log.info("[Punishment Edit] Actor={} Type={} Id={} Until: '{}' -> '{}' (rows={})", log.info("[Punishment Edit] Actor={} Type={} Id={} Until: '{}' -> '{}' (rows={})",
actor, historyTypeEnum, id, before.getUntil(), after != null ? after.getUntil() : null, changed); actor, historyTypeEnum, id, before.getUntil(), after != null ? after.getUntil() : null, changed);
result.complete(after != null ? mapPunishmentHistory(after) : null); result.complete(after != null ? mapPunishmentHistory(after) : null);
@@ -311,6 +313,8 @@ public class HistoryApiController implements HistoryApi {
HistoryType historyTypeEnum = HistoryType.getHistoryType(type); HistoryType historyTypeEnum = HistoryType.getHistoryType(type);
CompletableFuture<Boolean> result = new CompletableFuture<>(); CompletableFuture<Boolean> result = new CompletableFuture<>();
final UUID actorUuid = authenticatedUuid.getAuthenticatedUserUuid();
Connection.getConnection(Databases.LITE_BANS).runQuery(sqlSession -> { Connection.getConnection(Databases.LITE_BANS).runQuery(sqlSession -> {
try { try {
IdHistoryMapper idMapper = sqlSession.getMapper(IdHistoryMapper.class); IdHistoryMapper idMapper = sqlSession.getMapper(IdHistoryMapper.class);
@@ -320,7 +324,6 @@ public class HistoryApiController implements HistoryApi {
result.complete(false); result.complete(false);
return; return;
} }
UUID actorUuid = authenticatedUuid.getAuthenticatedUserUuid();
String actorName = sqlSession.getMapper(RecentNamesMapper.class).getUsername(actorUuid.toString()); String actorName = sqlSession.getMapper(RecentNamesMapper.class).getUsername(actorUuid.toString());
int changed = editMapper.remove(historyTypeEnum, id); int changed = editMapper.remove(historyTypeEnum, id);
log.info("[Punishment Remove] Actor={} ({}) Type={} Id={} Before(active={} removedBy={} reason='{}') (rows={})", log.info("[Punishment Remove] Actor={} ({}) Type={} Id={} Before(active={} removedBy={} reason='{}') (rows={})",
@@ -1,5 +1,6 @@
package com.alttd.altitudeweb.services.limits; package com.alttd.altitudeweb.services.limits;
import com.alttd.altitudeweb.controllers.data_from_auth.AuthenticatedUuid;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@@ -16,6 +17,8 @@ import org.springframework.web.context.request.ServletRequestAttributes;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.time.Duration; import java.time.Duration;
import java.util.Optional;
import java.util.UUID;
@Aspect @Aspect
@Component @Component
@@ -24,6 +27,7 @@ import java.time.Duration;
public class RateLimitAspect { public class RateLimitAspect {
private final InMemoryRateLimiterService rateLimiterService; private final InMemoryRateLimiterService rateLimiterService;
private final AuthenticatedUuid authenticatedUuid;
@Around(""" @Around("""
@annotation(com.alttd.altitudeweb.services.limits.RateLimit) @annotation(com.alttd.altitudeweb.services.limits.RateLimit)
@@ -37,7 +41,6 @@ public class RateLimitAspect {
HttpServletRequest request = requestAttributes.getRequest(); HttpServletRequest request = requestAttributes.getRequest();
HttpServletResponse response = requestAttributes.getResponse(); HttpServletResponse response = requestAttributes.getResponse();
String clientIp = request.getRemoteAddr();
MethodSignature signature = (MethodSignature) joinPoint.getSignature(); MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod(); Method method = signature.getMethod();
@@ -54,7 +57,12 @@ public class RateLimitAspect {
Duration duration = Duration.ofSeconds(rateLimit.timeUnit().toSeconds(rateLimit.timeValue())); Duration duration = Duration.ofSeconds(rateLimit.timeUnit().toSeconds(rateLimit.timeValue()));
String customKey = rateLimit.key(); 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); boolean allowed = rateLimiterService.tryAcquire(key, limit, duration);
@@ -67,7 +75,7 @@ public class RateLimitAspect {
return joinPoint.proceed(); return joinPoint.proceed();
} else { } 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); Duration nextResetTime = rateLimiterService.getNextResetTime(key, duration);
@@ -15,7 +15,7 @@ public interface TeamMemberMapper {
SELECT players.username, players.uuid SELECT players.username, players.uuid
FROM luckperms_user_permissions AS permissions FROM luckperms_user_permissions AS permissions
INNER JOIN luckperms_players AS players ON players.uuid = permissions.uuid INNER JOIN luckperms_players AS players ON players.uuid = permissions.uuid
WHERE permission = #{groupPermission} WHERE permission = #{groupPermission} AND server = 'global'
AND world = 'global' AND world = 'global'
""") """)
List<Player> getTeamMembers(@Param("groupPermission") String groupPermission); List<Player> getTeamMembers(@Param("groupPermission") String groupPermission);
@@ -21,7 +21,7 @@ public interface VotingPluginUsersMapper {
WeeklyTotal as totalVotesThisWeek, WeeklyTotal as totalVotesThisWeek,
MonthTotal as totalVotesThisMonth, MonthTotal as totalVotesThisMonth,
AllTimeTotal as totalVotesAllTime AllTimeTotal as totalVotesAllTime
FROM votingplugin.votingplugin_users FROM votingplugin.VotingPlugin_Users
WHERE uuid = #{uuid} WHERE uuid = #{uuid}
""") """)
Optional<VotingStatsRow> getStatsByUuid(@Param("uuid") UUID uuid); Optional<VotingStatsRow> getStatsByUuid(@Param("uuid") UUID uuid);
+1 -1
View File
@@ -154,6 +154,6 @@ export const routes: Routes = [
}, },
{ {
path: 'nickgenerator', 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 316 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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
}
@@ -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>
<div class="voteDisclaimer"> <div class="voteDisclaimer">
<h3 style="text-align: center;">Disclaimers & Info</h3> <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> keys.</p><br>
<p style="text-align: center;">Voting within 30 minutes of midnight UTC can cause your votes to glitch. Keys <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. lost due to voting too close to midnight UTC will not be reimbursed.
@@ -41,6 +41,12 @@
</section> </section>
</section> </section>
<section class="voteSection"> <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"> <div class="container voteContainer">
@for (voteSite of Object.keys(voteSites); track voteSite) { @for (voteSite of Object.keys(voteSites); track voteSite) {
<div class="vote"> <div class="vote">
@@ -51,7 +57,9 @@
[href]="voteSites[voteSite]"> [href]="voteSites[voteSite]">
<div class=button-outer [class.not-available-button-outer]="!canVote(voteSite)" <div class=button-outer [class.not-available-button-outer]="!canVote(voteSite)"
[class.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> </div>
</a> </a>
</div> </div>
@@ -43,9 +43,9 @@
} }
.available-button-outer { .available-button-outer {
background-color: #4caf50 !important; border: 2px solid #4caf50;
} }
.not-available-button-outer { .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 {AuthService} from '@services/auth.service';
import {interval, Subscription} from 'rxjs'; import {interval, Subscription} from 'rxjs';
import {TimeAgoPipe} from '@pipes/TimeAgoPipe'; import {TimeAgoPipe} from '@pipes/TimeAgoPipe';
import {MatIconModule} from '@angular/material/icon';
@Component({ @Component({
selector: 'app-vote', selector: 'app-vote',
standalone: true, standalone: true,
imports: [ imports: [
HeaderComponent, HeaderComponent,
TimeAgoPipe TimeAgoPipe,
MatIconModule,
], ],
templateUrl: './vote.component.html', templateUrl: './vote.component.html',
styleUrl: './vote.component.scss' styleUrl: './vote.component.scss'
@@ -48,7 +50,7 @@ export class VoteComponent implements OnInit, OnDestroy {
} }
ngOnInit(): void { ngOnInit(): void {
this.refreshSubscription = interval(300000).subscribe(() => { this.refreshSubscription = interval(60000).subscribe(() => {
this.loadVoteStats(); this.loadVoteStats();
}); });
} }
@@ -94,9 +96,10 @@ export class VoteComponent implements OnInit, OnDestroy {
return false; return false;
} }
const now: Date = new Date(); const now: Date = new Date();
return ( const voteInfo = this.voteStats.allVoteInfo.find(voteInfo => voteInfo.siteName === voteSite);
this.voteStats.allVoteInfo.some(voteInfo => voteInfo.siteName === voteSite if (!voteInfo) {
&& voteInfo.lastVoteTimestamp - now.getTime() < 86400000) return true;
) }
return (now.getTime() - voteInfo.lastVoteTimestamp < 86400000)
} }
} }
+9
View File
@@ -155,6 +155,15 @@ time, mark, audio, video {
justify-content: center; justify-content: center;
} }
.full-width {
width: 100%;
}
.centered {
margin-left: auto;
margin-right: auto;
}
/* flex end */ /* flex end */
/* main css */ /* main css */