Removable footer, compact styling for particle creator

This commit is contained in:
akastijn 2025-12-27 21:04:55 +01:00
parent d1ff7b3f88
commit d9b60d8a94
13 changed files with 175 additions and 195 deletions

View File

@ -1,29 +0,0 @@
import {TestBed} from '@angular/core/testing';
import {AppComponent} from './app.component';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AppComponent],
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it(`should have the 'frontend' title`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app.title).toEqual('frontend');
});
it('should render title', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, frontend');
});
});

View File

@ -1,7 +1,7 @@
import {Component, OnInit} from '@angular/core'; import {Component, inject, OnInit} from '@angular/core';
import {Meta, Title} from '@angular/platform-browser'; import {Meta, Title} from '@angular/platform-browser';
import {ALTITUDE_VERSION} from '@custom-types/constant'; import {ALTITUDE_VERSION} from '@custom-types/constant';
import {Router, RouterOutlet} from '@angular/router'; import {RouterOutlet} from '@angular/router';
import {FooterComponent} from '@pages/footer/footer/footer.component'; import {FooterComponent} from '@pages/footer/footer/footer.component';
@Component({ @Component({
@ -27,9 +27,8 @@ export class AppComponent implements OnInit {
ALTITUDE_VERSION + ',altitude,alttd,play,join,find,friends,friendly,simple,private,whitelist,whitelisted,creative,' + ALTITUDE_VERSION + ',altitude,alttd,play,join,find,friends,friendly,simple,private,whitelist,whitelisted,creative,' +
'worldedit' 'worldedit'
constructor(private titleService: Title, private metaService: Meta, private router: Router) { private readonly titleService = inject(Title);
private readonly metaService = inject(Meta);
}
ngOnInit(): void { ngOnInit(): void {
this.titleService.setTitle(this.title) this.titleService.setTitle(this.title)

View File

@ -1,44 +1,49 @@
<footer> @if (hideFooter()) {
<div class="footer">
<div class="footerInner"> } @else {
<div class="footerText"> <footer>
<h2>ABOUT US</h2> <div class="footer">
<p>Altitude is a community-centered {{ ALTITUDE_VERSION }} survival server. We're one of those servers you come <div class="footerInner">
to call "home". We are your place to get together with friends and play survival, with a few extra features <div class="footerText">
suggested by our community!</p> <h2>ABOUT US</h2>
<div class="followUs" style="height: 35px; display: flex; align-items: flex-end;"> <p>Altitude is a community-centered {{ ALTITUDE_VERSION }} survival server. We're one of those servers you
<a target="_blank" rel="noopener" href="https://discordapp.com/invite/TGqpzCJ"> come
<img priority ngSrc="/public/img/logos/discord.png" alt="Discord Button" height="32" width="32"> to call "home". We are your place to get together with friends and play survival, with a few extra features
</a> suggested by our community!</p>
<a target="_blank" rel="noopener" href="https://twitter.com/alttdmc"> <div class="followUs" style="height: 35px; display: flex; align-items: flex-end;">
<img ngSrc="/public/img/logos/twitter.png" alt="Twitter Button" height="32" width="32"> <a target="_blank" rel="noopener" href="https://discordapp.com/invite/TGqpzCJ">
</a> <img priority ngSrc="/public/img/logos/discord.png" alt="Discord Button" height="32" width="32">
<a target="_blank" rel="noopener" href="https://instagram.com/alttdmc"> </a>
<img ngSrc="/public/img/logos/instagram.png" alt="Instagram Button" height="32" width="32"> <a target="_blank" rel="noopener" href="https://twitter.com/alttdmc">
</a> <img ngSrc="/public/img/logos/twitter.png" alt="Twitter Button" height="32" width="32">
</a>
<a target="_blank" rel="noopener" href="https://instagram.com/alttdmc">
<img ngSrc="/public/img/logos/instagram.png" alt="Instagram Button" height="32" width="32">
</a>
</div>
</div>
<div class="footerNav">
<h2>COMMUNITY</h2>
<ul>
<li><a target="_blank" rel="noopener" href="https://discordapp.com/invite/TGqpzCJ">Discord</a></li>
<li><a target="_blank" rel="noopener" href="https://alttd.com/blog">Blog</a></li>
<li><a target="_blank" rel="noopener" href="https://twitter.com/alttdmc">Twitter</a></li>
<li><a target="_blank" rel="noopener" href="https://instagram.com/alttdmc">Instagram</a></li>
<li><a target="_blank" rel="noopener" href="https://reddit.com/r/alttd">Reddit</a></li>
</ul>
</div>
<div class="footerNav">
<h2>SERVER</h2>
<ul>
<li><a [routerLink]="['/about']">About Us</a></li>
<li><a [routerLink]="['/team']">Staffing Team</a></li>
<li><a [routerLink]="['/privacy']">Privacy Policy</a></li>
<li><a [routerLink]="['/terms']">Terms of Use</a></li>
</ul>
</div> </div>
</div> </div>
<div class="footerNav"> <p class="copyright">Copyright © 2015-{{ getCurrentYear() }} Altitude. All rights Reserved. Not affiliated with
<h2>COMMUNITY</h2> Mojang AB or Microsoft.</p>
<ul>
<li><a target="_blank" rel="noopener" href="https://discordapp.com/invite/TGqpzCJ">Discord</a></li>
<li><a target="_blank" rel="noopener" href="https://alttd.com/blog">Blog</a></li>
<li><a target="_blank" rel="noopener" href="https://twitter.com/alttdmc">Twitter</a></li>
<li><a target="_blank" rel="noopener" href="https://instagram.com/alttdmc">Instagram</a></li>
<li><a target="_blank" rel="noopener" href="https://reddit.com/r/alttd">Reddit</a></li>
</ul>
</div>
<div class="footerNav">
<h2>SERVER</h2>
<ul>
<li><a [routerLink]="['/about']">About Us</a></li>
<li><a [routerLink]="['/team']">Staffing Team</a></li>
<li><a [routerLink]="['/privacy']">Privacy Policy</a></li>
<li><a [routerLink]="['/terms']">Terms of Use</a></li>
</ul>
</div>
</div> </div>
<p class="copyright">Copyright © 2015-{{ getCurrentYear() }} Altitude. All rights Reserved. Not affiliated with </footer>
Mojang AB or Microsoft.</p> }
</div>
</footer>

View File

@ -1,19 +1,21 @@
import {Component} from '@angular/core'; import {Component, computed, inject} from '@angular/core';
import {ALTITUDE_VERSION} from '@custom-types/constant'; import {ALTITUDE_VERSION} from '@custom-types/constant';
import { NgOptimizedImage } from '@angular/common'; import {FooterService} from '@services/footer.service';
import {RouterLink} from '@angular/router'; import {RouterLink} from '@angular/router';
@Component({ @Component({
selector: 'app-footer', selector: 'app-footer',
standalone: true, standalone: true,
imports: [ imports: [
RouterLink, RouterLink
NgOptimizedImage ],
],
templateUrl: './footer.component.html', templateUrl: './footer.component.html',
styleUrl: './footer.component.scss' styleUrl: './footer.component.scss'
}) })
export class FooterComponent { export class FooterComponent {
private readonly footerService: FooterService = inject(FooterService);
hideFooter = computed(() => (this.footerService.hideFooter()));
public getCurrentYear() { public getCurrentYear() {
return new Date().getFullYear(); return new Date().getFullYear();
} }

View File

@ -1,12 +1,27 @@
<div class="card-div"> <div class="card-div">
<mat-card> <mat-card>
<mat-card-header> <mat-card-header class="frame-header-fullwidth">
<mat-card-title>Frames</mat-card-title> <div class="frame-header-row" mat-card-title>
<span class="frame-title-text">Frames</span>
<div>
<ng-content></ng-content>
<button mat-icon-button color="warn" (click)="removeFrame(currentFrame)"
matTooltip="Delete {{currentFrame}}"
[disabled]="frames.length <= 1">
<mat-icon [class.can-delete]="frames.length > 1" [class.can-not-delete]="frames.length <= 1">
delete
</mat-icon>
</button>
<button mat-icon-button (click)="addFrame()" matTooltip="Add Frame">
<mat-icon class="add-button">add</mat-icon>
</button>
</div>
</div>
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
<div class="frames-container"> <div class="frames-container">
<mat-tab-group [selectedIndex]="frames.indexOf(currentFrame)" <mat-tab-group [selectedIndex]="frames.indexOf(currentFrame)"
(selectedIndexChange)="switchFrame(frames[$event])"> (selectedIndexChange)="switchFrame(frames[$event])">
@for (frameId of frames; track frameId) { @for (frameId of frames; track frameId) {
<mat-tab [label]="frameId"> <mat-tab [label]="frameId">
<div class="frame-content"> <div class="frame-content">
@ -33,21 +48,10 @@
</div> </div>
} }
</div> </div>
<div class="frame-actions">
<button mat-raised-button color="warn" (click)="removeFrame(frameId)"
[disabled]="frames.length <= 1">
Remove Frame
</button>
</div>
</div> </div>
</mat-tab> </mat-tab>
} }
</mat-tab-group> </mat-tab-group>
<div class="add-frame">
<button mat-raised-button color="primary" (click)="addFrame()">
Add New Frame
</button>
</div>
</div> </div>
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>

View File

@ -6,6 +6,36 @@
padding: 15px; padding: 15px;
} }
:host ::ng-deep .mat-mdc-card-header-text {
width: 90%;
text-align: center;
}
.frame-header-row {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.frame-title-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.add-button {
color: green
}
.can-delete {
color: red;
}
.can-not-delete {
color: gray;
}
.particles-list { .particles-list {
height: 550px; height: 550px;
overflow-y: auto; overflow-y: auto;

View File

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

View File

@ -1,5 +1,5 @@
import {Component} from '@angular/core'; import {Component, inject} from '@angular/core';
import {MatButton, MatIconButton} from "@angular/material/button"; import {MatIconButton} from "@angular/material/button";
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from "@angular/material/card"; import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from "@angular/material/card";
import {MatTab, MatTabGroup} from "@angular/material/tabs"; import {MatTab, MatTabGroup} from "@angular/material/tabs";
@ -7,11 +7,11 @@ import {ParticleData} from '../../models/particle.model';
import {MatIcon} from '@angular/material/icon'; import {MatIcon} from '@angular/material/icon';
import {ParticleManagerService} from '../../services/particle-manager.service'; import {ParticleManagerService} from '../../services/particle-manager.service';
import {FrameManagerService} from '../../services/frame-manager.service'; import {FrameManagerService} from '../../services/frame-manager.service';
import {MatTooltipModule} from '@angular/material/tooltip';
@Component({ @Component({
selector: 'app-frames', selector: 'app-frames',
imports: [ imports: [
MatButton,
MatCard, MatCard,
MatCardContent, MatCardContent,
MatCardHeader, MatCardHeader,
@ -19,17 +19,15 @@ import {FrameManagerService} from '../../services/frame-manager.service';
MatIcon, MatIcon,
MatIconButton, MatIconButton,
MatTab, MatTab,
MatTabGroup MatTabGroup,
], MatTooltipModule,
],
templateUrl: './frames.component.html', templateUrl: './frames.component.html',
styleUrl: './frames.component.scss' styleUrl: './frames.component.scss'
}) })
export class FramesComponent { export class FramesComponent {
private particleManagerService = inject(ParticleManagerService);
constructor( private frameManagerService = inject(FrameManagerService);
private particleManagerService: ParticleManagerService,
private frameManagerService: FrameManagerService) {
}
/** /**
* Get the particle data * Get the particle data

View File

@ -6,35 +6,35 @@
</app-header> </app-header>
<main> <main>
<section class="darkmodeSection"> <app-full-size [hideFooter]="true">
<section class="column"> <section class="darkmodeSection full-height">
<div class="renderer-section column"> <section class="column">
<div class="flex row"> <div class="renderer-section column">
<div class="flex side-column"> <div class="flex row">
<app-particle-properties></app-particle-properties> <div class="flex side-column">
</div> <app-particle-properties></app-particle-properties>
<div class="flex middle-column">
<app-render-container></app-render-container>
<div class="plane-controls">
<label>Plane Position (Z-axis):</label>
<mat-slider [min]="minOffset" [max]="maxOffset" step="1" #planeSlider>
<input matSliderThumb [(ngModel)]="planePosition" (input)="updatePlanePosition($event)">
</mat-slider>
<span>{{ planePosition }} offset from center</span>
</div> </div>
</div> <div class="flex middle-column">
<div class="flex side-column"> <app-render-container></app-render-container>
<app-particle></app-particle> <div class="plane-controls">
<app-frames></app-frames> <label>Plane Position (Z-axis):</label>
<div> <mat-slider [min]="minOffset" [max]="maxOffset" step="1" #planeSlider>
<button mat-fab extended (click)="copyJson()"> <input matSliderThumb [(ngModel)]="planePosition" (input)="updatePlanePosition($event)">
<mat-icon>content_copy</mat-icon> </mat-slider>
Copy JSON to clipboard <span>{{ planePosition }} offset from center</span>
</button> </div>
</div>
<div class="flex side-column">
<app-particle></app-particle>
<app-frames>
<button mat-icon-button matTooltip="Copy JSON to clipboard" (click)="copyJson()">
<mat-icon>content_copy</mat-icon>
</button>
</app-frames>
</div> </div>
</div> </div>
</div> </div>
</div> </section>
</section> </section>
</section> </app-full-size>
</main> </main>

View File

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

View File

@ -1,4 +1,4 @@
import {Component, ElementRef, ViewChild} from '@angular/core'; import {Component, ElementRef, inject, ViewChild} from '@angular/core';
import {FormsModule, ReactiveFormsModule} from '@angular/forms'; import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {MatButtonModule} from '@angular/material/button'; import {MatButtonModule} from '@angular/material/button';
@ -23,6 +23,8 @@ import {FramesComponent} from './components/frames/frames.component';
import {MatSnackBar} from '@angular/material/snack-bar'; import {MatSnackBar} from '@angular/material/snack-bar';
import {RenderContainerComponent} from './components/render-container/render-container.component'; import {RenderContainerComponent} from './components/render-container/render-container.component';
import {ParticlesService} from '@api'; import {ParticlesService} from '@api';
import {MatTooltipModule} from '@angular/material/tooltip';
import {FullSizeComponent} from '@shared-components/full-size/full-size.component';
@Component({ @Component({
selector: 'app-particles', selector: 'app-particles',
@ -43,21 +45,20 @@ import {ParticlesService} from '@api';
PropertiesComponent, PropertiesComponent,
ParticleComponent, ParticleComponent,
FramesComponent, FramesComponent,
RenderContainerComponent RenderContainerComponent,
], MatTooltipModule,
FullSizeComponent
],
templateUrl: './particles.component.html', templateUrl: './particles.component.html',
styleUrl: './particles.component.scss' styleUrl: './particles.component.scss'
}) })
export class ParticlesComponent { export class ParticlesComponent {
@ViewChild('planeSlider') planeSlider!: ElementRef; @ViewChild('planeSlider') planeSlider!: ElementRef;
constructor( private readonly intersectionPlaneService = inject(IntersectionPlaneService);
private intersectionPlaneService: IntersectionPlaneService, private readonly particleManagerService = inject(ParticleManagerService);
private particleManagerService: ParticleManagerService, private readonly matSnackBar = inject(MatSnackBar);
private matSnackBar: MatSnackBar, private readonly particlesService = inject(ParticlesService);
private particlesService: ParticlesService,
) {
}
/** /**
* Update plane position based on slider * Update plane position based on slider

View File

@ -0,0 +1,10 @@
import {Injectable, signal} from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class FooterService {
public hideFooter = signal<boolean>(false);
}

View File

@ -1,4 +1,5 @@
import {AfterViewInit, Component, ElementRef, Input, OnDestroy, Renderer2} from '@angular/core'; import {AfterViewInit, Component, ElementRef, inject, input, OnDestroy, OnInit, Renderer2} from '@angular/core';
import {FooterService} from '@services/footer.service';
@Component({ @Component({
selector: 'app-full-size', selector: 'app-full-size',
@ -7,17 +8,20 @@ import {AfterViewInit, Component, ElementRef, Input, OnDestroy, Renderer2} from
templateUrl: './full-size.component.html', templateUrl: './full-size.component.html',
styleUrl: './full-size.component.scss' styleUrl: './full-size.component.scss'
}) })
export class FullSizeComponent implements AfterViewInit, OnDestroy { export class FullSizeComponent implements OnInit, AfterViewInit, OnDestroy {
private resizeObserver: ResizeObserver | null = null; private resizeObserver: ResizeObserver | null = null;
private boundHandleResize: any; private boundHandleResize: any;
// Optional extra offset in pixels to subtract from available height // Optional extra offset in pixels to subtract from available height
@Input() extraOffset: number = 0; extraOffset = input<number>(0);
hideFooter = input<boolean>(false);
constructor( private readonly elementRef = inject(ElementRef);
private elementRef: ElementRef, private readonly renderer = inject(Renderer2);
private renderer: Renderer2 private readonly footerService = inject(FooterService);
) {
ngOnInit(): void {
this.footerService.hideFooter.set(this.hideFooter());
} }
ngAfterViewInit(): void { ngAfterViewInit(): void {
@ -40,6 +44,8 @@ export class FullSizeComponent implements AfterViewInit, OnDestroy {
if (this.boundHandleResize) { if (this.boundHandleResize) {
window.removeEventListener('resize', this.boundHandleResize); window.removeEventListener('resize', this.boundHandleResize);
} }
this.footerService.hideFooter.set(false);
} }
private handleResize() { private handleResize() {
@ -70,9 +76,9 @@ export class FullSizeComponent implements AfterViewInit, OnDestroy {
if (container) { if (container) {
const headerHeight = headerElement ? headerElement.getBoundingClientRect().height : 0; const headerHeight = headerElement ? headerElement.getBoundingClientRect().height : 0;
const footerHeight = footerElement ? footerElement.getBoundingClientRect().height : 0; const footerHeight = this.hideFooter() ? 0 : (footerElement ? footerElement.getBoundingClientRect().height : 0);
const totalOffset = headerHeight + footerHeight + (this.extraOffset || 0); const totalOffset = headerHeight + footerHeight + (this.extraOffset() || 0);
const calculatedHeight = `calc(100vh - ${totalOffset}px)`; const calculatedHeight = `calc(100vh - ${totalOffset}px)`;
this.renderer.setStyle(container, 'height', calculatedHeight); this.renderer.setStyle(container, 'height', calculatedHeight);
} }