Skip to content

Commit 8e62476

Browse files
committed
Add last update info and sort titles by recency
- Show last chapter number on each title card - Sort titles by last update date - Display relative dates (today, yesterday, N days ago)
1 parent 5ceeba8 commit 8e62476

3 files changed

Lines changed: 63 additions & 24 deletions

File tree

assets/css/style.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -722,4 +722,12 @@ body.reader-theme-sepia .reader-content-wrap {
722722
}
723723
.btn-sort:active {
724724
transform: scale(0.97);
725+
}
726+
727+
/* Последнее обновление на карточке */
728+
.title-card-last-update {
729+
font-size: 0.8125rem;
730+
color: var(--color-accent);
731+
margin: 0.25rem 0 0.5rem;
732+
opacity: 0.85;
725733
}

assets/js/app.js

Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -93,21 +93,53 @@ var App = (function() {
9393

9494
function renderHome(container, data) {
9595
if (!container) return;
96+
97+
function timeAgo(dateStr) {
98+
if (!dateStr) return '';
99+
try {
100+
var now = new Date();
101+
var then = new Date(dateStr);
102+
var diff = Math.floor((now - then) / (1000 * 60 * 60 * 24));
103+
104+
if (diff === 0) return 'сегодня';
105+
if (diff === 1) return 'вчера';
106+
if (diff < 4) return diff + ' дня назад';
107+
if (diff < 7) return diff + ' дней назад';
108+
if (diff < 30) return Math.floor(diff / 7) + ' нед. назад';
109+
return dateStr;
110+
} catch(e) {
111+
return dateStr;
112+
}
113+
}
114+
96115
var titles = (data && data.titles) ? data.titles : [];
97116
if (titles.length === 0) {
98117
container.innerHTML = '<p class="text-muted">Пока нет добавленных тайтлов.</p>';
99118
return;
100119
}
120+
101121
var html = '';
102122
titles.forEach(function(t, i) {
103123
var translated = t.translatedChapters || 0;
104124
var total = t.totalChapters || 0;
105125
var pct = total > 0 ? Math.min(100, Math.round((translated / total) * 100)) : 0;
106-
var lastCh = t.chapters && t.chapters.length ? t.chapters.reduce(function(a, c) {
107-
var d = c.datePublished || '';
108-
return d > (a.datePublished || '') ? c : a;
109-
}, t.chapters[0]) : null;
110-
var lastDate = lastCh ? (lastCh.datePublished || t.lastChapterDate || '') : (t.lastChapterDate || '');
126+
127+
var lastCh = null;
128+
var lastDate = '';
129+
var lastNum = '';
130+
131+
if (t.chapters && t.chapters.length) {
132+
var sortedChapters = t.chapters.slice().sort(function(a, b) {
133+
return (b.id || 0) - (a.id || 0);
134+
});
135+
136+
if (sortedChapters.length > 0) {
137+
lastCh = sortedChapters[0];
138+
lastDate = lastCh.datePublished || t.lastChapterDate || '';
139+
lastNum = lastCh.number != null ? lastCh.number : lastCh.id;
140+
}
141+
}
142+
111143
var coverSrc = (t.cover || '').replace(/^\//, '');
112144

113145
html += '<a href="' + getTitleUrl(t.id) + '" class="title-card fade-in" style="animation-delay:' + (i * 0.05) + 's">';
@@ -117,13 +149,21 @@ var App = (function() {
117149
if (t.titleEn && t.title !== t.titleEn) html += '<p class="title-card-en">' + escapeHtml(t.titleEn) + '</p>';
118150
html += '<div class="progress-wrap"><div class="progress-bar"><div class="progress-fill" style="width:' + pct + '%"></div></div>';
119151
html += '<p class="progress-text">' + translated + ' / ' + total + ' chapters translated</p></div>';
152+
153+
if (lastDate && lastNum) {
154+
var displayDate = timeAgo(lastDate);
155+
html += '<p class="title-card-last-update">📖 Обновлено: гл. ' + lastNum + ' (' + escapeHtml(displayDate) + ')</p>';
156+
} else if (lastDate) {
157+
var displayDate = timeAgo(lastDate);
158+
html += '<p class="title-card-last-update">📖 Обновлено: ' + escapeHtml(displayDate) + '</p>';
159+
}
160+
120161
if (t.genre && t.genre.length) {
121162
html += '<div class="genres">';
122163
t.genre.slice(0, 4).forEach(function(g) { html += '<span class="genre-tag">' + escapeHtml(g) + '</span>'; });
123164
html += '</div>';
124165
}
125166
html += '<span class="status-badge ' + statusClass(t.status) + '">' + (t.status === 'completed' ? 'Completed' : t.status === 'hiatus' || t.status === 'on hiatus' ? 'On Hiatus' : 'Ongoing') + '</span>';
126-
if (lastDate) html += '<p class="title-card-date">Last chapter: ' + escapeHtml(lastDate) + '</p>';
127167
html += '</div></a>';
128168
});
129169
container.innerHTML = html;
@@ -138,10 +178,8 @@ var App = (function() {
138178
}
139179

140180
function renderTitlePage(root, title) {
141-
// Ключ для localStorage (уникальный для каждого тайтла)
142181
var SORT_KEY = 'bb_sort_' + title.id;
143182

144-
// Загружаем состояние сортировки из localStorage
145183
var sortDescending = false;
146184
try {
147185
var saved = localStorage.getItem(SORT_KEY);
@@ -150,16 +188,13 @@ var App = (function() {
150188
}
151189
} catch (e) {}
152190

153-
// Функция для рендеринга списка глав с учетом сортировки
154191
function renderChapters(lang) {
155192
var chapters = (lang === 'ru') ? byLang.ru : byLang.en;
156193
var container = document.getElementById('chapters-' + lang);
157194
if (!container) return;
158195

159-
// Копируем массив, чтобы не менять оригинал
160196
var sorted = chapters.slice();
161197

162-
// Сортируем в зависимости от состояния
163198
sorted.sort(function(a, b) {
164199
var na = a.number != null ? a.number : a.id;
165200
var nb = b.number != null ? b.number : b.id;
@@ -168,7 +203,6 @@ var App = (function() {
168203
return sortDescending ? nb - na : na - nb;
169204
});
170205

171-
// Рендерим список
172206
container.innerHTML = chapterListHtml(sorted, title.id, lang);
173207
}
174208

@@ -209,14 +243,12 @@ var App = (function() {
209243
html += '<span class="status-badge ' + statusClass(title.status) + '">' + (title.status === 'completed' ? 'Completed' : title.status === 'hiatus' || title.status === 'on hiatus' ? 'On Hiatus' : 'Ongoing') + '</span>';
210244
html += '</div></div>';
211245

212-
// Вкладки языков + кнопка сортировки
213246
html += '<div class="lang-tabs-wrapper" style="display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:0.5rem; margin-bottom:1rem;">';
214247
html += '<div class="lang-tabs" role="tablist" style="display:flex; gap:0; border-bottom:1px solid var(--color-border); flex-wrap:wrap;">';
215248
html += '<button type="button" class="lang-tab active" role="tab" aria-selected="true" data-lang="ru">RU · Перевод</button>';
216249
html += '<button type="button" class="lang-tab" role="tab" aria-selected="false" data-lang="en">EN · Оригинал</button>';
217250
html += '</div>';
218251

219-
// Кнопка сортировки (всегда один текст)
220252
html += '<button type="button" id="sort-toggle" class="btn btn-sort" style="font-size:0.8125rem; padding:0.3rem 0.75rem; min-height:32px; border-color:var(--color-border); background:var(--color-bg-elevated); color:var(--color-text-secondary);">↕ Сортировать</button>';
221253
html += '</div>';
222254

@@ -236,7 +268,6 @@ var App = (function() {
236268
document.getElementById('chapters-ru').style.display = lang === 'ru' ? '' : 'none';
237269
document.getElementById('chapters-en').style.display = lang === 'en' ? '' : 'none';
238270

239-
// При переключении вкладки применяем текущую сортировку
240271
renderChapters(lang);
241272
});
242273
});
@@ -247,21 +278,17 @@ var App = (function() {
247278
sortBtn.addEventListener('click', function() {
248279
sortDescending = !sortDescending;
249280

250-
// Сохраняем в localStorage
251281
try {
252282
localStorage.setItem(SORT_KEY, String(sortDescending));
253283
} catch (e) {}
254284

255-
// Определяем, какая вкладка сейчас активна
256285
var activeTab = root.querySelector('.lang-tab.active');
257286
var activeLang = activeTab ? activeTab.getAttribute('data-lang') : 'ru';
258287

259-
// Перерендериваем только активную вкладку
260288
renderChapters(activeLang);
261289
});
262290
}
263291

264-
// После загрузки применяем сохраненную сортировку к активной вкладке
265292
var activeTab = root.querySelector('.lang-tab.active');
266293
var activeLang = activeTab ? activeTab.getAttribute('data-lang') : 'ru';
267294
renderChapters(activeLang);
@@ -297,7 +324,6 @@ var App = (function() {
297324
return div.innerHTML;
298325
}
299326

300-
// Обновленная функция получения конкретной главы
301327
function getChapter(data, titleId, chapterNum) {
302328
var title = (data && data.titles) ? data.titles.find(function(t) { return t.id === titleId; }) : null;
303329
if (!title || !title.chapters) return { title: title, chapter: null };

index.html

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,19 @@
8686
var ft = document.getElementById('footer-telegram');
8787
if (ft) { ft.href = data.site.telegram || '#'; }
8888
}
89-
89+
90+
if (data && data.titles) {
91+
data.titles.sort(function(a, b) {
92+
var dateA = a.lastChapterDate || '';
93+
var dateB = b.lastChapterDate || '';
94+
return dateB.localeCompare(dateA);
95+
});
96+
}
97+
9098
var lastEl = document.getElementById('hero-last');
9199
if (lastEl && data && data.titles) {
92100
var allChapters = [];
93101

94-
// Собираем все главы со всех тайтлов
95102
data.titles.forEach(function(title) {
96103
if (title.chapters && title.chapters.length) {
97104
title.chapters.forEach(function(ch) {
@@ -105,7 +112,6 @@
105112
});
106113

107114
if (allChapters.length > 0) {
108-
// Сортируем по ID (самые новые главы имеют больший ID)
109115
allChapters.sort(function(a, b) {
110116
return (b.chapter.id || 0) - (a.chapter.id || 0);
111117
});
@@ -115,7 +121,6 @@
115121
var chapterNum = latest.chapter.number != null ? latest.chapter.number : latest.chapter.id;
116122
var pubDate = latest.chapter.datePublished || '';
117123

118-
// Форматируем дату для отображения
119124
function formatDate(dateStr) {
120125
if (!dateStr) return '';
121126
var date = new Date(dateStr);

0 commit comments

Comments
 (0)