style: enhance UI components with improved styles, search functionality, and error handling

This commit is contained in:
4gray committed 2026-02-26 00:45:58 +01:00
1 parent aa6a753aa5
commit 09efead104
15 files changed
+482 -156

No files matched your search

@@ -21,7 +21,7 @@
width: '100%',
'border-radius': '8px',
'aspect-ratio': '2/3',
display: 'block'
display: 'block',
}"
/>
</div>
@@ -35,7 +35,7 @@
height: '40px',
width: '60%',
'margin-bottom': '16px',
display: 'block'
display: 'block',
}"
/>
@@ -46,7 +46,7 @@
[theme]="{
height: '24px',
width: '60px',
display: 'block'
display: 'block',
}"
/>
</div>
@@ -58,7 +58,7 @@
height: '16px',
'margin-bottom': '8px',
width: '100%',
display: 'block'
display: 'block',
}"
/>
@@ -69,7 +69,7 @@
[theme]="{
height: '40px',
width: '100px',
display: 'block'
display: 'block',
}"
/>
</div>
@@ -81,7 +81,7 @@
[theme]="{
height: '48px',
width: '140px',
display: 'block'
display: 'block',
}"
/>
</div>
@@ -91,13 +91,15 @@
<!-- Backdrop Image -->
<div
class="hero__backdrop"
[class.hero__backdrop--blurred]="!backdropUrl()"
[class.hero__backdrop--blurred]="
!backdropUrl() && posterUrl() && !posterError()
"
[style.backgroundImage]="
backdropUrl()
backdropUrl() && !posterError()
? 'url(' + backdropUrl() + ')'
: posterUrl()
: posterUrl() && !posterError()
? 'url(' + posterUrl() + ')'
: 'none'
: fallbackBackdropBackground()
"
></div>
@@ -108,24 +110,30 @@
<div class="hero__content">
<!-- Poster Card -->
<div class="poster">
@if (posterUrl()) {
@if (posterUrl() && !posterError()) {
<img
class="poster__image"
[src]="posterUrl()"
(error)="
$event.target.src =
'./assets/images/default-poster.png'
"
(error)="onPosterError()"
alt="Poster"
/>
} @else {
<div class="poster__placeholder">No Poster</div>
<div
class="poster__placeholder"
[style.backgroundImage]="fallbackPosterBackground()"
>
<mat-icon class="poster__placeholder-icon"
>movie</mat-icon
>
<span class="poster__placeholder-text">{{
formattedTitle()
}}</span>
</div>
}
</div>
<!-- Details -->
<div class="details">
<h1 class="details__title">{{ title() }}</h1>
<h1 class="details__title">{{ formattedTitle() }}</h1>
<!-- Tags Slot -->
<div class="details__tags">
@@ -233,11 +233,32 @@
border-radius: 8px;
background: linear-gradient(135deg, #3a3a3a 0%, #2a2a2a 100%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: var(--text-muted);
font-size: 0.875rem;
color: rgba(255, 255, 255, 0.8);
box-shadow: var(--poster-shadow);
overflow: hidden;
&-icon {
font-size: 48px;
width: 48px;
height: 48px;
opacity: 0.5;
margin-bottom: 12px;
}
&-text {
font-size: 1rem;
font-weight: 500;
text-align: center;
display: -webkit-box;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
overflow: hidden;
overflow-wrap: anywhere;
padding: 16px;
}
}
}
@@ -257,6 +278,11 @@
margin: 0 0 16px 0;
letter-spacing: -0.02em;
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
overflow-wrap: anywhere;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
&__tags {
@@ -1,5 +1,4 @@
import { Component, input, output } from '@angular/core';
import { Component, computed, input, output, signal } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { NgxSkeletonLoaderComponent } from 'ngx-skeleton-loader';
@@ -7,11 +6,7 @@ import { NgxSkeletonLoaderComponent } from 'ngx-skeleton-loader';
@Component({
selector: 'app-content-hero',
standalone: true,
imports: [
MatIconModule,
MatButtonModule,
NgxSkeletonLoaderComponent
],
imports: [MatIconModule, MatButtonModule, NgxSkeletonLoaderComponent],
templateUrl: './content-hero.component.html',
styleUrls: ['./content-hero.component.scss'],
})
@@ -24,6 +19,40 @@ export class ContentHeroComponent {
readonly errorMessage = input<string>();
readonly backClicked = output<void>();
readonly posterError = signal(false);
onPosterError(): void {
this.posterError.set(true);
}
readonly formattedTitle = computed(() => {
const t = this.title();
if (!t) return '';
// Replace underscores with spaces for cleaner UX on slug/filename style titles
return t.replace(/_/g, ' ').replace(/\s+/g, ' ').trim();
});
private calculateHue(text: string): number {
if (!text) return 0;
let hash = 0;
for (let i = 0; i < text.length; i++) {
hash = text.charCodeAt(i) + ((hash << 5) - hash);
hash = hash & hash;
}
return Math.abs(hash) % 360;
}
readonly fallbackPosterBackground = computed(() => {
const hue = this.calculateHue(this.title() || 'placeholder');
const h2 = (hue + 40) % 360;
return `linear-gradient(135deg, hsl(${hue}, 40%, 25%) 0%, hsl(${h2}, 50%, 15%) 100%)`;
});
readonly fallbackBackdropBackground = computed(() => {
const hue = this.calculateHue(this.title() || 'placeholder');
const h2 = (hue + 60) % 360;
return `linear-gradient(135deg, hsl(${hue}, 50%, 15%) 0%, hsl(${h2}, 80%, 5%) 100%)`;
});
onBack(): void {
this.backClicked.emit();