feat: randomized layout in 10% chunks based on likes

This commit is contained in:
Jannik committed 2026-09-24 04:11:33 +02:00
1 parent 21c1166fba
commit f53e27d3ae
5 files changed
+140 -21

No files matched your search

+55 -7
View File
@@ -2,6 +2,8 @@ import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/db';
import { validateSession } from '@/lib/auth/session';
import { getShuffledItemIds } from '@/lib/sorting';
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams;
@@ -29,12 +31,18 @@ export async function GET(request: NextRequest) {
where.directory = directoryParam;
}
let itemsRaw: any[] = [];
let items: any[] = [];
let nextCursor: string | number | null = null;
if (isAdminRequest) {
// Standard cursor pagination for Admin
const cursor =
cursorParam && !isNaN(parseInt(cursorParam, 10))
? parseInt(cursorParam, 10)
: undefined;
const items = await prisma.mediaItem.findMany({
items = await prisma.mediaItem.findMany({
where,
take: limit + 1,
...(cursor ? { skip: 1, cursor: { id: cursor } } : {}),
@@ -43,18 +51,58 @@ export async function GET(request: NextRequest) {
{ sortOrder: 'asc' },
{ createdAt: 'desc' },
],
include: {
_count: {
select: { likes: true },
},
},
include: { _count: { select: { likes: true } } },
});
let nextCursor: number | null = null;
if (items.length > limit) {
const nextItem = items.pop();
nextCursor = nextItem!.id;
}
} else {
// Custom pagination with randomized chunk sorting
let seed = Math.floor(Math.random() * 1000000);
let page = 1;
if (cursorParam && cursorParam.includes('_')) {
const parts = cursorParam.split('_');
seed = parseInt(parts[0], 10);
page = parseInt(parts[1], 10);
} else if (!cursorParam) {
// Initial load for a directory filter, maybe? We just generate a new seed.
page = 1;
}
// Fetch all valid IDs and metadata
const allMetadata = await prisma.mediaItem.findMany({
where,
select: {
id: true,
featured: true,
placementMode: true,
sortOrder: true,
_count: { select: { likes: true } }
}
});
const sortedIds = getShuffledItemIds(allMetadata, seed);
const startIndex = (page - 1) * limit;
const pageIds = sortedIds.slice(startIndex, startIndex + limit);
const hasMore = startIndex + limit < sortedIds.length;
itemsRaw = await prisma.mediaItem.findMany({
where: { id: { in: pageIds } },
include: { _count: { select: { likes: true } } },
});
// Re-order to match sortedIds
const itemsMap = new Map(itemsRaw.map(item => [item.id, item]));
items = pageIds.map(id => itemsMap.get(id)).filter(Boolean);
if (hasMore) {
nextCursor = `${seed}_${page + 1}`;
}
}
// Fetch voting status for the client
const settings = await prisma.appSettings.findUnique({ where: { id: 1 } });
+32 -11
View File
@@ -3,23 +3,44 @@ import { prisma } from '@/lib/db';
import Header from '@/components/Header';
import MediaWall from '@/components/MediaWall';
import { getShuffledItemIds } from '@/lib/sorting';
async function getInitialMedia() {
try {
const items = await prisma.mediaItem.findMany({
// 1. Fetch metadata for all published items to sort them properly
const allMetadata = await prisma.mediaItem.findMany({
where: { published: true },
take: 51,
orderBy: [
{ featured: 'desc' },
{ sortOrder: 'asc' },
{ createdAt: 'desc' },
],
select: {
id: true,
featured: true,
placementMode: true,
sortOrder: true,
_count: { select: { likes: true } }
}
});
// 2. Generate a random seed for this session
const seed = Math.floor(Math.random() * 1000000);
// 3. Get properly sorted and chunk-shuffled IDs
const sortedIds = getShuffledItemIds(allMetadata, seed);
// 4. Take the first 50 items
const pageIds = sortedIds.slice(0, 50);
// 5. Fetch full data for these items
const itemsRaw = await prisma.mediaItem.findMany({
where: { id: { in: pageIds } },
include: { _count: { select: { likes: true } } },
});
let nextCursor: number | null = null;
if (items.length > 50) {
const extra = items.pop();
nextCursor = extra!.id;
// 6. Re-order itemsRaw to match the sortedIds array exactly
const itemsMap = new Map(itemsRaw.map(item => [item.id, item]));
const items = pageIds.map(id => itemsMap.get(id)).filter(Boolean);
let nextCursor: string | null = null;
if (sortedIds.length > 50) {
nextCursor = `${seed}_2`; // next is page 2
}
// Fetch voting status for the client
+2 -2
View File
@@ -23,7 +23,7 @@ type MediaItem = {
type MediaWallProps = {
initialItems: MediaItem[];
initialCursor: number | null;
initialCursor: string | number | null;
initialViewerIndex?: number;
initialVotingOpen?: boolean;
directories?: string[];
@@ -37,7 +37,7 @@ export default function MediaWall({
directories = [],
}: MediaWallProps) {
const [items, setItems] = useState<MediaItem[]>(initialItems);
const [cursor, setCursor] = useState<number | null>(initialCursor);
const [cursor, setCursor] = useState<string | number | null>(initialCursor);
const [loading, setLoading] = useState(false);
const [hasMore, setHasMore] = useState(!!initialCursor);
const [votingOpen, setVotingOpen] = useState(initialVotingOpen);
+50
View File
@@ -0,0 +1,50 @@
export function mulberry32(a: number) {
return function() {
a |= 0;
a = (a + 0x6D2B79F5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
}
}
export function getShuffledItemIds(metadata: any[], seed: number) {
// Separate fixed layout items from normal items
const fixedItems = metadata.filter(m => m.featured || m.placementMode === 'FIXED_POSITION' || m.placementMode === 'FIXED_POSITION_ADJUSTABLE' || m.sortOrder > 0);
const normalItems = metadata.filter(m => !(m.featured || m.placementMode === 'FIXED_POSITION' || m.placementMode === 'FIXED_POSITION_ADJUSTABLE' || m.sortOrder > 0));
// Sort fixed items as before (strict DB order)
fixedItems.sort((a, b) => {
if (a.featured !== b.featured) return a.featured ? -1 : 1;
if (a.sortOrder !== b.sortOrder) return a.sortOrder - b.sortOrder;
return b.id - a.id; // Fallback to newest first
});
// Sort normal items by likes DESC, then newest first
normalItems.sort((a, b) => {
const likesA = a._count?.likes || 0;
const likesB = b._count?.likes || 0;
if (likesA !== likesB) return likesB - likesA;
return b.id - a.id;
});
// Chunk normal items into 10% blocks
const chunkSize = Math.max(1, Math.ceil(normalItems.length * 0.1));
const chunks: any[][] = [];
for (let i = 0; i < normalItems.length; i += chunkSize) {
chunks.push(normalItems.slice(i, i + chunkSize));
}
// Shuffle each chunk using the seed
const prng = mulberry32(seed);
chunks.forEach(chunk => {
for (let i = chunk.length - 1; i > 0; i--) {
const j = Math.floor(prng() * (i + 1));
[chunk[i], chunk[j]] = [chunk[j], chunk[i]];
}
});
// Combine and return IDs
const combined = [...fixedItems, ...chunks.flat()];
return combined.map(m => m.id);
}
+1 -1
View File
@@ -1 +1 @@
{"program":{"fileNames":["./node_modules/typescript/lib/lib.es5.d.ts","./node_modules/typescript/lib/lib.es2015.d.ts","./node_modules/typescript/lib/lib.es2016.d.ts","./node_modules/typescript/lib/lib.es2017.d.ts","./node_modules/typescript/lib/lib.es2018.d.ts","./node_modules/typescript/lib/lib.es2019.d.ts","./node_modules/typescript/lib/lib.es2020.d.ts","./node_modules/typescript/lib/lib.es2021.d.ts","./node_modules/typescript/lib/lib.es2022.d.ts","./node_modules/typescript/lib/lib.es2023.d.ts","./node_modules/typescript/lib/lib.esnext.d.ts","./node_modules/typescript/lib/lib.dom.d.ts","./node_modules/typescript/lib/lib.dom.iterable.d.ts","./node_modules/typescript/lib/lib.es2015.core.d.ts","./node_modules/typescript/lib/lib.es2015.collection.d.ts","./node_modules/typescript/lib/lib.es2015.generator.d.ts","./node_modules/typescript/lib/lib.es2015.iterable.d.ts","./node_modules/typescript/lib/lib.es2015.promise.d.ts","./node_modules/typescript/lib/lib.es2015.proxy.d.ts","./node_modules/typescript/lib/lib.es2015.reflect.d.ts","./node_modules/typescript/lib/lib.es2015.symbol.d.ts","./node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","./node_modules/typescript/lib/lib.es2016.array.include.d.ts","./node_modules/typescript/lib/lib.es2016.intl.d.ts","./node_modules/typescript/lib/lib.es2017.date.d.ts","./node_modules/typescript/lib/lib.es2017.object.d.ts","./node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2017.string.d.ts","./node_modules/typescript/lib/lib.es2017.intl.d.ts","./node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","./node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","./node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","./node_modules/typescript/lib/lib.es2018.intl.d.ts","./node_modules/typescript/lib/lib.es2018.promise.d.ts","./node_modules/typescript/lib/lib.es2018.regexp.d.ts","./node_modules/typescript/lib/lib.es2019.array.d.ts","./node_modules/typescript/lib/lib.es2019.object.d.ts","./node_modules/typescript/lib/lib.es2019.string.d.ts","./node_modules/typescript/lib/lib.es2019.symbol.d.ts","./node_modules/typescript/lib/lib.es2019.intl.d.ts","./node_modules/typescript/lib/lib.es2020.bigint.d.ts","./node_modules/typescript/lib/lib.es2020.date.d.ts","./node_modules/typescript/lib/lib.es2020.promise.d.ts","./node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2020.string.d.ts","./node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","./node_modules/typescript/lib/lib.es2020.intl.d.ts","./node_modules/typescript/lib/lib.es2020.number.d.ts","./node_modules/typescript/lib/lib.es2021.promise.d.ts","./node_modules/typescript/lib/lib.es2021.string.d.ts","./node_modules/typescript/lib/lib.es2021.weakref.d.ts","./node_modules/typescript/lib/lib.es2021.intl.d.ts","./node_modules/typescript/lib/lib.es2022.array.d.ts","./node_modules/typescript/lib/lib.es2022.error.d.ts","./node_modules/typescript/lib/lib.es2022.intl.d.ts","./node_modules/typescript/lib/lib.es2022.object.d.ts","./node_modules/typescript/lib/lib.es2022.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2022.string.d.ts","./node_modules/typescript/lib/lib.es2022.regexp.d.ts","./node_modules/typescript/lib/lib.es2023.array.d.ts","./node_modules/typescript/lib/lib.es2023.collection.d.ts","./node_modules/typescript/lib/lib.es2023.intl.d.ts","./node_modules/typescript/lib/lib.esnext.array.d.ts","./node_modules/typescript/lib/lib.esnext.collection.d.ts","./node_modules/typescript/lib/lib.esnext.intl.d.ts","./node_modules/typescript/lib/lib.esnext.disposable.d.ts","./node_modules/typescript/lib/lib.esnext.string.d.ts","./node_modules/typescript/lib/lib.esnext.promise.d.ts","./node_modules/typescript/lib/lib.esnext.decorators.d.ts","./node_modules/typescript/lib/lib.esnext.object.d.ts","./node_modules/typescript/lib/lib.esnext.regexp.d.ts","./node_modules/typescript/lib/lib.decorators.d.ts","./node_modules/typescript/lib/lib.decorators.legacy.d.ts","./node_modules/bullmq/dist/esm/classes/async-fifo-queue.d.ts","./node_modules/bullmq/dist/esm/interfaces/parent.d.ts","./node_modules/bullmq/dist/esm/interfaces/job-json.d.ts","./node_modules/bullmq/dist/esm/interfaces/minimal-job.d.ts","./node_modules/bullmq/dist/esm/types/backoff-strategy.d.ts","./node_modules/bullmq/dist/esm/types/finished-status.d.ts","./node_modules/@types/node/ts5.6/globals.typedarray.d.ts","./node_modules/@types/node/ts5.6/buffer.buffer.d.ts","./node_modules/@types/node/assert.d.ts","./node_modules/@types/node/assert/strict.d.ts","./node_modules/buffer/index.d.ts","./node_modules/undici-types/header.d.ts","./node_modules/undici-types/readable.d.ts","./node_modules/undici-types/file.d.ts","./node_modules/undici-types/fetch.d.ts","./node_modules/undici-types/formdata.d.ts","./node_modules/undici-types/connector.d.ts","./node_modules/undici-types/client.d.ts","./node_modules/undici-types/errors.d.ts","./node_modules/undici-types/dispatcher.d.ts",Line truncated
{"program":{"fileNames":["./node_modules/typescript/lib/lib.es5.d.ts","./node_modules/typescript/lib/lib.es2015.d.ts","./node_modules/typescript/lib/lib.es2016.d.ts","./node_modules/typescript/lib/lib.es2017.d.ts","./node_modules/typescript/lib/lib.es2018.d.ts","./node_modules/typescript/lib/lib.es2019.d.ts","./node_modules/typescript/lib/lib.es2020.d.ts","./node_modules/typescript/lib/lib.es2021.d.ts","./node_modules/typescript/lib/lib.es2022.d.ts","./node_modules/typescript/lib/lib.es2023.d.ts","./node_modules/typescript/lib/lib.esnext.d.ts","./node_modules/typescript/lib/lib.dom.d.ts","./node_modules/typescript/lib/lib.dom.iterable.d.ts","./node_modules/typescript/lib/lib.es2015.core.d.ts","./node_modules/typescript/lib/lib.es2015.collection.d.ts","./node_modules/typescript/lib/lib.es2015.generator.d.ts","./node_modules/typescript/lib/lib.es2015.iterable.d.ts","./node_modules/typescript/lib/lib.es2015.promise.d.ts","./node_modules/typescript/lib/lib.es2015.proxy.d.ts","./node_modules/typescript/lib/lib.es2015.reflect.d.ts","./node_modules/typescript/lib/lib.es2015.symbol.d.ts","./node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","./node_modules/typescript/lib/lib.es2016.array.include.d.ts","./node_modules/typescript/lib/lib.es2016.intl.d.ts","./node_modules/typescript/lib/lib.es2017.date.d.ts","./node_modules/typescript/lib/lib.es2017.object.d.ts","./node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2017.string.d.ts","./node_modules/typescript/lib/lib.es2017.intl.d.ts","./node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","./node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","./node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","./node_modules/typescript/lib/lib.es2018.intl.d.ts","./node_modules/typescript/lib/lib.es2018.promise.d.ts","./node_modules/typescript/lib/lib.es2018.regexp.d.ts","./node_modules/typescript/lib/lib.es2019.array.d.ts","./node_modules/typescript/lib/lib.es2019.object.d.ts","./node_modules/typescript/lib/lib.es2019.string.d.ts","./node_modules/typescript/lib/lib.es2019.symbol.d.ts","./node_modules/typescript/lib/lib.es2019.intl.d.ts","./node_modules/typescript/lib/lib.es2020.bigint.d.ts","./node_modules/typescript/lib/lib.es2020.date.d.ts","./node_modules/typescript/lib/lib.es2020.promise.d.ts","./node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2020.string.d.ts","./node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","./node_modules/typescript/lib/lib.es2020.intl.d.ts","./node_modules/typescript/lib/lib.es2020.number.d.ts","./node_modules/typescript/lib/lib.es2021.promise.d.ts","./node_modules/typescript/lib/lib.es2021.string.d.ts","./node_modules/typescript/lib/lib.es2021.weakref.d.ts","./node_modules/typescript/lib/lib.es2021.intl.d.ts","./node_modules/typescript/lib/lib.es2022.array.d.ts","./node_modules/typescript/lib/lib.es2022.error.d.ts","./node_modules/typescript/lib/lib.es2022.intl.d.ts","./node_modules/typescript/lib/lib.es2022.object.d.ts","./node_modules/typescript/lib/lib.es2022.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2022.string.d.ts","./node_modules/typescript/lib/lib.es2022.regexp.d.ts","./node_modules/typescript/lib/lib.es2023.array.d.ts","./node_modules/typescript/lib/lib.es2023.collection.d.ts","./node_modules/typescript/lib/lib.es2023.intl.d.ts","./node_modules/typescript/lib/lib.esnext.array.d.ts","./node_modules/typescript/lib/lib.esnext.collection.d.ts","./node_modules/typescript/lib/lib.esnext.intl.d.ts","./node_modules/typescript/lib/lib.esnext.disposable.d.ts","./node_modules/typescript/lib/lib.esnext.string.d.ts","./node_modules/typescript/lib/lib.esnext.promise.d.ts","./node_modules/typescript/lib/lib.esnext.decorators.d.ts","./node_modules/typescript/lib/lib.esnext.object.d.ts","./node_modules/typescript/lib/lib.esnext.regexp.d.ts","./node_modules/typescript/lib/lib.decorators.d.ts","./node_modules/typescript/lib/lib.decorators.legacy.d.ts","./node_modules/next/dist/styled-jsx/types/css.d.ts","./node_modules/@types/react/global.d.ts","./node_modules/csstype/index.d.ts","./node_modules/@types/prop-types/index.d.ts","./node_modules/@types/react/index.d.ts","./node_modules/next/dist/styled-jsx/types/index.d.ts","./node_modules/next/dist/styled-jsx/types/macro.d.ts","./node_modules/next/dist/styled-jsx/types/style.d.ts","./node_modules/next/dist/styled-jsx/types/global.d.ts","./node_modules/next/dist/shared/lib/amp.d.ts","./node_modules/next/amp.d.ts","./node_modules/@types/node/ts5.6/globals.typedarray.d.ts","./node_modules/@types/node/ts5.6/buffer.buffer.d.ts","./node_modules/@types/node/assert.d.ts","./node_modules/@types/node/assert/strict.d.ts","./node_modules/buffer/index.d.ts","./node_modules/undici-types/header.d.ts","./node_modules/undici-types/readable.d.ts","./node_modules/undici-types/file.d.ts","./node_modules/undici-types/fetch.d.ts","./node_modules/undici-types/formdata.d.ts","./node_modules/uLine truncated