1dbcc39bc7
Major UI/UX overhaul for SearchMovies.razor and SearchShows.razor, including new navigation toggles, animated search forms, improved results display, and enhanced styling. Moved layout and header styles from MainLayout.razor to CSS, unified button styles on Home.razor, and added interactive elements and animations for a more engaging user experience.
945 lines
27 KiB
Plaintext
945 lines
27 KiB
Plaintext
@page "/searchshows"
|
|
@using MovieActorLookup.Models
|
|
@inject ShowService ShowService
|
|
@inject IJSRuntime JSRuntime
|
|
@rendermode InteractiveServer
|
|
|
|
<div class="search-container">
|
|
<div class="search-header">
|
|
<div class="navigation-toggle">
|
|
<a href="/searchmovies" class="nav-btn movies-btn">
|
|
<span class="nav-icon">🎬</span>
|
|
<span class="nav-text">Switch to Movies</span>
|
|
</a>
|
|
</div>
|
|
<h2 class="search-title">Discover Actors & Their Shows</h2>
|
|
<p class="search-subtitle">Search by TV show and character to explore an actor's complete filmography</p>
|
|
</div>
|
|
|
|
<div class="search-form">
|
|
<div class="input-group">
|
|
<div class="input-wrapper">
|
|
<input type="text"
|
|
class="search-input"
|
|
@bind="_tvTitle"
|
|
placeholder="Enter TV show title..."
|
|
@onkeypress="@(async (e) => { if (e.Key == "Enter") await SearchForActorAndCharacters(); })" />
|
|
<div class="input-icon">📺</div>
|
|
</div>
|
|
<div class="input-wrapper">
|
|
<input type="text"
|
|
class="search-input"
|
|
@bind="_characterName"
|
|
placeholder="Enter character name..."
|
|
@onkeypress="@(async (e) => { if (e.Key == "Enter") await SearchForActorAndCharacters(); })" />
|
|
<div class="input-icon">🎭</div>
|
|
</div>
|
|
</div>
|
|
<button class="search-btn @(_isSearching ? "searching" : "")"
|
|
@onclick="SearchForActorAndCharacters"
|
|
disabled="@_isSearching">
|
|
@if (_isSearching)
|
|
{
|
|
<span class="spinner"></span>
|
|
<span>Searching...</span>
|
|
}
|
|
else
|
|
{
|
|
<span>🔍 Search</span>
|
|
}
|
|
</button>
|
|
</div>
|
|
|
|
@if (_isSearching)
|
|
{
|
|
<div class="loading-state">
|
|
<div class="loading-animation">
|
|
<div class="loading-dot"></div>
|
|
<div class="loading-dot"></div>
|
|
<div class="loading-dot"></div>
|
|
</div>
|
|
<p>Finding the perfect match...</p>
|
|
</div>
|
|
}
|
|
else if (_actor != null)
|
|
{
|
|
<div class="results-section">
|
|
<div class="actor-header">
|
|
<h3 class="actor-name">@_actor.Name</h3>
|
|
<div class="results-count">@_actor.TvCharacters.Count show@(_actor.TvCharacters.Count != 1 ? "s" : "") found</div>
|
|
</div>
|
|
|
|
@if (_matchedShow != null)
|
|
{
|
|
<div class="matched-show-section">
|
|
<h4 class="section-title">Your Search Match</h4>
|
|
<div class="show-card featured">
|
|
<div class="poster-container">
|
|
<img class="poster-image"
|
|
src="@($"https://image.tmdb.org/t/p/w500{_matchedShow.ShowPosterPath}")"
|
|
alt="@_matchedShow.ShowTitle Poster"
|
|
loading="lazy" />
|
|
<div class="match-badge">✨ Match</div>
|
|
<div class="poster-overlay">
|
|
<div class="overlay-content">
|
|
<span class="view-details">View Details</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-content">
|
|
<h5 class="show-title">@_matchedShow.ShowTitle</h5>
|
|
<div class="character-name">as @_matchedShow.CharacterName</div>
|
|
@if (!string.IsNullOrEmpty(_matchedShow.Overview?.Trim()))
|
|
{
|
|
<p class="show-overview expanded">
|
|
@_matchedShow.Overview.Trim()
|
|
</p>
|
|
}
|
|
<div class="air-date">First aired: @_matchedShow.FirstAirDate</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
<div class="filmography-section">
|
|
<h4 class="section-title">Complete TV Filmography</h4>
|
|
|
|
@foreach (var group in _actor.TvCharacters
|
|
.GroupBy(tc => tc.Franchise)
|
|
.OrderBy(g => g.Key == "Standalone")
|
|
.ThenBy(g => g.Key))
|
|
{
|
|
@if (group.Key != "Standalone")
|
|
{
|
|
<div class="franchise-group">
|
|
<h5 class="franchise-title">@group.Key Series</h5>
|
|
</div>
|
|
}
|
|
|
|
<div class="shows-grid">
|
|
@foreach (var show in (group.Key == "Standalone"
|
|
? group.OrderByDescending(tc => DateTime.TryParse(tc.FirstAirDate, out var d) ? d : DateTime.MinValue)
|
|
: group.OrderBy(tc => DateTime.TryParse(tc.FirstAirDate, out var d) ? d : DateTime.MaxValue)))
|
|
{
|
|
<div class="show-card @(show == _matchedShow ? "matched" : "")">
|
|
<div class="poster-container">
|
|
<img class="poster-image"
|
|
src="@($"https://image.tmdb.org/t/p/w500{show.ShowPosterPath}")"
|
|
alt="@show.ShowTitle Poster"
|
|
loading="lazy" />
|
|
<div class="poster-overlay">
|
|
<div class="overlay-content">
|
|
<span class="view-details">View Details</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-content">
|
|
<h5 class="show-title">@show.ShowTitle</h5>
|
|
<div class="character-name">as @show.CharacterName</div>
|
|
@if (!string.IsNullOrEmpty(show.Overview))
|
|
{
|
|
<div class="overview-container">
|
|
<p class="show-overview @(IsOverviewExpanded(show.ShowTitle) ? "expanded" : "truncated")">
|
|
@show.Overview
|
|
</p>
|
|
@if (show.Overview.Length > 150)
|
|
{
|
|
<button class="read-more-btn" @onclick="() => ToggleOverview(show.ShowTitle)">
|
|
@(IsOverviewExpanded(show.ShowTitle) ? "Show less" : "Read more")
|
|
</button>
|
|
}
|
|
</div>
|
|
}
|
|
<div class="air-date">@show.FirstAirDate</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
else if (!string.IsNullOrEmpty(_tvTitle) || !string.IsNullOrEmpty(_characterName))
|
|
{
|
|
<div class="no-results">
|
|
<div class="no-results-icon">🔍</div>
|
|
<h4>No matches found</h4>
|
|
<p>Try adjusting your search terms or check the spelling</p>
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="welcome-state">
|
|
<div class="welcome-icon">📺</div>
|
|
<h4>Ready to explore?</h4>
|
|
<p>Enter a TV show title and character name to discover an actor's complete television journey</p>
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
<style>
|
|
.search-container {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
padding: 2rem 1rem;
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
|
|
min-height: 100vh;
|
|
position: relative;
|
|
}
|
|
|
|
.search-container::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect x="10" y="20" width="15" height="10" fill="rgba(255,255,255,0.06)" rx="2"/><rect x="70" y="10" width="20" height="15" fill="rgba(255,255,255,0.04)" rx="2"/><rect x="30" y="70" width="12" height="8" fill="rgba(255,255,255,0.05)" rx="1"/></svg>') repeat;
|
|
opacity: 0.4;
|
|
pointer-events: none;
|
|
animation: float 25s ease-in-out infinite;
|
|
}
|
|
|
|
@@keyframes float {
|
|
0%, 100%
|
|
|
|
{
|
|
transform: translateY(0px) rotate(0deg);
|
|
}
|
|
|
|
50% {
|
|
transform: translateY(-30px) rotate(180deg);
|
|
}
|
|
|
|
}
|
|
|
|
.search-header {
|
|
text-align: center;
|
|
margin-bottom: 2.5rem;
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
.navigation-toggle {
|
|
display: flex;
|
|
justify-content: center;
|
|
margin-bottom: 1.5rem;
|
|
animation: slideInDown 0.8s ease-out 0.2s both;
|
|
}
|
|
|
|
.nav-btn {
|
|
background: rgba(255,255,255,0.15);
|
|
color: white;
|
|
text-decoration: none;
|
|
padding: 0.75rem 1.5rem;
|
|
border-radius: 2rem;
|
|
font-weight: 600;
|
|
font-size: 0.9rem;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
transition: all 0.3s ease;
|
|
backdrop-filter: blur(10px);
|
|
border: 1px solid rgba(255,255,255,0.2);
|
|
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.nav-btn:hover {
|
|
background: rgba(255,255,255,0.25);
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
|
|
color: white;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.nav-btn:active {
|
|
transform: translateY(0px);
|
|
}
|
|
|
|
.nav-icon {
|
|
font-size: 1.1rem;
|
|
transition: transform 0.3s ease;
|
|
}
|
|
|
|
.nav-btn:hover .nav-icon {
|
|
transform: scale(1.2);
|
|
}
|
|
|
|
.movies-btn {
|
|
background: linear-gradient(135deg, rgba(102, 126, 234, 0.3), rgba(118, 75, 162, 0.3));
|
|
}
|
|
|
|
.movies-btn:hover {
|
|
background: linear-gradient(135deg, rgba(102, 126, 234, 0.5), rgba(118, 75, 162, 0.5));
|
|
}
|
|
|
|
.search-title {
|
|
font-size: 2.5rem;
|
|
font-weight: 700;
|
|
color: white;
|
|
margin-bottom: 0.5rem;
|
|
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
|
|
animation: slideInDown 1s ease-out;
|
|
}
|
|
|
|
@@keyframes slideInDown {
|
|
from
|
|
|
|
{
|
|
opacity: 0;
|
|
transform: translateY(-50px);
|
|
}
|
|
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
|
|
}
|
|
|
|
.search-subtitle {
|
|
font-size: 1.1rem;
|
|
color: rgba(255,255,255,0.9);
|
|
margin: 0;
|
|
animation: fadeIn 1.5s ease-out 0.3s both;
|
|
}
|
|
|
|
@@keyframes fadeIn {
|
|
from
|
|
|
|
{
|
|
opacity: 0;
|
|
}
|
|
|
|
to {
|
|
opacity: 1;
|
|
}
|
|
|
|
}
|
|
|
|
.search-form {
|
|
background: rgba(255,255,255,0.95);
|
|
border-radius: 1.5rem;
|
|
padding: 2rem;
|
|
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
|
|
margin-bottom: 2rem;
|
|
backdrop-filter: blur(10px);
|
|
border: 1px solid rgba(255,255,255,0.2);
|
|
animation: slideInUp 1s ease-out 0.6s both;
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
@@keyframes slideInUp {
|
|
from
|
|
|
|
{
|
|
opacity: 0;
|
|
transform: translateY(30px);
|
|
}
|
|
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
|
|
}
|
|
|
|
.input-group {
|
|
display: flex;
|
|
gap: 1rem;
|
|
margin-bottom: 1.5rem;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.input-wrapper {
|
|
flex: 1;
|
|
min-width: 250px;
|
|
position: relative;
|
|
}
|
|
|
|
.search-input {
|
|
width: 100%;
|
|
padding: 1rem 1rem 1rem 3.5rem;
|
|
border: 2px solid #e1e5e9;
|
|
border-radius: 1rem;
|
|
font-size: 1rem;
|
|
transition: all 0.3s ease;
|
|
background: #fafbfc;
|
|
}
|
|
|
|
.search-input:focus {
|
|
outline: none;
|
|
border-color: #4f46e5;
|
|
background: white;
|
|
box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1), 0 8px 25px rgba(79, 70, 229, 0.15);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.input-icon {
|
|
position: absolute;
|
|
left: 1.2rem;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
font-size: 1.3rem;
|
|
transition: transform 0.3s ease;
|
|
}
|
|
|
|
.input-wrapper:focus-within .input-icon {
|
|
transform: translateY(-50%) scale(1.1);
|
|
}
|
|
|
|
.search-btn {
|
|
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
|
|
border: none;
|
|
color: white;
|
|
padding: 1rem 2.5rem;
|
|
border-radius: 1rem;
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
min-width: 160px;
|
|
justify-content: center;
|
|
box-shadow: 0 4px 15px rgba(79, 70, 229, 0.3);
|
|
}
|
|
|
|
.search-btn:hover:not(:disabled) {
|
|
transform: translateY(-3px);
|
|
box-shadow: 0 12px 35px rgba(79, 70, 229, 0.4);
|
|
}
|
|
|
|
.search-btn:active:not(:disabled) {
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.search-btn:disabled {
|
|
opacity: 0.7;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.spinner {
|
|
width: 18px;
|
|
height: 18px;
|
|
border: 2px solid rgba(255,255,255,0.3);
|
|
border-top: 2px solid white;
|
|
border-radius: 50%;
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
|
|
@@keyframes spin {
|
|
0%
|
|
|
|
{
|
|
transform: rotate(0deg);
|
|
}
|
|
|
|
100% {
|
|
transform: rotate(360deg);
|
|
}
|
|
|
|
}
|
|
|
|
.loading-state, .welcome-state, .no-results {
|
|
text-align: center;
|
|
padding: 3rem 2rem;
|
|
color: rgba(255,255,255,0.9);
|
|
animation: fadeIn 0.5s ease-out;
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
.loading-animation {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 0.5rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.loading-dot {
|
|
width: 12px;
|
|
height: 12px;
|
|
background: white;
|
|
border-radius: 50%;
|
|
animation: bounce 1.4s ease-in-out infinite both;
|
|
}
|
|
|
|
.loading-dot:nth-child(1) {
|
|
animation-delay: -0.32s;
|
|
}
|
|
|
|
.loading-dot:nth-child(2) {
|
|
animation-delay: -0.16s;
|
|
}
|
|
|
|
@@keyframes bounce {
|
|
0%, 80%, 100%
|
|
|
|
{
|
|
transform: scale(0);
|
|
}
|
|
|
|
40% {
|
|
transform: scale(1);
|
|
}
|
|
|
|
}
|
|
|
|
.welcome-icon, .no-results-icon {
|
|
font-size: 4rem;
|
|
margin-bottom: 1rem;
|
|
animation: pulse 2s ease-in-out infinite;
|
|
}
|
|
|
|
@@keyframes pulse {
|
|
0%, 100%
|
|
|
|
{
|
|
transform: scale(1);
|
|
}
|
|
|
|
50% {
|
|
transform: scale(1.1);
|
|
}
|
|
|
|
}
|
|
|
|
.results-section {
|
|
margin-top: 2rem;
|
|
animation: slideInUp 0.8s ease-out;
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
.actor-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 2rem;
|
|
flex-wrap: wrap;
|
|
gap: 1rem;
|
|
background: rgba(255,255,255,0.1);
|
|
padding: 1.5rem 2rem;
|
|
border-radius: 1rem;
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
|
|
.actor-name {
|
|
font-size: 2rem;
|
|
font-weight: 700;
|
|
color: white;
|
|
margin: 0;
|
|
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
|
|
}
|
|
|
|
.results-count {
|
|
background: rgba(255,255,255,0.2);
|
|
color: white;
|
|
padding: 0.5rem 1.5rem;
|
|
border-radius: 2rem;
|
|
font-weight: 500;
|
|
font-size: 0.9rem;
|
|
backdrop-filter: blur(10px);
|
|
border: 1px solid rgba(255,255,255,0.1);
|
|
}
|
|
|
|
.matched-show-section {
|
|
margin-bottom: 3rem;
|
|
}
|
|
|
|
.section-title {
|
|
font-size: 1.5rem;
|
|
font-weight: 600;
|
|
color: white;
|
|
margin-bottom: 1.5rem;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
text-shadow: 1px 1px 2px rgba(0,0,0,0.3);
|
|
}
|
|
|
|
.franchise-group {
|
|
margin: 2rem 0 1rem 0;
|
|
}
|
|
|
|
.franchise-title {
|
|
font-size: 1.3rem;
|
|
font-weight: 600;
|
|
color: white;
|
|
margin: 0;
|
|
padding: 1rem 1.5rem;
|
|
background: rgba(255,255,255,0.1);
|
|
border-radius: 0.5rem;
|
|
border-left: 4px solid rgba(255,255,255,0.5);
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
|
|
.shows-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|
gap: 1.5rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.show-card {
|
|
background: white;
|
|
border-radius: 1.2rem;
|
|
overflow: hidden;
|
|
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
|
|
transition: all 0.4s ease;
|
|
border: 1px solid rgba(255,255,255,0.2);
|
|
position: relative;
|
|
transform-style: preserve-3d;
|
|
}
|
|
|
|
.show-card:hover {
|
|
transform: translateY(-8px) scale(1.02);
|
|
box-shadow: 0 20px 40px rgba(0,0,0,0.25);
|
|
}
|
|
|
|
.show-card.featured {
|
|
border: 2px solid rgba(255,255,255,0.5);
|
|
box-shadow: 0 12px 35px rgba(79, 70, 229, 0.25);
|
|
}
|
|
|
|
.show-card.matched {
|
|
border: 2px solid #10b981;
|
|
}
|
|
|
|
.poster-container {
|
|
position: relative;
|
|
width: 100%;
|
|
aspect-ratio: 2/3;
|
|
overflow: hidden;
|
|
background: linear-gradient(45deg, #f0f0f0, #e0e0e0);
|
|
}
|
|
|
|
.poster-image {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: contain;
|
|
background-color: #f8f9fa;
|
|
transition: all 0.4s ease;
|
|
}
|
|
|
|
.poster-overlay {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: linear-gradient(to bottom, transparent 0%, rgba(0,0,0,0.8) 100%);
|
|
opacity: 0;
|
|
transition: all 0.3s ease;
|
|
display: flex;
|
|
align-items: flex-end;
|
|
padding: 1rem;
|
|
}
|
|
|
|
.show-card:hover .poster-overlay {
|
|
opacity: 1;
|
|
}
|
|
|
|
.show-card:hover .poster-image {
|
|
transform: scale(1.05);
|
|
}
|
|
|
|
.overlay-content {
|
|
color: white;
|
|
font-weight: 600;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.match-badge {
|
|
position: absolute;
|
|
top: 0.8rem;
|
|
right: 0.8rem;
|
|
background: linear-gradient(135deg, #10b981, #059669);
|
|
color: white;
|
|
padding: 0.4rem 1rem;
|
|
border-radius: 1.5rem;
|
|
font-size: 0.8rem;
|
|
font-weight: 600;
|
|
box-shadow: 0 4px 15px rgba(16, 185, 129, 0.4);
|
|
z-index: 2;
|
|
}
|
|
|
|
.card-content {
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.show-title {
|
|
font-size: 1.2rem;
|
|
font-weight: 600;
|
|
color: #1a1a1a;
|
|
margin: 0 0 0.5rem 0;
|
|
line-height: 1.3;
|
|
}
|
|
|
|
.character-name {
|
|
color: #4f46e5;
|
|
font-weight: 500;
|
|
margin-bottom: 0.75rem;
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
.show-overview {
|
|
color: #666;
|
|
font-size: 0.9rem;
|
|
line-height: 1.4;
|
|
margin: 0.75rem 0;
|
|
}
|
|
|
|
.show-overview.truncated {
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 3;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.show-overview.expanded {
|
|
display: block;
|
|
}
|
|
|
|
.read-more-btn {
|
|
background: none;
|
|
border: none;
|
|
color: #4f46e5;
|
|
font-size: 0.85rem;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
padding: 0.25rem 0;
|
|
margin-top: 0.5rem;
|
|
text-decoration: underline;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.read-more-btn:hover {
|
|
color: #3730a3;
|
|
transform: translateX(5px);
|
|
}
|
|
|
|
.air-date {
|
|
color: #999;
|
|
font-size: 0.85rem;
|
|
font-weight: 500;
|
|
}
|
|
|
|
@@media (max-width: 768px) {
|
|
.search-container
|
|
|
|
{
|
|
padding: 1rem;
|
|
}
|
|
|
|
.search-title {
|
|
font-size: 2rem;
|
|
}
|
|
|
|
.input-group {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.input-wrapper {
|
|
min-width: auto;
|
|
}
|
|
|
|
.actor-header {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
}
|
|
|
|
.shows-grid {
|
|
grid-template-columns: 1fr;
|
|
gap: 1rem;
|
|
}
|
|
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
window.startAnimations = () => {
|
|
console.log('Starting TV show page animations...');
|
|
|
|
// Enhanced spinner animation with easing
|
|
const spinners = document.querySelectorAll('.spinner');
|
|
spinners.forEach((spinner, index) => {
|
|
let rotation = 0;
|
|
const animate = () => {
|
|
rotation += 6;
|
|
spinner.style.transform = `rotate(${rotation}deg)`;
|
|
if (document.body.contains(spinner)) {
|
|
requestAnimationFrame(animate);
|
|
}
|
|
};
|
|
animate();
|
|
});
|
|
|
|
// Enhanced loading dots with wave effect
|
|
const loadingDots = document.querySelectorAll('.loading-dot');
|
|
loadingDots.forEach((dot, index) => {
|
|
setTimeout(() => {
|
|
dot.style.animationDelay = `${index * 0.16}s`;
|
|
}, 50);
|
|
});
|
|
|
|
// Add stagger animation to show cards
|
|
const showCards = document.querySelectorAll('.show-card');
|
|
showCards.forEach((card, index) => {
|
|
card.style.opacity = '0';
|
|
card.style.transform = 'translateY(30px)';
|
|
setTimeout(() => {
|
|
card.style.transition = 'all 0.6s ease';
|
|
card.style.opacity = '1';
|
|
card.style.transform = 'translateY(0)';
|
|
}, index * 100 + 200);
|
|
});
|
|
|
|
// Add floating animation to icons
|
|
const icons = document.querySelectorAll('.input-icon');
|
|
icons.forEach((icon, index) => {
|
|
setTimeout(() => {
|
|
icon.style.animation = 'iconFloat 3s ease-in-out infinite';
|
|
icon.style.animationDelay = `${index * 0.5}s`;
|
|
}, 1000);
|
|
});
|
|
};
|
|
|
|
// Add parallax effect
|
|
window.addEventListener('scroll', () => {
|
|
const scrolled = window.pageYOffset;
|
|
const parallaxElements = document.querySelectorAll('.search-container::before');
|
|
parallaxElements.forEach(element => {
|
|
element.style.transform = `translateY(${scrolled * 0.5}px)`;
|
|
});
|
|
});
|
|
|
|
// Additional CSS for icon floating animation
|
|
const style = document.createElement('style');
|
|
style.textContent = `
|
|
@@keyframes iconFloat {
|
|
0%, 100% { transform: translateY(-50%) translateX(0px); }
|
|
50% { transform: translateY(-50%) translateX(2px); }
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|
|
</script>
|
|
|
|
@code {
|
|
private string _tvTitle = "";
|
|
private string _characterName = "";
|
|
private Actor _actor;
|
|
private TvCharacter _matchedShow;
|
|
private bool _isSearching = false;
|
|
private HashSet<string> _expandedOverviews = new HashSet<string>();
|
|
|
|
private bool IsOverviewExpanded(string showTitle)
|
|
{
|
|
return _expandedOverviews.Contains(showTitle);
|
|
}
|
|
|
|
private void ToggleOverview(string showTitle)
|
|
{
|
|
if (_expandedOverviews.Contains(showTitle))
|
|
{
|
|
_expandedOverviews.Remove(showTitle);
|
|
}
|
|
else
|
|
{
|
|
_expandedOverviews.Add(showTitle);
|
|
}
|
|
}
|
|
|
|
private async Task SearchForActorAndCharacters()
|
|
{
|
|
if (string.IsNullOrWhiteSpace(_tvTitle) || string.IsNullOrWhiteSpace(_characterName))
|
|
return;
|
|
|
|
_isSearching = true;
|
|
_actor = null;
|
|
_matchedShow = null;
|
|
StateHasChanged();
|
|
|
|
await Task.Delay(100);
|
|
await JSRuntime.InvokeVoidAsync("startAnimations");
|
|
|
|
try
|
|
{
|
|
var shows = await ShowService.GetShowsByTitle(_tvTitle);
|
|
|
|
foreach (var show in shows)
|
|
{
|
|
var credits = await ShowService.GetShowCreditsById(show.Id);
|
|
|
|
foreach (var cast in credits.Cast)
|
|
{
|
|
if (cast.Character.Contains(_characterName, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
if (_actor == null)
|
|
{
|
|
_actor = new Actor(cast.Name);
|
|
await LoadActorFilmography(cast.Id, (object)show, (object)cast);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (_actor != null) break;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"Search error: {ex.Message}");
|
|
}
|
|
finally
|
|
{
|
|
_isSearching = false;
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
|
|
private async Task LoadActorFilmography(int personId, object originalShow, object originalCast)
|
|
{
|
|
var filmography = await ShowService.GetActorTvCreditsById(personId);
|
|
|
|
if (filmography?.Cast != null)
|
|
{
|
|
foreach (var role in filmography.Cast)
|
|
{
|
|
var fullShow = await ShowService.GetShowDetailsById(role.Id);
|
|
|
|
var tvCharacter = new TvCharacter
|
|
{
|
|
ShowTitle = role.ShowTitle ?? "Unknown Title",
|
|
CharacterName = role.Character ?? "Unknown Character",
|
|
ShowPosterPath = role.PosterPath ?? "",
|
|
FirstAirDate = DateTime.TryParse(fullShow?.FirstAirDate, out var parsedDate)
|
|
? parsedDate.ToString("MMM dd, yyyy")
|
|
: "Unknown Date",
|
|
Overview = fullShow?.Overview ?? "",
|
|
Franchise = fullShow?.BelongsToCollection?.Name ?? "Standalone"
|
|
};
|
|
|
|
_actor.TvCharacters.Add(tvCharacter);
|
|
|
|
if (_matchedShow == null &&
|
|
string.Equals(role.ShowTitle, GetPropertyValue(originalShow, "Name")?.ToString(), StringComparison.OrdinalIgnoreCase) &&
|
|
string.Equals(role.Character, GetPropertyValue(originalCast, "Character")?.ToString(), StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
_matchedShow = tvCharacter;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private object GetPropertyValue(object obj, string propertyName)
|
|
{
|
|
if (obj == null) return null;
|
|
var property = obj.GetType().GetProperty(propertyName);
|
|
return property?.GetValue(obj);
|
|
}
|
|
} |