mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
feat(ui): add ContentHero component and detail view styles
Add a new standalone ContentHero component (template and TS) that provides a cinematic hero section with backdrop, poster, title, description, tags/meta/action slots, and a back button event emitter. The component chooses backdrop or poster as the background, falls back to a default poster image on load error, and exposes inputs for title, description, posterUrl, and backdropUrl. Introduce extensive cinematic styles for the detail view: - define CSS custom properties for light and dark themes, poster size, shadows, and color tokens - implement gradient vignette, layout sizing, and theming via :host and :host-context(.dark-theme) - change container sizing behavior to full-height and hide overflow These changes create a reusable hero UI and themed detail-page styles to support a Netflix-style streaming detail experience.
This commit is contained in:
1 parent
ad9bf45518
commit
afad80e61b
5 files changed
+900
-110
No files matched your search
@@ -1,140 +1,360 @@
|
||||
// ============================================================================
|
||||
// Netflix-style Detail View - Cinematic Streaming Design
|
||||
// ============================================================================
|
||||
|
||||
:host {
|
||||
--poster-width: 220px;
|
||||
--poster-shadow: 0 8px 40px rgba(0, 0, 0, 0.15);
|
||||
--content-max-width: 1400px;
|
||||
|
||||
// Light theme colors (default - no .dark-theme class on body)
|
||||
--hero-gradient-start: rgba(255, 250, 245, 0);
|
||||
--hero-gradient-mid: rgba(255, 250, 245, 0.75);
|
||||
--hero-gradient-end: rgba(255, 250, 245, 1);
|
||||
--surface-bg: #fffaf5;
|
||||
--text-primary: #1a1a1a;
|
||||
--text-secondary: rgba(26, 26, 26, 0.75);
|
||||
--text-muted: rgba(26, 26, 26, 0.5);
|
||||
--accent-color: #b81d24;
|
||||
--tag-bg: rgba(0, 0, 0, 0.06);
|
||||
--tag-border: rgba(0, 0, 0, 0.12);
|
||||
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
background: var(--surface-bg);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
// Dark theme overrides - cinematic dark mode
|
||||
:host-context(.dark-theme) {
|
||||
--hero-gradient-start: rgba(20, 20, 20, 0);
|
||||
--hero-gradient-mid: rgba(20, 20, 20, 0.7);
|
||||
--hero-gradient-end: rgba(20, 20, 20, 1);
|
||||
--surface-bg: #141414;
|
||||
--text-primary: #fff;
|
||||
--text-secondary: rgba(255, 255, 255, 0.7);
|
||||
--text-muted: rgba(255, 255, 255, 0.5);
|
||||
--accent-color: #e50914;
|
||||
--tag-bg: rgba(255, 255, 255, 0.1);
|
||||
--tag-border: rgba(255, 255, 255, 0.2);
|
||||
--poster-shadow: 0 8px 40px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Details Section - Content Elements
|
||||
// ============================================================================
|
||||
|
||||
.details {
|
||||
h2 {
|
||||
word-wrap: break-word;
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
// Styles for projected content elements
|
||||
|
||||
.container {
|
||||
display: grid;
|
||||
grid-gap: 20px;
|
||||
grid-template-columns: minmax(250px, 30%) minmax(300px, 1fr);
|
||||
grid-template-areas:
|
||||
'image details'
|
||||
'seasons seasons';
|
||||
padding: 20px;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
height: auto;
|
||||
&__tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 12px;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
background: var(--tag-bg);
|
||||
border: 1px solid var(--tag-border);
|
||||
border-radius: 4px;
|
||||
backdrop-filter: blur(8px);
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background-size: cover;
|
||||
z-index: -1;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
}
|
||||
|
||||
.seasons {
|
||||
padding: 20px;
|
||||
|
||||
.episodes {
|
||||
margin-bottom: 10px;
|
||||
|
||||
button {
|
||||
margin-top: 5px;
|
||||
&--rating {
|
||||
color: #ffd700;
|
||||
border-color: rgba(255, 215, 0, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
&__meta-item {
|
||||
font-size: 0.875rem;
|
||||
|
||||
.label {
|
||||
color: var(--text-muted);
|
||||
font-weight: 400;
|
||||
margin-bottom: 2px;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: var(--text-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Action Buttons - Play, Favorite
|
||||
// ============================================================================
|
||||
|
||||
.play-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 28px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
background: #fff;
|
||||
color: #141414;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
font-size: 24px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.favorite-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 20px;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 500;
|
||||
background: rgba(109, 109, 110, 0.7);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(109, 109, 110, 0.5);
|
||||
}
|
||||
|
||||
&--active {
|
||||
background: rgba(229, 9, 20, 0.8);
|
||||
|
||||
&:hover {
|
||||
background: rgba(229, 9, 20, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
font-size: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Content Sections - Below the hero fold
|
||||
// ============================================================================
|
||||
|
||||
.content-section {
|
||||
padding: 20px 0;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
&__title {
|
||||
font-size: 1.375rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 20px 0;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Seasons & Episodes - Inside details section
|
||||
// ============================================================================
|
||||
|
||||
.seasons {
|
||||
margin-top: 24px;
|
||||
width: 100%;
|
||||
|
||||
.season-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 0;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.expand-icon {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
}
|
||||
|
||||
.episodes {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
padding: 8px 0 16px;
|
||||
|
||||
button {
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.episodes-loading,
|
||||
.seasons-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 20px 0;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.no-episodes {
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
padding: 8px 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Seasons loading when inside details
|
||||
.details .seasons-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 20px 0;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
// Season container when inside details section
|
||||
.details app-season-container {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-top: 24px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// YouTube Trailer Section - Inside details
|
||||
// ============================================================================
|
||||
|
||||
.youtube-trailer {
|
||||
margin-top: 24px;
|
||||
width: 100%;
|
||||
|
||||
h3 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 12px 0;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.trailer-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 560px;
|
||||
aspect-ratio: 16 / 9;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
|
||||
iframe {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Legacy container support (for gradual migration)
|
||||
// ============================================================================
|
||||
|
||||
.container {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.image {
|
||||
grid-area: image;
|
||||
margin: 0;
|
||||
border-radius: 12px;
|
||||
max-width: 100%;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 12px;
|
||||
object-fit: cover;
|
||||
max-width: 450px;
|
||||
}
|
||||
|
||||
.placeholder-cover {
|
||||
width: 100%;
|
||||
aspect-ratio: 2/3;
|
||||
background-color: #999;
|
||||
border-radius: 12px;
|
||||
|
||||
&::after {
|
||||
content: 'No cover';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
}
|
||||
display: none; // Hidden - now using .poster in hero
|
||||
}
|
||||
|
||||
app-season-container {
|
||||
grid-area: seasons;
|
||||
width: 100%;
|
||||
}
|
||||
// ============================================================================
|
||||
// Mobile Responsive - Option A: Keep poster grid layout
|
||||
// ============================================================================
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.container {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-areas:
|
||||
'image'
|
||||
'details'
|
||||
'seasons';
|
||||
padding: 10px;
|
||||
:host {
|
||||
--poster-width: 120px;
|
||||
}
|
||||
|
||||
.image,
|
||||
.details {
|
||||
.action-buttons {
|
||||
width: 100%;
|
||||
flex: 1 1 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
|
||||
img {
|
||||
max-height: 500px;
|
||||
object-fit: contain;
|
||||
border-radius: 0;
|
||||
}
|
||||
// This class is in ContentHeroComponent now, but these rules apply to children?
|
||||
// No, .play-btn is a child of the PROJECTED content.
|
||||
// So we should target the buttons directly for mobile
|
||||
}
|
||||
|
||||
.details {
|
||||
padding: 0 10px;
|
||||
.play-btn,
|
||||
.favorite-btn {
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
padding: 10px 16px;
|
||||
}
|
||||
|
||||
.youtube-trailer {
|
||||
width: 100% !important;
|
||||
margin-top: 16px;
|
||||
|
||||
.trailer-wrapper {
|
||||
max-width: 100%;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.seasons {
|
||||
padding: 16px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
font-weight: 100;
|
||||
font-size: 0.9em;
|
||||
padding: 5px 0;
|
||||
text-decoration: underline;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
margin-top: 10px;
|
||||
}
|
||||
// ============================================================================
|
||||
// Animations
|
||||
// ============================================================================
|
||||
|
||||
.youtube-trailer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-self: center;
|
||||
width: 100%;
|
||||
max-width: 650px;
|
||||
margin: 0 auto;
|
||||
animation: fadeInUp 0.5s ease-out 0.4s both;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Utility - keep .label for backwards compatibility
|
||||
// ============================================================================
|
||||
|
||||
.label {
|
||||
font-weight: 400;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--text-muted);
|
||||
text-decoration: none;
|
||||
padding: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ export * from './lib/audio-player/audio-player.component';
|
||||
export * from './lib/channel-list-container/channel-list-container.component';
|
||||
export * from './lib/confirm-dialog/confirm-dialog.component';
|
||||
export * from './lib/confirm-dialog/dialog.service';
|
||||
export * from './lib/content-hero/content-hero.component';
|
||||
export * from './lib/epg-list/epg-item-description/epg-item-description.component';
|
||||
export * from './lib/epg-list/epg-list-item/epg-list-item.component';
|
||||
export * from './lib/epg-list/epg-list.component';
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
<!-- Hero Section with Backdrop -->
|
||||
<section class="hero">
|
||||
<!-- Back Button -->
|
||||
<button class="hero__back-button" (click)="onBack()">
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
</button>
|
||||
|
||||
<!-- Backdrop Image -->
|
||||
<div
|
||||
class="hero__backdrop"
|
||||
[class.hero__backdrop--blurred]="!backdropUrl"
|
||||
[style.backgroundImage]="
|
||||
backdropUrl
|
||||
? 'url(' + backdropUrl + ')'
|
||||
: posterUrl
|
||||
? 'url(' + posterUrl + ')'
|
||||
: 'none'
|
||||
"
|
||||
></div>
|
||||
|
||||
<!-- Gradient Vignette Overlay -->
|
||||
<div class="hero__vignette"></div>
|
||||
|
||||
<!-- Hero Content -->
|
||||
<div class="hero__content">
|
||||
<!-- Poster Card -->
|
||||
<div class="poster">
|
||||
@if (posterUrl) {
|
||||
<img
|
||||
class="poster__image"
|
||||
[src]="posterUrl"
|
||||
(error)="
|
||||
$event.target.src = './assets/images/default-poster.png'
|
||||
"
|
||||
alt="Poster"
|
||||
/>
|
||||
} @else {
|
||||
<div class="poster__placeholder">No Poster</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Details -->
|
||||
<div class="details">
|
||||
<h1 class="details__title">{{ title }}</h1>
|
||||
|
||||
<!-- Tags Slot -->
|
||||
<div class="details__tags">
|
||||
<ng-content select="[hero-tags]"></ng-content>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
@if (description) {
|
||||
<p class="details__description">
|
||||
{{ description }}
|
||||
</p>
|
||||
}
|
||||
|
||||
<!-- Metadata Grid Slot -->
|
||||
<div class="details__meta">
|
||||
<ng-content select="[hero-meta]"></ng-content>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons Slot -->
|
||||
<div class="action-buttons">
|
||||
<ng-content select="[hero-actions]"></ng-content>
|
||||
</div>
|
||||
|
||||
<!-- Additional Content (Seasons, Trailers, etc.) -->
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,473 @@
|
||||
// ============================================================================
|
||||
// Netflix-style Detail View - Cinematic Streaming Design
|
||||
// ============================================================================
|
||||
|
||||
:host {
|
||||
--poster-width: 220px;
|
||||
--poster-shadow: 0 8px 40px rgba(0, 0, 0, 0.15);
|
||||
--content-max-width: 1400px;
|
||||
|
||||
// Light theme colors (default - no .dark-theme class on body)
|
||||
--hero-gradient-start: rgba(255, 250, 245, 0);
|
||||
--hero-gradient-mid: rgba(255, 250, 245, 0.75);
|
||||
--hero-gradient-end: rgba(255, 250, 245, 1);
|
||||
--surface-bg: #fffaf5;
|
||||
--text-primary: #1a1a1a;
|
||||
--text-secondary: rgba(26, 26, 26, 0.75);
|
||||
--text-muted: rgba(26, 26, 26, 0.5);
|
||||
--accent-color: #b81d24;
|
||||
--tag-bg: rgba(0, 0, 0, 0.06);
|
||||
--tag-border: rgba(0, 0, 0, 0.12);
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
background: var(--surface-bg);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
// Dark theme overrides - cinematic dark mode
|
||||
:host-context(.dark-theme) {
|
||||
--hero-gradient-start: rgba(20, 20, 20, 0);
|
||||
--hero-gradient-mid: rgba(20, 20, 20, 0.7);
|
||||
--hero-gradient-end: rgba(20, 20, 20, 1);
|
||||
--surface-bg: #141414;
|
||||
--text-primary: #fff;
|
||||
--text-secondary: rgba(255, 255, 255, 0.7);
|
||||
--text-muted: rgba(255, 255, 255, 0.5);
|
||||
--accent-color: #e50914;
|
||||
--tag-bg: rgba(255, 255, 255, 0.1);
|
||||
--tag-border: rgba(255, 255, 255, 0.2);
|
||||
--poster-shadow: 0 8px 40px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Hero Section - Full container with backdrop
|
||||
// ============================================================================
|
||||
|
||||
.hero {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: var(
|
||||
--surface-bg
|
||||
); // Solid background prevents any bleed-through
|
||||
|
||||
// Floating back button over backdrop
|
||||
&__back-button {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
left: 16px;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(8px);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
color: #fff;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
font-size: 24px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
// Backdrop image layer - fixed at top, never repeats
|
||||
&__backdrop {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: auto; // Only use height, not bottom
|
||||
height: 60%;
|
||||
background-size: cover;
|
||||
background-position: center 20%;
|
||||
background-repeat: no-repeat;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
|
||||
// Fallback blur effect when using poster as backdrop
|
||||
&--blurred {
|
||||
filter: blur(40px) saturate(1.3);
|
||||
transform: scale(1.4);
|
||||
height: 65%;
|
||||
}
|
||||
}
|
||||
|
||||
// Gradient overlays for text legibility
|
||||
&__vignette {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0; // Full height to cover everything
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
transparent 0%,
|
||||
transparent 35%,
|
||||
var(--surface-bg) 55%,
|
||||
var(--surface-bg) 100%
|
||||
);
|
||||
|
||||
// Top area gradient for text legibility over backdrop
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 60%;
|
||||
background: linear-gradient(
|
||||
to top,
|
||||
var(--hero-gradient-end) 0%,
|
||||
var(--hero-gradient-mid) 50%,
|
||||
var(--hero-gradient-start) 100%
|
||||
);
|
||||
}
|
||||
|
||||
// Side fade for poster area
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 50%;
|
||||
width: 40%;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
var(--hero-gradient-mid) 0%,
|
||||
var(--hero-gradient-start) 100%
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Scrollable content area
|
||||
&__content {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
gap: 32px;
|
||||
align-items: flex-start;
|
||||
padding: 140px 40px 60px;
|
||||
max-width: var(--content-max-width);
|
||||
margin: 0 auto;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
box-sizing: border-box;
|
||||
|
||||
// Hide scrollbar but keep functionality
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Poster Card - Floating with shadow
|
||||
// ============================================================================
|
||||
|
||||
.poster {
|
||||
flex-shrink: 0;
|
||||
width: var(--poster-width);
|
||||
align-self: flex-start;
|
||||
|
||||
&:hover .poster__image {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
&__image {
|
||||
width: 100%;
|
||||
aspect-ratio: 2 / 3;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--poster-shadow);
|
||||
background: linear-gradient(135deg, #2a2a2a 0%, #1a1a1a 100%);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
&__placeholder {
|
||||
width: 100%;
|
||||
aspect-ratio: 2 / 3;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, #3a3a3a 0%, #2a2a2a 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.875rem;
|
||||
box-shadow: var(--poster-shadow);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Details Section - Title, metadata, description
|
||||
// ============================================================================
|
||||
|
||||
.details {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding-bottom: 24px;
|
||||
|
||||
&__title {
|
||||
font-size: clamp(1.75rem, 4vw, 3rem);
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
margin: 0 0 16px 0;
|
||||
letter-spacing: -0.02em;
|
||||
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
&__tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
// These styles are for Projected Content, we need to ensure they apply
|
||||
// using ::ng-deep or just assume the global styles will handle them?
|
||||
// Actually, since we project content, the styles defined here might NOT apply to the projected content
|
||||
// unless we use ::ng-deep or the content itself has these styles.
|
||||
// However, since we are moving the "shell", the container classes (.details__tags) are here.
|
||||
// The inner items (.details__tag) will be in the projected content.
|
||||
// So we need ::ng-deep here for .details__tag.
|
||||
|
||||
::ng-deep .details__tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 12px;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
background: var(--tag-bg);
|
||||
border: 1px solid var(--tag-border);
|
||||
border-radius: 4px;
|
||||
backdrop-filter: blur(8px);
|
||||
|
||||
&--rating {
|
||||
color: #ffd700;
|
||||
border-color: rgba(255, 215, 0, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
&__description {
|
||||
font-size: 0.9375rem;
|
||||
line-height: 1.6;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 20px;
|
||||
max-width: 600px;
|
||||
|
||||
// Truncate long descriptions
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 4;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px 32px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
// Similar issue for meta items
|
||||
::ng-deep .details__meta-item {
|
||||
font-size: 0.875rem;
|
||||
|
||||
.label {
|
||||
color: var(--text-muted);
|
||||
font-weight: 400;
|
||||
margin-bottom: 2px;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: var(--text-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Action Buttons - Play, Favorite
|
||||
// ============================================================================
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 0;
|
||||
|
||||
// Use ::ng-deep to style the projected buttons
|
||||
::ng-deep .play-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 28px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
background: #fff;
|
||||
color: #141414;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
font-size: 24px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep .favorite-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 20px;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 500;
|
||||
background: rgba(109, 109, 110, 0.7);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(109, 109, 110, 0.5);
|
||||
}
|
||||
|
||||
&--active {
|
||||
background: rgba(229, 9, 20, 0.8);
|
||||
|
||||
&:hover {
|
||||
background: rgba(229, 9, 20, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
font-size: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// Mobile Responsive - Option A: Keep poster grid layout
|
||||
// ============================================================================
|
||||
|
||||
@media (max-width: 800px) {
|
||||
:host {
|
||||
--poster-width: 120px;
|
||||
}
|
||||
|
||||
.hero {
|
||||
&__backdrop {
|
||||
height: 45%;
|
||||
|
||||
&--blurred {
|
||||
height: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
&__vignette {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
transparent 0%,
|
||||
transparent 25%,
|
||||
var(--surface-bg) 45%,
|
||||
var(--surface-bg) 100%
|
||||
);
|
||||
|
||||
&::before {
|
||||
height: 50%;
|
||||
}
|
||||
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
padding: 60px 20px 40px;
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.poster {
|
||||
// Mobile poster adjustments - none needed
|
||||
}
|
||||
|
||||
.details {
|
||||
&__title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
&__description {
|
||||
-webkit-line-clamp: 3;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
gap: 12px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
width: 100%;
|
||||
|
||||
::ng-deep .play-btn,
|
||||
::ng-deep .favorite-btn {
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
padding: 10px 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Animations
|
||||
// ============================================================================
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.hero__content {
|
||||
animation: fadeInUp 0.6s ease-out 0.2s both;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
|
||||
@Component({
|
||||
selector: 'app-content-hero',
|
||||
standalone: true,
|
||||
imports: [CommonModule, MatIconModule, MatButtonModule],
|
||||
templateUrl: './content-hero.component.html',
|
||||
styleUrls: ['./content-hero.component.scss'],
|
||||
})
|
||||
export class ContentHeroComponent {
|
||||
@Input() title: string | undefined;
|
||||
@Input() description: string | undefined;
|
||||
@Input() posterUrl: string | undefined;
|
||||
@Input() backdropUrl: string | undefined;
|
||||
|
||||
@Output() backClicked = new EventEmitter<void>();
|
||||
|
||||
onBack(): void {
|
||||
this.backClicked.emit();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user