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;
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();
}
}
}
@@ -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);
@@ -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);
@@ -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);
+1 -1
View File
@@ -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 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 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)
}
}
+9
View File
@@ -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 */