Files
mmcghen 26a7b04e82 Improve layout and visual styles for search pages
Enhanced the MainLayout and search pages with improved CSS for smoother scrolling, better overflow handling, and more visually appealing backgrounds. Updated the SearchShows match result section for a cleaner, more modern look. Refined keyframe animations and mobile responsiveness for a more polished user experience.
2025-08-26 09:29:12 -04:00

1041 lines
29 KiB
Plaintext

@page "/searchmovies"
@using MovieActorLookup.Models
@using MovieActorLookup.Models.Movies
@using System.ComponentModel.DataAnnotations
@inject MovieService MovieService
@inject IJSRuntime JSRuntime
@rendermode InteractiveServer
<div class="search-container">
<div class="search-header">
<div class="navigation-toggle">
<a href="/searchshows" class="nav-btn shows-btn">
<i class="bi bi-tv"></i>
<span class="nav-text">Switch to TV Shows</span>
</a>
</div>
<h2 class="search-title">Discover Actors & Their Movies</h2>
<p class="search-subtitle">Search by movie 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="_movieTitle"
placeholder="Enter movie title..."
@onkeypress="@(async (e) => { if (e.Key == "Enter") await SearchForActorAndCharacters(); })" />
<div class="input-icon bi bi-film"></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 bi bi-person-vcard"></div>
</div>
</div>
<button class="search-btn @(_isSearching ? "searching" : "")"
@onclick="SearchForActorAndCharacters"
disabled="@_isSearching">
@if (_isSearching)
{
<span class="spinner"></span>
<span>Searching...</span>
}
else
{
<span class="bi bi-search"> 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.MovieCharacters.Count movie@(_actor.MovieCharacters.Count != 1 ? "s" : "") found</div>
</div>
@if (_matchedMovie != null)
{
<div class="matched-movie-section">
<h4 class="section-title">
<span class="match-indicator"></span>
Your Search Result
</h4>
<div class="search-match-banner">
<div class="match-poster">
<img src="@($"https://image.tmdb.org/t/p/w200{_matchedMovie.MoviePosterPath}")"
alt="@_matchedMovie.MovieTitle Poster"
loading="lazy" />
</div>
<div class="match-info">
<h5 class="match-title">@_matchedMovie.MovieTitle</h5>
<div class="match-character">as @_matchedMovie.CharacterName</div>
<div class="match-date">Released: @_matchedMovie.ReleaseDate</div>
</div>
</div>
</div>
}
<div class="filmography-section">
<h4 class="section-title">Complete Movie Filmography</h4>
@foreach (var group in _actor.MovieCharacters
.GroupBy(mc => mc.Franchise)
.OrderBy(g => g.Key == "Standalone")
.ThenBy(g => g.Key))
{
@if (group.Key != "Standalone")
{
<div class="franchise-group">
<h5 class="franchise-title">@group.Key Collection</h5>
</div>
}
<div class="movies-grid">
@foreach (var movie in (group.Key == "Standalone"
? group.OrderByDescending(mc => DateTime.TryParse(mc.ReleaseDate, out var d) ? d : DateTime.MinValue)
: group.OrderBy(mc => DateTime.TryParse(mc.ReleaseDate, out var d) ? d : DateTime.MaxValue)))
{
<div class="movie-card @(movie == _matchedMovie ? "matched" : "")">
<div class="poster-container">
<img class="poster-image"
src="@($"https://image.tmdb.org/t/p/w500{movie.MoviePosterPath}")"
alt="@movie.MovieTitle 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="movie-title">@movie.MovieTitle</h5>
<div class="character-name">as @movie.CharacterName</div>
@if (!string.IsNullOrEmpty(movie.Overview))
{
<div class="overview-container">
<p class="movie-overview @(IsOverviewExpanded(movie.MovieTitle) ? "expanded" : "truncated")">
@movie.Overview
</p>
@if (movie.Overview.Length > 150)
{
<button class="read-more-btn" @onclick="() => ToggleOverview(movie.MovieTitle)">
@(IsOverviewExpanded(movie.MovieTitle) ? "Show less" : "Read more")
</button>
}
</div>
}
<div class="release-date">@movie.ReleaseDate</div>
</div>
</div>
}
</div>
}
</div>
</div>
}
else if (!string.IsNullOrEmpty(_movieTitle) || !string.IsNullOrEmpty(_characterName))
{
<div class="no-results">
<div class="no-results-icon bi bi-search"></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 bi bi-film"></div>
<h4>Ready to explore?</h4>
<p>Enter a movie title and character name to discover an actor's complete film journey</p>
</div>
}
</div>
<style>
body {
margin: 0;
padding: 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
position: relative;
overflow-x: hidden;
}
body::before {
content: '';
position: fixed;
top: -100px;
left: -100px;
right: -100px;
bottom: -100px;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="50" cy="50" r="2" fill="rgba(255,255,255,0.1)"/><circle cx="20" cy="20" r="1" fill="rgba(255,255,255,0.08)"/><circle cx="80" cy="30" r="1.5" fill="rgba(255,255,255,0.06)"/></svg>') repeat;
opacity: 0.3;
pointer-events: none;
animation: floatContained 20s ease-in-out infinite;
z-index: -1;
will-change: transform;
backface-visibility: hidden;
transform-style: preserve-3d;
}
.search-container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem 1rem;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
position: relative;
z-index: 1;
overflow: hidden;
}
.search-container::before {
content: '';
position: absolute;
top: -100px;
left: -100px;
right: -100px;
bottom: -100px;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="50" cy="50" r="2" fill="rgba(255,255,255,0.1)"/><circle cx="20" cy="20" r="1" fill="rgba(255,255,255,0.08)"/><circle cx="80" cy="30" r="1.5" fill="rgba(255,255,255,0.06)"/></svg>') repeat;
opacity: 0.3;
pointer-events: none;
animation: floatContained 20s ease-in-out infinite;
z-index: -1;
overflow: hidden;
will-change: transform;
backface-visibility: hidden;
transform-style: preserve-3d;
}
@@keyframes floatContained {
0%, 100%
{
transform: translateY(0px) rotate(0deg) scale(1);
}
25% {
transform: translateY(-5px) rotate(45deg) scale(1.02);
}
50% {
transform: translateY(-8px) rotate(90deg) scale(0.98);
}
75% {
transform: translateY(-5px) rotate(135deg) scale(1.02);
}
}
html {
overflow-x: hidden;
scroll-behavior: smooth;
}
.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);
}
.shows-btn {
background: linear-gradient(135deg, rgba(79, 70, 229, 0.3), rgba(124, 58, 237, 0.3));
}
.shows-btn:hover {
background: linear-gradient(135deg, rgba(79, 70, 229, 0.5), rgba(124, 58, 237, 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: #667eea;
background: white;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1), 0 8px 25px rgba(102, 126, 234, 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, #667eea 0%, #764ba2 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(102, 126, 234, 0.3);
}
.search-btn:hover:not(:disabled) {
transform: translateY(-3px);
box-shadow: 0 12px 35px rgba(102, 126, 234, 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-movie-section {
margin-bottom: 2rem;
}
.search-match-banner {
background: rgba(255,255,255,0.15);
border-radius: 1rem;
padding: 1.5rem;
display: flex;
align-items: center;
gap: 1.5rem;
backdrop-filter: blur(10px);
border: 1px solid rgba(255,255,255,0.2);
box-shadow: 0 8px 25px rgba(0,0,0,0.1);
}
.match-poster {
flex-shrink: 0;
}
.match-poster img {
width: 80px;
height: 120px;
object-fit: cover;
border-radius: 0.5rem;
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
}
.match-info {
flex: 1;
color: white;
}
.match-title {
font-size: 1.25rem;
font-weight: 600;
margin: 0 0 0.5rem 0;
color: white;
}
.match-character {
font-size: 0.95rem;
color: rgba(255,255,255,0.8);
margin-bottom: 0.25rem;
}
.match-date {
font-size: 0.85rem;
color: rgba(255,255,255,0.6);
}
.match-indicator {
font-family: 'Courier New', monospace;
background: rgba(16, 185, 129, 0.8);
color: white;
padding: 0.25rem 0.75rem;
border-radius: 0.5rem;
font-size: 0.8rem;
font-weight: 600;
margin-right: 0.5rem;
}
.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);
}
.movies-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.movie-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;
}
.movie-card:hover {
transform: translateY(-8px) scale(1.02);
box-shadow: 0 20px 40px rgba(0,0,0,0.25);
}
.movie-card.featured {
border: 2px solid rgba(255,255,255,0.5);
box-shadow: 0 12px 35px rgba(102, 126, 234, 0.25);
}
.movie-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: cover;
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;
}
.movie-card:hover .poster-overlay {
opacity: 1;
}
.movie-card:hover .poster-image {
transform: scale(1.1);
}
.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;
}
.movie-title {
font-size: 1.2rem;
font-weight: 600;
color: #1a1a1a;
margin: 0 0 0.5rem 0;
line-height: 1.3;
}
.character-name {
color: #667eea;
font-weight: 500;
margin-bottom: 0.75rem;
font-size: 0.95rem;
}
.movie-overview {
color: #666;
font-size: 0.9rem;
line-height: 1.4;
margin: 0.75rem 0;
}
.movie-overview.truncated {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.movie-overview.expanded {
display: block;
}
.read-more-btn {
background: none;
border: none;
color: #667eea;
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: #4c63d2;
transform: translateX(5px);
}
.release-date {
color: #999;
font-size: 0.85rem;
font-weight: 500;
}
/* Mobile optimization */
@@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;
}
.movies-grid {
grid-template-columns: 1fr;
gap: 1rem;
}
body::before,
.search-container::before {
animation: floatContained 25s ease-in-out infinite;
}
@@keyframes floatContained {
0%, 100%
{
transform: translateY(0px) rotate(0deg) scale(1);
}
25% {
transform: translateY(-3px) rotate(45deg) scale(1.01);
}
50% {
transform: translateY(-5px) rotate(90deg) scale(0.99);
}
75% {
transform: translateY(-3px) rotate(135deg) scale(1.01);
}
}
}
</style>
<script>
window.startAnimations = () => {
console.log('Starting movie page animations...');
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();
});
const loadingDots = document.querySelectorAll('.loading-dot');
loadingDots.forEach((dot, index) => {
setTimeout(() => {
dot.style.animationDelay = `${index * 0.16}s`;
}, 50);
});
const movieCards = document.querySelectorAll('.movie-card');
movieCards.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);
});
};
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)`;
});
});
</script>
@code {
private string _movieTitle = "";
private string _characterName = "";
private Actor _actor;
private MovieCharacter _matchedMovie;
private bool _isSearching = false;
private HashSet<string> _expandedOverviews = new HashSet<string>();
private bool IsOverviewExpanded(string movieTitle)
{
return _expandedOverviews.Contains(movieTitle);
}
private void ToggleOverview(string movieTitle)
{
if (_expandedOverviews.Contains(movieTitle))
{
_expandedOverviews.Remove(movieTitle);
}
else
{
_expandedOverviews.Add(movieTitle);
}
}
private async Task SearchForActorAndCharacters()
{
if (string.IsNullOrWhiteSpace(_movieTitle) || string.IsNullOrWhiteSpace(_characterName))
return;
_isSearching = true;
_actor = null;
_matchedMovie = null;
StateHasChanged();
await Task.Delay(100);
await JSRuntime.InvokeVoidAsync("startAnimations");
try
{
var movies = await MovieService.GetMoviesByTitle(_movieTitle);
foreach (var movie in movies)
{
var credits = await MovieService.GetMovieCreditsByMovieId(movie.Id);
foreach (var castMember in credits.Cast)
{
if (castMember.Character.Contains(_characterName, StringComparison.OrdinalIgnoreCase))
{
if (_actor == null)
{
_actor = new Actor(castMember.Name);
await LoadActorFilmography(castMember.Id, movie, castMember);
}
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 originalMovie, object originalCast)
{
var filmography = await MovieService.GetActorMovieCreditsById(personId);
if (filmography?.Cast != null)
{
foreach (var role in filmography.Cast)
{
var fullMovie = await MovieService.GetMovieDetailsById(role.Id);
var movieCharacter = new MovieCharacter
{
MovieTitle = role.Title ?? "Unknown Title",
CharacterName = role.Character ?? "Unknown Character",
MoviePosterPath = role.PosterPath ?? "",
ReleaseDate = DateTime.TryParse(fullMovie?.ReleaseDate, out var parsedDate)
? parsedDate.ToString("MMM dd, yyyy")
: "Unknown Date",
Overview = fullMovie?.Overview ?? "",
Franchise = fullMovie?.BelongsToCollection?.Name ?? "Standalone"
};
_actor.MovieCharacters.Add(movieCharacter);
if (_matchedMovie == null &&
string.Equals(role.Title, GetPropertyValue(originalMovie, "Title")?.ToString(), StringComparison.OrdinalIgnoreCase) &&
string.Equals(role.Character, GetPropertyValue(originalCast, "Character")?.ToString(), StringComparison.OrdinalIgnoreCase))
{
_matchedMovie = movieCharacter;
}
}
}
}
private object GetPropertyValue(object obj, string propertyName)
{
if (obj == null) return null;
var property = obj.GetType().GetProperty(propertyName);
return property?.GetValue(obj);
}
}