Refactor Nickname Generator component with Angular Material, update logic for fields and commands, and improve styling.

This commit is contained in:
akastijn 2025-10-29 21:39:39 +01:00
parent 423d5e4a4c
commit 2be79c180a
8 changed files with 412 additions and 91 deletions

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)
},
];

View File

@ -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>

View File

@ -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;
}

View File

@ -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;');
}
}

View File

@ -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>

View File

@ -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();
});
});

View File

@ -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 {
}