improvements on the games list page, better design and grid/card/list views
This commit is contained in:
@@ -40,7 +40,7 @@ class BuildGameLibrary extends Command
|
|||||||
$startTimestamp = strtotime("{$year}-01-01");
|
$startTimestamp = strtotime("{$year}-01-01");
|
||||||
$endTimestamp = strtotime("{$year}-12-31");
|
$endTimestamp = strtotime("{$year}-12-31");
|
||||||
|
|
||||||
$games = $igdbService->get($limit);
|
$games = $igdbService->get($limit, $startTimestamp, $endTimestamp);
|
||||||
|
|
||||||
if (empty($games)) {
|
if (empty($games)) {
|
||||||
$this->warn("No games found for this batch. Moving to next batch...");
|
$this->warn("No games found for this batch. Moving to next batch...");
|
||||||
|
|||||||
@@ -17,8 +17,11 @@ use MarcReichel\IGDBLaravel\Models\Platform as IgdbPlatform;
|
|||||||
|
|
||||||
class IgdbService
|
class IgdbService
|
||||||
{
|
{
|
||||||
public function get(int $limit)
|
public function get(
|
||||||
{
|
int $limit,
|
||||||
|
$startDate = null,
|
||||||
|
$endDate = null,
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
$existingIds = $this->getExistingIds();
|
$existingIds = $this->getExistingIds();
|
||||||
|
|
||||||
@@ -38,6 +41,14 @@ class IgdbService
|
|||||||
// ->where('category', '=', 0)
|
// ->where('category', '=', 0)
|
||||||
;
|
;
|
||||||
|
|
||||||
|
if ($startDate) {
|
||||||
|
$queryBuilder->where('first_release_date', '>=', $startDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($endDate) {
|
||||||
|
$queryBuilder->where('first_release_date', '<=', $endDate);
|
||||||
|
}
|
||||||
|
|
||||||
if (!empty($existingIds)) {
|
if (!empty($existingIds)) {
|
||||||
// Convert array of quoted strings to array of integers
|
// Convert array of quoted strings to array of integers
|
||||||
// $existingIds = array_map(function($id) {
|
// $existingIds = array_map(function($id) {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class DatabaseSeeder extends Seeder
|
|||||||
User::factory()->create([
|
User::factory()->create([
|
||||||
'name' => 'Test User',
|
'name' => 'Test User',
|
||||||
'email' => 'test@example.com',
|
'email' => 'test@example.com',
|
||||||
|
'password' => bcrypt('password'),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -244,3 +244,127 @@ body {
|
|||||||
color: #fff; background: rgba(0, 0, 0, 0.7);
|
color: #fff; background: rgba(0, 0, 0, 0.7);
|
||||||
display: none; justify-content: center; align-items: center;
|
display: none; justify-content: center; align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Game Card Styles */
|
||||||
|
.game-card-grid {
|
||||||
|
transition: transform 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-card-grid:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-cover-wrapper {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-cover {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
aspect-ratio: 3/4;
|
||||||
|
object-fit: cover;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-card-grid:hover .game-cover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-cover-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: linear-gradient(to top, rgba(0, 0, 0, 0.9) 0%, transparent 50%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-card-grid:hover .game-cover-overlay {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-content {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rating-badge {
|
||||||
|
background: linear-gradient(90deg, var(--primary), var(--accent));
|
||||||
|
color: white;
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-info-grid .game-title {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card View Styles */
|
||||||
|
.game-card-enhanced {
|
||||||
|
background-color: rgba(30, 30, 30, 0.8);
|
||||||
|
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-card-enhanced:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 8px 20px rgba(127, 0, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-card-enhanced .card-body {
|
||||||
|
background-color: rgba(18, 18, 18, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* List View Styles */
|
||||||
|
.game-card-list {
|
||||||
|
background-color: rgba(30, 30, 30, 0.8);
|
||||||
|
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-card-list:hover {
|
||||||
|
transform: translateX(5px);
|
||||||
|
box-shadow: 0 4px 15px rgba(127, 0, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-card-list .card-body {
|
||||||
|
background-color: rgba(18, 18, 18, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* View Mode Button Styles */
|
||||||
|
.view-mode-btn {
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-mode-btn.active {
|
||||||
|
background: linear-gradient(90deg, var(--primary), var(--accent));
|
||||||
|
border-color: transparent;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-mode-btn:not(.active):hover {
|
||||||
|
background-color: rgba(127, 0, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.game-info-grid .game-title {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rating-badge {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
padding: 0.3rem 0.6rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,6 +17,17 @@
|
|||||||
<i class="fas fa-search"></i>
|
<i class="fas fa-search"></i>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
<div class="btn-group me-2" role="group" aria-label="View mode">
|
||||||
|
<button type="button" class="btn btn-outline-gradient view-mode-btn active" data-view="grid" title="Grid View">
|
||||||
|
<i class="fas fa-th"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-gradient view-mode-btn" data-view="cards" title="Card View">
|
||||||
|
<i class="fas fa-th-large"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-gradient view-mode-btn" data-view="list" title="List View">
|
||||||
|
<i class="fas fa-list"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
<button class="btn btn-outline-gradient dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
<button class="btn btn-outline-gradient dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
||||||
<i class="fas fa-sort me-1"></i> Sort by
|
<i class="fas fa-sort me-1"></i> Sort by
|
||||||
@@ -136,48 +147,149 @@
|
|||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<div class="row g-4">
|
<!-- Grid View -->
|
||||||
|
<div id="grid-view" class="row g-4 view-container">
|
||||||
@foreach($games as $game)
|
@foreach($games as $game)
|
||||||
<div class="col-md-2">
|
<div class="col-6 col-sm-4 col-md-3 col-lg-2">
|
||||||
<div class="cover-container">
|
<div class="game-card-grid">
|
||||||
<a href="{{ route('games.show', $game->slug) }}">
|
<a href="{{ route('games.show', $game->slug) }}" class="text-decoration-none">
|
||||||
<img src="{{ $game->cover_url ?? 'https://via.placeholder.com/300x400' }}"
|
<div class="game-cover-wrapper">
|
||||||
class="card-img-top"
|
<img src="{{ $game->cover_url ?? 'https://via.placeholder.com/264x352?text=No+Image' }}"
|
||||||
alt="{{ $game->name }}"
|
class="game-cover"
|
||||||
style="height: 300px; object-fit: cover;">
|
alt="{{ $game->name }}">
|
||||||
<div class="game-info-cover">
|
<div class="game-cover-overlay">
|
||||||
text here
|
<div class="overlay-content">
|
||||||
|
@if($game->rating)
|
||||||
|
<div class="rating-badge">
|
||||||
|
<i class="fas fa-star"></i> {{ number_format($game->rating, 1) }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="game-info-grid mt-2">
|
||||||
|
<h6 class="game-title text-white mb-1">{{ Str::limit($game->name, 30) }}</h6>
|
||||||
|
@if($game->first_release_date)
|
||||||
|
<small class="text-muted">
|
||||||
|
<i class="fas fa-calendar-alt me-1"></i>{{ \Carbon\Carbon::parse($game->first_release_date)->format('Y') }}
|
||||||
|
</small>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{-- <div class="col">--}}
|
@endforeach
|
||||||
{{-- <div class="card gradient-border h-100">--}}
|
</div>
|
||||||
{{-- <img src="{{ $game->cover_url ?? 'https://via.placeholder.com/300x400' }}"--}}
|
|
||||||
{{-- class="card-img-top"--}}
|
<!-- Card View -->
|
||||||
{{-- alt="{{ $game->name }}"--}}
|
<div id="card-view" class="row g-4 view-container" style="display: none;">
|
||||||
{{-- style="height: 300px; object-fit: cover;">--}}
|
@foreach($games as $game)
|
||||||
{{-- <div class="card-body">--}}
|
<div class="col-12 col-sm-6 col-md-4 col-lg-3">
|
||||||
{{-- <h5 class="card-title">{{ $game->name }}</h5>--}}
|
<div class="card game-card-enhanced gradient-border h-100">
|
||||||
{{-- <p class="card-text text-gray-400 small">--}}
|
<a href="{{ route('games.show', $game->slug) }}" class="text-decoration-none">
|
||||||
{{-- {{ Str::limit($game->summary, 100) }}--}}
|
<div class="position-relative">
|
||||||
{{-- </p>--}}
|
<img src="{{ $game->cover_url ?? 'https://via.placeholder.com/300x400?text=No+Image' }}"
|
||||||
{{-- <div class="d-flex gap-2 mb-3">--}}
|
class="card-img-top"
|
||||||
{{-- @if($game->rating)--}}
|
alt="{{ $game->name }}"
|
||||||
{{-- <span class="badge bg-primary"><i class="fas fa-star me-1"></i> {{ number_format($game->rating, 1) }}</span>--}}
|
style="height: 350px; object-fit: cover;">
|
||||||
{{-- @endif--}}
|
@if($game->rating)
|
||||||
{{-- @if($game->first_release_date)--}}
|
<div class="position-absolute top-0 end-0 m-2">
|
||||||
{{-- <span class="badge bg-secondary">--}}
|
<span class="badge bg-primary" style="font-size: 1rem; padding: 0.5rem 0.75rem;">
|
||||||
{{-- <i class="fas fa-calendar-alt me-1"></i> {{ \Carbon\Carbon::parse($game->first_release_date)->format('Y') }}--}}
|
<i class="fas fa-star me-1"></i>{{ number_format($game->rating, 1) }}
|
||||||
{{-- </span>--}}
|
</span>
|
||||||
{{-- @endif--}}
|
</div>
|
||||||
{{-- </div>--}}
|
@endif
|
||||||
{{-- <a href="{{ route('games.show', $game->slug) }}" class="btn btn-gradient w-100">--}}
|
</div>
|
||||||
{{-- <i class="fas fa-info-circle me-1"></i> View Details--}}
|
</a>
|
||||||
{{-- </a>--}}
|
<div class="card-body d-flex flex-column">
|
||||||
{{-- </div>--}}
|
<h5 class="card-title text-white mb-2">
|
||||||
{{-- </div>--}}
|
<a href="{{ route('games.show', $game->slug) }}" class="text-white text-decoration-none">
|
||||||
{{-- </div>--}}
|
{{ $game->name }}
|
||||||
|
</a>
|
||||||
|
</h5>
|
||||||
|
@if($game->summary)
|
||||||
|
<p class="card-text text-muted small flex-grow-1">
|
||||||
|
{{ Str::limit($game->summary, 120) }}
|
||||||
|
</p>
|
||||||
|
@endif
|
||||||
|
<div class="d-flex flex-wrap gap-2 mb-3">
|
||||||
|
@if($game->first_release_date)
|
||||||
|
<span class="badge bg-secondary">
|
||||||
|
<i class="fas fa-calendar-alt me-1"></i>{{ \Carbon\Carbon::parse($game->first_release_date)->format('M Y') }}
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
@if($game->rating_count)
|
||||||
|
<span class="badge bg-secondary">
|
||||||
|
<i class="fas fa-users me-1"></i>{{ number_format($game->rating_count) }} ratings
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<a href="{{ route('games.show', $game->slug) }}" class="btn btn-gradient w-100 mt-auto">
|
||||||
|
<i class="fas fa-info-circle me-1"></i> View Details
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- List View -->
|
||||||
|
<div id="list-view" class="view-container" style="display: none;">
|
||||||
|
@foreach($games as $game)
|
||||||
|
<div class="card game-card-list gradient-border mb-3">
|
||||||
|
<div class="row g-0">
|
||||||
|
<div class="col-md-2 col-lg-1">
|
||||||
|
<a href="{{ route('games.show', $game->slug) }}">
|
||||||
|
<img src="{{ $game->cover_url ?? 'https://via.placeholder.com/90x120?text=No+Image' }}"
|
||||||
|
class="img-fluid rounded-start"
|
||||||
|
alt="{{ $game->name }}"
|
||||||
|
style="height: 100%; min-height: 120px; object-fit: cover;">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-10 col-lg-11">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<h5 class="card-title text-white mb-2">
|
||||||
|
<a href="{{ route('games.show', $game->slug) }}" class="text-white text-decoration-none">
|
||||||
|
{{ $game->name }}
|
||||||
|
</a>
|
||||||
|
</h5>
|
||||||
|
@if($game->summary)
|
||||||
|
<p class="card-text text-muted small mb-2">
|
||||||
|
{{ Str::limit($game->summary, 200) }}
|
||||||
|
</p>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-3">
|
||||||
|
<div class="d-flex flex-wrap gap-2 mb-2">
|
||||||
|
@if($game->rating)
|
||||||
|
<span class="badge bg-primary">
|
||||||
|
<i class="fas fa-star me-1"></i>{{ number_format($game->rating, 1) }}
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
@if($game->first_release_date)
|
||||||
|
<span class="badge bg-secondary">
|
||||||
|
<i class="fas fa-calendar-alt me-1"></i>{{ \Carbon\Carbon::parse($game->first_release_date)->format('M Y') }}
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@if($game->rating_count)
|
||||||
|
<small class="text-muted">
|
||||||
|
<i class="fas fa-users me-1"></i>{{ number_format($game->rating_count) }} ratings
|
||||||
|
</small>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-3">
|
||||||
|
<a href="{{ route('games.show', $game->slug) }}" class="btn btn-gradient w-100">
|
||||||
|
<i class="fas fa-info-circle me-1"></i> View Details
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -185,3 +297,53 @@
|
|||||||
{{ $games->links() }}
|
{{ $games->links() }}
|
||||||
</div>
|
</div>
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
|
@push('scripts')
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const viewButtons = document.querySelectorAll('.view-mode-btn');
|
||||||
|
const viewContainers = {
|
||||||
|
grid: document.getElementById('grid-view'),
|
||||||
|
cards: document.getElementById('card-view'),
|
||||||
|
list: document.getElementById('list-view')
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load saved view preference from localStorage or default to 'grid'
|
||||||
|
const savedView = localStorage.getItem('gamesViewMode') || 'grid';
|
||||||
|
switchView(savedView);
|
||||||
|
|
||||||
|
// Add click handlers to view buttons
|
||||||
|
viewButtons.forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
const viewMode = this.getAttribute('data-view');
|
||||||
|
switchView(viewMode);
|
||||||
|
localStorage.setItem('gamesViewMode', viewMode);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function switchView(viewMode) {
|
||||||
|
// Hide all views
|
||||||
|
Object.values(viewContainers).forEach(container => {
|
||||||
|
container.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show selected view
|
||||||
|
if (viewContainers[viewMode]) {
|
||||||
|
if (viewMode === 'grid' || viewMode === 'cards') {
|
||||||
|
viewContainers[viewMode].style.display = 'flex';
|
||||||
|
} else {
|
||||||
|
viewContainers[viewMode].style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update button states
|
||||||
|
viewButtons.forEach(btn => {
|
||||||
|
btn.classList.remove('active');
|
||||||
|
if (btn.getAttribute('data-view') === viewMode) {
|
||||||
|
btn.classList.add('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
|
|||||||
Reference in New Issue
Block a user