Statistics page redesign #60
@@ -218,4 +218,62 @@ footer {
|
|||||||
|
|
||||||
.watched-badge {
|
.watched-badge {
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stats → "Films regardés" modal */
|
||||||
|
.stats-movies-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-movie-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||||
|
border-radius: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-movie-poster {
|
||||||
|
width: 56px;
|
||||||
|
height: 84px;
|
||||||
|
border-radius: 10px;
|
||||||
|
object-fit: cover;
|
||||||
|
background: rgba(0, 0, 0, 0.06);
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-movie-poster.stats-movie-poster--empty {
|
||||||
|
background: linear-gradient(135deg, rgba(0,0,0,0.08), rgba(0,0,0,0.03));
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-movie-title {
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-movie-meta {
|
||||||
|
margin-top: 6px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px 10px;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-movie-imdb {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 0.2rem 0.45rem;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-movie-imdb .imdb-icon {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
+245
-33
@@ -356,6 +356,231 @@ window.addEventListener('DOMContentLoaded', async function () {
|
|||||||
try {
|
try {
|
||||||
pageLoader.setProgress(12);
|
pageLoader.setProgress(12);
|
||||||
|
|
||||||
|
// Modal UI (Films regardés)
|
||||||
|
const moviesModalEl = document.getElementById('moviesModal');
|
||||||
|
const movieGridEl = document.getElementById('movie-grid');
|
||||||
|
const movieSearchEl = document.getElementById('movie-search');
|
||||||
|
const movieSortEl = document.getElementById('movie-sort');
|
||||||
|
const moviesSummaryEl = document.getElementById('movies-summary');
|
||||||
|
const moviesEmptyEl = document.getElementById('movies-empty');
|
||||||
|
const moviesModalTitleEl = document.getElementById('moviesModalLabel');
|
||||||
|
|
||||||
|
let currentModalUserName = '';
|
||||||
|
let currentModalMovies = [];
|
||||||
|
|
||||||
|
function safeStr(v) {
|
||||||
|
return (v === null || v === undefined) ? '' : String(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseMaybeNumber(v) {
|
||||||
|
const n = Number(v);
|
||||||
|
return Number.isFinite(n) ? n : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatWatchedDate(iso) {
|
||||||
|
if (!iso) return '';
|
||||||
|
const d = new Date(iso);
|
||||||
|
if (Number.isNaN(d.getTime())) return '';
|
||||||
|
try {
|
||||||
|
return d.toLocaleDateString('fr-FR', { year: 'numeric', month: 'short', day: 'numeric' });
|
||||||
|
} catch (_) {
|
||||||
|
return d.toLocaleDateString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePosterUrl(poster) {
|
||||||
|
const s = safeStr(poster).trim();
|
||||||
|
if (!s || s.toUpperCase() === 'N/A') return '';
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
function movieMatchesQuery(movie, q) {
|
||||||
|
if (!q) return true;
|
||||||
|
const hay = `${safeStr(movie?.title)} ${safeStr(movie?.category)} ${safeStr(movie?.imdb_id)}`.toLowerCase();
|
||||||
|
return hay.includes(q.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortMovies(list, mode) {
|
||||||
|
const arr = Array.isArray(list) ? list.slice() : [];
|
||||||
|
if (mode === 'title') {
|
||||||
|
arr.sort((a, b) => safeStr(a?.title).localeCompare(safeStr(b?.title), 'fr', { sensitivity: 'base' }));
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
if (mode === 'category') {
|
||||||
|
arr.sort((a, b) => {
|
||||||
|
const c = safeStr(a?.category).localeCompare(safeStr(b?.category), 'fr', { sensitivity: 'base' });
|
||||||
|
if (c !== 0) return c;
|
||||||
|
return safeStr(a?.title).localeCompare(safeStr(b?.title), 'fr', { sensitivity: 'base' });
|
||||||
|
});
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
// recent (default): watchedDate desc, fallback title
|
||||||
|
arr.sort((a, b) => {
|
||||||
|
const at = a?.watchedDate ? new Date(a.watchedDate).getTime() : 0;
|
||||||
|
const bt = b?.watchedDate ? new Date(b.watchedDate).getTime() : 0;
|
||||||
|
if (Number.isFinite(at) && Number.isFinite(bt) && at !== bt) return bt - at;
|
||||||
|
return safeStr(a?.title).localeCompare(safeStr(b?.title), 'fr', { sensitivity: 'base' });
|
||||||
|
});
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMoviesModal() {
|
||||||
|
if (!movieGridEl || !movieSearchEl || !movieSortEl || !moviesSummaryEl || !moviesEmptyEl) return;
|
||||||
|
|
||||||
|
const q = safeStr(movieSearchEl.value).trim();
|
||||||
|
const sortMode = safeStr(movieSortEl.value).trim() || 'recent';
|
||||||
|
|
||||||
|
const filtered = (Array.isArray(currentModalMovies) ? currentModalMovies : []).filter((m) => movieMatchesQuery(m, q));
|
||||||
|
const sorted = sortMovies(filtered, sortMode);
|
||||||
|
|
||||||
|
const total = Array.isArray(currentModalMovies) ? currentModalMovies.length : 0;
|
||||||
|
moviesSummaryEl.textContent = total
|
||||||
|
? `${sorted.length} / ${total}`
|
||||||
|
: '0';
|
||||||
|
|
||||||
|
movieGridEl.innerHTML = '';
|
||||||
|
if (!sorted.length) {
|
||||||
|
moviesEmptyEl.classList.remove('d-none');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
moviesEmptyEl.classList.add('d-none');
|
||||||
|
|
||||||
|
sorted.forEach((movie) => {
|
||||||
|
const imdbId = safeStr(movie?.imdb_id).trim();
|
||||||
|
const title = safeStr(movie?.title).trim() || '(sans titre)';
|
||||||
|
const category = safeStr(movie?.category).trim();
|
||||||
|
const ratingRaw = safeStr(movie?.rating).trim();
|
||||||
|
const rating = parseMaybeNumber(ratingRaw);
|
||||||
|
const watchedLabel = formatWatchedDate(movie?.watchedDate);
|
||||||
|
const posterUrl = normalizePosterUrl(movie?.poster);
|
||||||
|
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'stats-movie-item';
|
||||||
|
|
||||||
|
const poster = document.createElement('img');
|
||||||
|
poster.className = 'stats-movie-poster';
|
||||||
|
poster.alt = title;
|
||||||
|
if (posterUrl) {
|
||||||
|
poster.src = posterUrl;
|
||||||
|
} else {
|
||||||
|
poster.classList.add('stats-movie-poster--empty');
|
||||||
|
// Keep src empty; browser will not request anything.
|
||||||
|
poster.src = '';
|
||||||
|
}
|
||||||
|
poster.addEventListener('error', () => {
|
||||||
|
poster.classList.add('stats-movie-poster--empty');
|
||||||
|
try { poster.removeAttribute('src'); } catch (_) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
const main = document.createElement('div');
|
||||||
|
main.className = 'flex-grow-1';
|
||||||
|
|
||||||
|
const topRow = document.createElement('div');
|
||||||
|
topRow.className = 'd-flex align-items-start justify-content-between gap-2';
|
||||||
|
|
||||||
|
const titleEl = document.createElement('div');
|
||||||
|
titleEl.className = 'stats-movie-title';
|
||||||
|
titleEl.textContent = title;
|
||||||
|
|
||||||
|
const right = document.createElement('div');
|
||||||
|
right.className = 'text-nowrap';
|
||||||
|
|
||||||
|
if (imdbId) {
|
||||||
|
const imdbLink = document.createElement('a');
|
||||||
|
imdbLink.className = 'btn btn-outline-dark btn-sm stats-movie-imdb';
|
||||||
|
imdbLink.href = `https://www.imdb.com/title/${encodeURIComponent(imdbId)}/`;
|
||||||
|
imdbLink.target = '_blank';
|
||||||
|
imdbLink.rel = 'noopener noreferrer';
|
||||||
|
imdbLink.setAttribute('aria-label', 'Voir sur IMDb');
|
||||||
|
|
||||||
|
const imdbText = document.createElement('span');
|
||||||
|
imdbText.className = 'small';
|
||||||
|
imdbText.textContent = 'IMDb';
|
||||||
|
|
||||||
|
const imdbIcon = document.createElement('img');
|
||||||
|
imdbIcon.src = '/img/imdb.png';
|
||||||
|
imdbIcon.className = 'imdb-icon';
|
||||||
|
imdbIcon.alt = 'IMDb';
|
||||||
|
|
||||||
|
imdbLink.appendChild(imdbText);
|
||||||
|
imdbLink.appendChild(imdbIcon);
|
||||||
|
right.appendChild(imdbLink);
|
||||||
|
}
|
||||||
|
|
||||||
|
topRow.appendChild(titleEl);
|
||||||
|
topRow.appendChild(right);
|
||||||
|
|
||||||
|
const meta = document.createElement('div');
|
||||||
|
meta.className = 'stats-movie-meta';
|
||||||
|
|
||||||
|
if (category) {
|
||||||
|
const badge = document.createElement('span');
|
||||||
|
badge.className = 'badge bg-light text-dark border';
|
||||||
|
badge.textContent = category;
|
||||||
|
meta.appendChild(badge);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rating !== null && rating > 0) {
|
||||||
|
const r = document.createElement('span');
|
||||||
|
r.className = 'text-muted';
|
||||||
|
r.textContent = `⭐ ${rating.toFixed(1)}`;
|
||||||
|
meta.appendChild(r);
|
||||||
|
} else if (ratingRaw) {
|
||||||
|
const r = document.createElement('span');
|
||||||
|
r.className = 'text-muted';
|
||||||
|
r.textContent = `⭐ ${ratingRaw}`;
|
||||||
|
meta.appendChild(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (watchedLabel) {
|
||||||
|
const d = document.createElement('span');
|
||||||
|
d.className = 'text-muted';
|
||||||
|
d.textContent = `Vu le ${watchedLabel}`;
|
||||||
|
meta.appendChild(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
main.appendChild(topRow);
|
||||||
|
if (meta.childNodes.length) main.appendChild(meta);
|
||||||
|
|
||||||
|
item.appendChild(poster);
|
||||||
|
item.appendChild(main);
|
||||||
|
movieGridEl.appendChild(item);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function openMoviesModalForUser(userStat) {
|
||||||
|
currentModalUserName = safeStr(userStat?.name).trim();
|
||||||
|
currentModalMovies = Array.isArray(userStat?.watchedMovies) ? userStat.watchedMovies : [];
|
||||||
|
|
||||||
|
if (moviesModalTitleEl) {
|
||||||
|
const count = currentModalMovies.length;
|
||||||
|
moviesModalTitleEl.textContent = currentModalUserName
|
||||||
|
? `Films regardés — ${currentModalUserName} (${count})`
|
||||||
|
: `Films regardés (${count})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (movieSearchEl) movieSearchEl.value = '';
|
||||||
|
if (movieSortEl) movieSortEl.value = 'recent';
|
||||||
|
renderMoviesModal();
|
||||||
|
|
||||||
|
if (moviesModalEl) {
|
||||||
|
const modal = new bootstrap.Modal(moviesModalEl);
|
||||||
|
modal.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (movieSearchEl) {
|
||||||
|
movieSearchEl.addEventListener('input', () => renderMoviesModal());
|
||||||
|
}
|
||||||
|
if (movieSortEl) {
|
||||||
|
movieSortEl.addEventListener('change', () => renderMoviesModal());
|
||||||
|
}
|
||||||
|
if (moviesModalEl) {
|
||||||
|
moviesModalEl.addEventListener('shown.bs.modal', () => {
|
||||||
|
try { movieSearchEl?.focus(); } catch (_) {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Show loading states immediately (avoid "blank" panels)
|
// Show loading states immediately (avoid "blank" panels)
|
||||||
const table = document.querySelector('.user-table');
|
const table = document.querySelector('.user-table');
|
||||||
const statsError = document.getElementById('stats-error');
|
const statsError = document.getElementById('stats-error');
|
||||||
@@ -402,13 +627,26 @@ window.addEventListener('DOMContentLoaded', async function () {
|
|||||||
|
|
||||||
stats.forEach(userStat => {
|
stats.forEach(userStat => {
|
||||||
const userRow = document.createElement('tr');
|
const userRow = document.createElement('tr');
|
||||||
userRow.innerHTML = `
|
const nameTd = document.createElement('td');
|
||||||
<td>
|
const link = document.createElement('a');
|
||||||
<a href="#" class="user-link" data-movies='${JSON.stringify(userStat.watchedMovies)}'>${userStat.name}</a>
|
link.href = '#';
|
||||||
</td>
|
link.className = 'user-link';
|
||||||
<td>${userStat.watchedCount}</td>
|
link.textContent = userStat.name;
|
||||||
<td>${userStat.watchedRatio}</td>
|
link.addEventListener('click', function (event) {
|
||||||
`;
|
event.preventDefault();
|
||||||
|
openMoviesModalForUser(userStat);
|
||||||
|
});
|
||||||
|
nameTd.appendChild(link);
|
||||||
|
|
||||||
|
const countTd = document.createElement('td');
|
||||||
|
countTd.textContent = String(userStat.watchedCount ?? '');
|
||||||
|
|
||||||
|
const ratioTd = document.createElement('td');
|
||||||
|
ratioTd.textContent = String(userStat.watchedRatio ?? '');
|
||||||
|
|
||||||
|
userRow.appendChild(nameTd);
|
||||||
|
userRow.appendChild(countTd);
|
||||||
|
userRow.appendChild(ratioTd);
|
||||||
userTableBody.appendChild(userRow);
|
userTableBody.appendChild(userRow);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -417,32 +655,6 @@ window.addEventListener('DOMContentLoaded', async function () {
|
|||||||
if (statsError) statsError.classList.add('d-none');
|
if (statsError) statsError.classList.add('d-none');
|
||||||
pageLoader.setProgress(92);
|
pageLoader.setProgress(92);
|
||||||
|
|
||||||
// Add event listener for user links
|
|
||||||
document.querySelectorAll('.user-link').forEach(link => {
|
|
||||||
link.addEventListener('click', function (event) {
|
|
||||||
event.preventDefault();
|
|
||||||
const datasetMovies = this.dataset.movies;
|
|
||||||
console.log(datasetMovies);
|
|
||||||
const movies = JSON.parse(datasetMovies);
|
|
||||||
|
|
||||||
// Clear the current movie list in the modal
|
|
||||||
const movieList = document.getElementById('movie-list');
|
|
||||||
movieList.innerHTML = '';
|
|
||||||
|
|
||||||
// Populate the modal with the movies the user has watched
|
|
||||||
movies.forEach(movie => {
|
|
||||||
const movieItem = document.createElement('li');
|
|
||||||
movieItem.classList.add('list-group-item');
|
|
||||||
movieItem.textContent = movie.title;
|
|
||||||
movieList.appendChild(movieItem);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Show the modal
|
|
||||||
const modal = new bootstrap.Modal(document.getElementById('moviesModal'));
|
|
||||||
modal.show();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
pageLoader.done();
|
pageLoader.done();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading user statistics:', error);
|
console.error('Error loading user statistics:', error);
|
||||||
|
|||||||
+19
-4
@@ -165,16 +165,31 @@
|
|||||||
|
|
||||||
<!-- Modal for displaying movies watched -->
|
<!-- Modal for displaying movies watched -->
|
||||||
<div class="modal fade" id="moviesModal" tabindex="-1" aria-labelledby="moviesModalLabel" aria-hidden="true">
|
<div class="modal fade" id="moviesModal" tabindex="-1" aria-labelledby="moviesModalLabel" aria-hidden="true">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title" id="moviesModalLabel">Films regardés</h5>
|
<h5 class="modal-title" id="moviesModalLabel">Films regardés</h5>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<ul id="movie-list" class="list-group">
|
<div class="d-flex flex-column gap-3">
|
||||||
<!-- Movie titles will be inserted here -->
|
<div class="d-flex flex-wrap gap-2 align-items-center">
|
||||||
</ul>
|
<div class="input-group input-group-sm flex-grow-1" style="min-width: 220px;">
|
||||||
|
<span class="input-group-text">Rechercher</span>
|
||||||
|
<input id="movie-search" type="search" class="form-control" placeholder="Titre, catégorie…" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
<select id="movie-sort" class="form-select form-select-sm" style="max-width: 240px;">
|
||||||
|
<option value="recent">Plus récents</option>
|
||||||
|
<option value="title">Titre (A → Z)</option>
|
||||||
|
<option value="category">Catégorie</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="movies-summary" class="text-muted small"></div>
|
||||||
|
|
||||||
|
<div id="movies-empty" class="text-muted d-none">Aucun film à afficher.</div>
|
||||||
|
<div id="movie-grid" class="stats-movies-grid" aria-live="polite"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+25
-13
@@ -122,14 +122,16 @@ async function getOrInitActiveYear() {
|
|||||||
// Get user stats with watched movie titles and details from the movies collection
|
// Get user stats with watched movie titles and details from the movies collection
|
||||||
router.get("/stats", verifyToken, async (req, res) => {
|
router.get("/stats", verifyToken, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
// Fetch all users
|
// Fetch all users (only needed fields)
|
||||||
const users = await User.find();
|
const users = await User.find().select("name watchedMovies").lean();
|
||||||
|
|
||||||
// If a year is explicitly provided, use it; otherwise default to the global active year.
|
// If a year is explicitly provided, use it; otherwise default to the global active year.
|
||||||
const year = parseOscarYear(req.query.year) || await getOrInitActiveYear();
|
const year = parseOscarYear(req.query.year) || await getOrInitActiveYear();
|
||||||
|
|
||||||
// Fetch movies (optionally filtered by year)
|
// Fetch movies (optionally filtered by year)
|
||||||
const allMovies = await Movie.find({ year });
|
const allMovies = await Movie.find({ year })
|
||||||
|
.select("imdb_id title poster rating category year")
|
||||||
|
.lean();
|
||||||
const totalMoviesCount = allMovies.length;
|
const totalMoviesCount = allMovies.length;
|
||||||
|
|
||||||
if (totalMoviesCount === 0) {
|
if (totalMoviesCount === 0) {
|
||||||
@@ -140,19 +142,29 @@ router.get("/stats", verifyToken, async (req, res) => {
|
|||||||
const userStats = users.map((user) => {
|
const userStats = users.map((user) => {
|
||||||
const movieByImdbId = new Map(allMovies.map((m) => [m.imdb_id, m]));
|
const movieByImdbId = new Map(allMovies.map((m) => [m.imdb_id, m]));
|
||||||
|
|
||||||
// Get watched movies with details from the database
|
// Join watched movies with movie details for the selected year (keep watchedDate)
|
||||||
const watchedMovies = user.watchedMovies
|
const watchedRaw = Array.isArray(user?.watchedMovies) ? user.watchedMovies : [];
|
||||||
.map((wm) => movieByImdbId.get(wm.imdb_id)) // Match movie details
|
const watchedMoviesDetails = watchedRaw
|
||||||
.filter(Boolean); // Remove any `undefined` values (if movie is not found)
|
.map((wm) => {
|
||||||
|
const imdbId = typeof wm?.imdb_id === "string" ? wm.imdb_id : "";
|
||||||
|
if (!imdbId) return null;
|
||||||
|
const movie = movieByImdbId.get(imdbId);
|
||||||
|
if (!movie) return null;
|
||||||
|
return {
|
||||||
|
imdb_id: movie.imdb_id,
|
||||||
|
title: movie.title || "",
|
||||||
|
poster: movie.poster || "",
|
||||||
|
rating: movie.rating || "",
|
||||||
|
category: movie.category || "",
|
||||||
|
year: Number.isInteger(movie.year) ? movie.year : year,
|
||||||
|
watchedDate: wm?.watchedDate ? new Date(wm.watchedDate).toISOString() : null,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
const watchedCount = watchedMovies.length;
|
const watchedCount = watchedMoviesDetails.length;
|
||||||
const watchedRatio = ((watchedCount / totalMoviesCount) * 100).toFixed(1);
|
const watchedRatio = ((watchedCount / totalMoviesCount) * 100).toFixed(1);
|
||||||
|
|
||||||
const watchedMoviesDetails = watchedMovies.map((movie) => ({
|
|
||||||
imdb_id: movie.imdb_id,
|
|
||||||
title: movie.title
|
|
||||||
}));
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: user.name,
|
name: user.name,
|
||||||
year,
|
year,
|
||||||
|
|||||||
Reference in New Issue
Block a user