feat: randomized layout in 10% chunks based on likes
This commit is contained in:
1 parent
21c1166fba
commit
f53e27d3ae
5 files changed
+156
-37
No files matched your search
+71
-23
@@ -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,31 +31,77 @@ export async function GET(request: NextRequest) {
|
||||
where.directory = directoryParam;
|
||||
}
|
||||
|
||||
const cursor =
|
||||
cursorParam && !isNaN(parseInt(cursorParam, 10))
|
||||
? parseInt(cursorParam, 10)
|
||||
: undefined;
|
||||
let itemsRaw: any[] = [];
|
||||
let items: any[] = [];
|
||||
let nextCursor: string | number | null = null;
|
||||
|
||||
const items = await prisma.mediaItem.findMany({
|
||||
where,
|
||||
take: limit + 1,
|
||||
...(cursor ? { skip: 1, cursor: { id: cursor } } : {}),
|
||||
orderBy: [
|
||||
{ featured: 'desc' },
|
||||
{ sortOrder: 'asc' },
|
||||
{ createdAt: 'desc' },
|
||||
],
|
||||
include: {
|
||||
_count: {
|
||||
select: { likes: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
if (isAdminRequest) {
|
||||
// Standard cursor pagination for Admin
|
||||
const cursor =
|
||||
cursorParam && !isNaN(parseInt(cursorParam, 10))
|
||||
? parseInt(cursorParam, 10)
|
||||
: undefined;
|
||||
|
||||
let nextCursor: number | null = null;
|
||||
if (items.length > limit) {
|
||||
const nextItem = items.pop();
|
||||
nextCursor = nextItem!.id;
|
||||
items = await prisma.mediaItem.findMany({
|
||||
where,
|
||||
take: limit + 1,
|
||||
...(cursor ? { skip: 1, cursor: { id: cursor } } : {}),
|
||||
orderBy: [
|
||||
{ featured: 'desc' },
|
||||
{ sortOrder: 'asc' },
|
||||
{ createdAt: 'desc' },
|
||||
],
|
||||
include: { _count: { select: { likes: true } } },
|
||||
});
|
||||
|
||||
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
|
||||
|
||||
+32
-11
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 @@
|
||||
{"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
|
||||
Reference in new issue
Block a user