Improve landscape screens for scroll

This commit is contained in:
Jordan Eldredge 2025-11-07 17:28:21 -08:00
parent f3054192e6
commit 608242b200
5 changed files with 210 additions and 4 deletions

View file

@ -27,7 +27,7 @@ export async function logUserEvent(sessionId: string, event: UserEvent) {
}
}
type UserEvent =
export type UserEvent =
| {
type: "session_start";
}
@ -35,6 +35,13 @@ type UserEvent =
type: "session_end";
reason: "unmount" | "before_unload";
}
/**
* @deprecated
*/
| {
type: "skin_view";
skinMd5: string;
}
| {
type: "skin_view_start";
skinMd5: string;

View file

@ -138,9 +138,16 @@ export default function SkinScroller({
ref={setContainerRef}
style={{
height: "100vh",
width: "100vw",
maxWidth: "56.25vh", // 9:16 aspect ratio (100vh * 9/16)
margin: "0 auto",
overflowY: "scroll",
scrollSnapType: "y mandatory",
scrollbarWidth: "none", // Firefox
msOverflowStyle: "none", // IE and Edge
WebkitOverflowScrolling: "touch", // Smooth scrolling on iOS
}}
className="hide-scrollbar"
>
{skins.map((skin, i) => {
return (

View file

@ -1,14 +1,16 @@
body {
margin: 0; /* Remove default margin */
height: 100vh; /* Set body height to viewport height */
background-color: rgb(0, 0, 0);
background-color: #1a1a1a; /* Dark charcoal instead of pure black */
}
.scroller::-webkit-scrollbar {
.scroller::-webkit-scrollbar,
.hide-scrollbar::-webkit-scrollbar {
display: none; /* Chrome, Safari, Edge */
}
.scroller {
.scroller,
.hide-scrollbar {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}

View file

@ -36,6 +36,7 @@ import * as S3 from "./s3";
import { generateDescription } from "./services/openAi";
import KeyValue from "./data/KeyValue";
import { postToBluesky } from "./tasks/bluesky";
import { computeSkinRankings } from "./tasks/computeScrollRanking";
async function withHandler(
cb: (handler: DiscordEventHandler) => Promise<void>
@ -308,6 +309,14 @@ program
console.table([await Skins.getStats()]);
});
program
.command("compute-scroll-ranking")
.description("Analyze user event data and compute skin ranking scores.")
.action(async () => {
const rankings = await computeSkinRankings();
console.log(JSON.stringify(rankings, null, 2));
});
program
.command("process-uploads")
.description("Process any unprocessed user uploads.")

View file

@ -0,0 +1,181 @@
import { knex } from "../db";
import type { UserEvent } from "../app/(modern)/scroll/Events";
const WEIGHTS = {
viewDurationPerSecond: 0.1,
like: 10,
download: 5,
share: 15,
readmeExpand: 2,
};
interface SessionAggregate {
skinViewDurations: Map<string, number>;
skinsLiked: Set<string>;
skinsDownloaded: Set<string>;
readmesExpanded: Set<string>;
sharesSucceeded: Set<string>;
}
interface SkinRanking {
skinMd5: string;
totalViewDurationMs: number;
viewCount: number;
averageViewDurationMs: number;
likeCount: number;
downloadCount: number;
shareCount: number;
readmeExpandCount: number;
rankingScore: number;
}
async function main() {
try {
const rankings = await computeSkinRankings();
console.log(JSON.stringify(rankings, null, 2));
} catch (error) {
console.error("Error during aggregation:", error);
throw error;
} finally {
await knex.destroy();
}
}
if (require.main === module) {
main();
}
export async function computeSkinRankings(): Promise<SkinRanking[]> {
const sessionMap = await buildSessionAggregates();
const skinDataMap = new Map<
string,
{
viewDurations: number[];
likes: number;
downloads: number;
shares: number;
readmeExpands: number;
}
>();
function getSkinData(skinMd5: string) {
if (!skinDataMap.has(skinMd5)) {
skinDataMap.set(skinMd5, {
viewDurations: [],
likes: 0,
downloads: 0,
shares: 0,
readmeExpands: 0,
});
}
return skinDataMap.get(skinMd5)!;
}
for (const session of sessionMap.values()) {
for (const [skinMd5, duration] of session.skinViewDurations) {
getSkinData(skinMd5).viewDurations.push(duration);
}
for (const skinMd5 of session.skinsLiked) {
getSkinData(skinMd5).likes++;
}
for (const skinMd5 of session.skinsDownloaded) {
getSkinData(skinMd5).downloads++;
}
for (const skinMd5 of session.sharesSucceeded) {
getSkinData(skinMd5).shares++;
}
for (const skinMd5 of session.readmesExpanded) {
getSkinData(skinMd5).readmeExpands++;
}
}
const rankings: SkinRanking[] = [];
for (const [skinMd5, data] of skinDataMap) {
const totalViewDurationMs = data.viewDurations.reduce(
(sum, duration) => sum + duration,
0
);
const viewCount = data.viewDurations.length;
const averageViewDurationMs =
viewCount > 0 ? totalViewDurationMs / viewCount : 0;
const rankingScore =
(averageViewDurationMs / 1000) * WEIGHTS.viewDurationPerSecond +
data.likes * WEIGHTS.like +
data.downloads * WEIGHTS.download +
data.shares * WEIGHTS.share +
data.readmeExpands * WEIGHTS.readmeExpand;
rankings.push({
skinMd5,
totalViewDurationMs,
viewCount,
averageViewDurationMs,
likeCount: data.likes,
downloadCount: data.downloads,
shareCount: data.shares,
readmeExpandCount: data.readmeExpands,
rankingScore,
});
}
rankings.sort((a, b) => b.rankingScore - a.rankingScore);
return rankings;
}
async function buildSessionAggregates(): Promise<
Map<string, SessionAggregate>
> {
const events = await knex("user_log_events")
.select("session_id", "timestamp", "metadata")
.orderBy("timestamp", "asc");
const sessionMap = new Map<string, SessionAggregate>();
function getSession(sessionId: string): SessionAggregate {
if (!sessionMap.has(sessionId)) {
sessionMap.set(sessionId, {
skinViewDurations: new Map(),
skinsLiked: new Set(),
skinsDownloaded: new Set(),
readmesExpanded: new Set(),
sharesSucceeded: new Set(),
});
}
return sessionMap.get(sessionId)!;
}
for (const row of events) {
const event: UserEvent = JSON.parse(row.metadata);
const session = getSession(row.session_id);
switch (event.type) {
case "skin_view_end":
session.skinViewDurations.set(event.skinMd5, event.durationMs);
break;
case "readme_expand":
session.readmesExpanded.add(event.skinMd5);
break;
case "skin_download":
session.skinsDownloaded.add(event.skinMd5);
break;
case "skin_like":
if (event.liked) {
session.skinsLiked.add(event.skinMd5);
} else {
session.skinsLiked.delete(event.skinMd5);
}
break;
case "share_success":
session.sharesSucceeded.add(event.skinMd5);
break;
}
}
return sessionMap;
}
export { buildSessionAggregates };
export type { SessionAggregate };