Skip to content

Commit b3286da

Browse files
committed
feat: implement animated post navigation and lazy load images
1 parent 6780b55 commit b3286da

3 files changed

Lines changed: 131 additions & 28 deletions

File tree

Components/PostCard.razor

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<article class="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow">
2-
<img src="@(_imageUrl)" alt="Post image" class="w-full h-48 object-cover">
2+
<img loading="lazy" decoding="async" src="@(_imageUrl)" alt="Post image" class="w-full h-48 object-cover">
33
<div class="p-6">
44
<div class="flex items-center mb-2">
55
<span class="@_chipBgColor @_chipTextColor text-xs font-medium px-2.5 py-0.5 rounded">@_keyTag</span>
@@ -46,4 +46,4 @@
4646
base.OnParametersSet();
4747
}
4848

49-
}
49+
}

Pages/Home.razor

Lines changed: 108 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
@page "/"
2+
@inject NavigationManager Nav
23

34
<PageTitle>{{BlakeSimpleTailwindSample}} - Home</PageTitle>
45

@@ -42,43 +43,66 @@
4243
<!-- Recent Posts -->
4344
<section>
4445
<h3 class="text-2xl font-bold text-gray-900 mb-6">Recent Posts</h3>
45-
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
46-
@foreach (var post in _recentPosts)
47-
{
48-
<PostCard Page="@post" />
49-
}
50-
</div>
46+
<div class="viewport">
47+
<div class="rail @(_isAnimating ? "anim" : null)" style="transform: translateX(@_railX%)">
48+
<div class="pane">
49+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
50+
@foreach (var post in _recentPosts)
51+
{
52+
<PostCard Page="@post"/>
53+
}
54+
</div>
55+
</div>
56+
<div class="pane">
57+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
58+
@foreach (var post in _incomingPosts)
59+
{
60+
<PostCard Page="@post"/>
61+
}
62+
</div>
63+
</div>
64+
</div>
65+
</div>
5166
</section>
5267

5368
<!-- Pagination -->
5469
@if (_pages.Count > 0)
5570
{
5671
<div class="flex justify-center mt-12">
5772
<div class="flex space-x-1">
58-
<a href="#" class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-l-md hover:bg-gray-50" @onclick=PreviousPage>Previous</a>
73+
<button class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-l-md hover:bg-gray-50" @onclick=PreviousPage>Previous</button>
5974
@foreach (var item in _pages)
6075
{
61-
<a href="#" class="px-3 py-2 text-sm font-medium @(item == _currentPage ? "text-white bg-blue-600 border-blue-600" : "text-gray-700 bg-white border-gray-300 hover:bg-gray-50")"
62-
@onclick="() => PageSelected(item)">@item</a>
76+
<button class="px-3 py-2 text-sm font-medium @(item == _currentPage ? "text-white bg-blue-600 border-blue-600" : "text-gray-700 bg-white border-gray-300 hover:bg-gray-50")"
77+
@onclick="() => PageSelected(item)">@item</button>
6378
}
64-
<a href="#" class="px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-r-md hover:bg-gray-50" @onclick=NextPage>Next</a>
79+
<button class="px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-r-md hover:bg-gray-50" @onclick=NextPage>Next</button>
6580
</div>
6681
</div>
6782
}
6883
</main>
6984

7085
<!-- Footer -->
71-
<Footer />
86+
<Footer></Footer>
7287

7388
@code {
89+
90+
[SupplyParameterFromQuery]
91+
private int? Page { get; set; } = 1;
92+
7493
private PageModel? _featuredPost;
7594

7695
private List<PageModel> _recentPosts = [];
7796

97+
private List<PageModel> _incomingPosts = [];
98+
7899
private List<int> _pages = [];
79100

80101
private int _currentPage = 1;
81102

103+
bool _isAnimating;
104+
int _railX; // 0 or -100 (or +100 for prev)
105+
82106
protected override void OnInitialized()
83107
{
84108
base.OnInitialized();
@@ -101,33 +125,91 @@
101125
_pages = Enumerable.Range(1, (int)Math.Ceiling((double)(pages.Count - 1) / 6)).ToList();
102126
}
103127
}
104-
}
105128

106-
private void PageSelected(int pageNumber)
107-
{
108-
_recentPosts = GeneratedContentIndex.GetPages()
109-
.Where(p => !p.Draft && p.Slug.Contains("/posts/", StringComparison.OrdinalIgnoreCase))
110-
.OrderByDescending(p => p.Date)
111-
.Skip((pageNumber - 1) * 6)
112-
.Take(6)
113-
.ToList();
129+
if (Page is > 1)
130+
{
131+
_currentPage = Page.Value;
132+
GoToPage(_currentPage);
133+
}
114134
}
115135

116-
private void NextPage()
136+
137+
private async Task NextPage()
117138
{
118139
if (_currentPage < _pages.Count)
119140
{
120-
_currentPage++;
121-
PageSelected(_currentPage);
141+
await PageSelected(_currentPage + 1);
122142
}
123143
}
124144

125-
private void PreviousPage()
145+
private async Task PreviousPage()
126146
{
127147
if (_currentPage > 1)
128148
{
129-
_currentPage--;
130-
PageSelected(_currentPage);
149+
await PageSelected(_currentPage - 1);
131150
}
132151
}
152+
153+
private async Task PageSelected(int pageNumber)
154+
{
155+
if (pageNumber > _currentPage - 1 || pageNumber < _currentPage + 1)
156+
{
157+
await GoToPageAsync(pageNumber);
158+
}
159+
else
160+
{
161+
GoToPage(pageNumber);
162+
}
163+
}
164+
165+
private async Task GoToPageAsync(int pageId)
166+
{
167+
if (pageId == _currentPage || _isAnimating) return;
168+
169+
_incomingPosts = GeneratedContentIndex.GetPages()
170+
.Where(p => !p.Draft && p.Slug.Contains("/posts/", StringComparison.OrdinalIgnoreCase))
171+
.OrderByDescending(p => p.Date)
172+
.Skip((pageId - 1) * 6)
173+
.Take(6)
174+
.ToList();
175+
176+
_isAnimating = true;
177+
_railX = pageId > _currentPage ? -100 : 100; // slide direction
178+
StateHasChanged();
179+
180+
await Task.Delay(250); // match CSS
181+
_currentPage = pageId;
182+
_recentPosts = _incomingPosts;
183+
_incomingPosts = [];
184+
_railX = 0; _isAnimating = false;
185+
GoToPage(_currentPage); // update URL
186+
}
187+
188+
private void GoToPage(int pageId)
189+
{
190+
_recentPosts = GeneratedContentIndex.GetPages()
191+
.Where(p => !p.Draft && p.Slug.Contains("/posts/", StringComparison.OrdinalIgnoreCase))
192+
.OrderByDescending(p => p.Date)
193+
.Skip((pageId - 1) * 6)
194+
.Take(6)
195+
.ToList();
196+
197+
_isAnimating = false;
198+
_railX = 0;
199+
200+
// TODO: forceLoad: false still navigates, just client-side. So we still get scroll to top.
201+
// Will need to either bypass that, or just use history.pushState in JSInterop.
202+
// SetQuery(pageId);
203+
}
204+
205+
private void SetQuery(int page)
206+
{
207+
var uri = new Uri(Nav.Uri);
208+
var baseUri = uri.GetLeftPart(UriPartial.Path);
209+
var qs = System.Web.HttpUtility.ParseQueryString(uri.Query);
210+
qs["page"] = (page + 1).ToString();
211+
212+
var newUri = $"{baseUri}?{qs}";
213+
Nav.NavigateTo(newUri, forceLoad: false);
214+
}
133215
}

Pages/Home.razor.css

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
.viewport {
2+
overflow:hidden;
3+
}
4+
.rail {
5+
display:grid; grid-template-columns:100% 100%; transform:translateX(0);
6+
}
7+
.rail.anim {
8+
transition: transform .25s ease;
9+
}
10+
11+
.results.fade {
12+
opacity:0; transition:opacity .15s ease;
13+
}
14+
.results {
15+
opacity:1; transition:opacity .15s ease;
16+
}
17+
18+
@media (prefers-reduced-motion: reduce) {
19+
.rail.anim { transition:none; }
20+
.results, .results.fade { transition:none; }
21+
}

0 commit comments

Comments
 (0)