This commit is contained in:
2025-02-14 11:59:11 +02:00
parent e6b898ef09
commit 9ddf068049
73 changed files with 5767 additions and 126 deletions

View File

@@ -0,0 +1,52 @@
import { type NextRequest, NextResponse } from "next/server"
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const apiUrl = "https://apis.roblox.com/discovery-api/omni-recommendation";
const url = `${apiUrl}?${searchParams.toString()}`;
try {
const response = await fetch(url, {
headers: {
"User-Agent": "OCbwoy3ChanAI/1.0",
"Accept": "application/json, text/plain, */*",
"Accept-Encoding": "gzip, deflate, br, zstd",
"Content-Type": "application/json;charset=UTF-8",
"Cookie": request.headers.get("Authorization") || "",
},
});
return NextResponse.json(await response.json());
} catch (error) {
console.error("Error proxying request:", error);
return NextResponse.json({ error: "Internal Server Error" });
}
}
export async function POST(request: NextRequest) {
const { searchParams } = new URL(request.url);
const apiUrl = "https://apis.roblox.com/discovery-api/omni-recommendation";
const url = `${apiUrl}?${searchParams.toString()}`;
try {
const body = await request.json()
const response = await fetch(url, {
method: "POST",
headers: {
"User-Agent": "OCbwoy3ChanAI/1.0",
"Accept": "application/json, text/plain, */*",
"Accept-Encoding": "gzip, deflate, br, zstd",
"Content-Type": "application/json;charset=UTF-8",
"Cookie": request.headers.get("Authorization") || "",
},
body: JSON.stringify(body),
});
return NextResponse.json(await response.json());
} catch (error) {
console.error("Error proxying request:", error);
return NextResponse.json({ error: "Internal Server Error" });
}
}

View File

@@ -0,0 +1,52 @@
import { type NextRequest, NextResponse } from "next/server"
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const apiUrl = "https://thumbnails.roblox.com/v1/batch";
const url = `${apiUrl}?${searchParams.toString()}`;
try {
const response = await fetch(url, {
headers: {
"User-Agent": "OCbwoy3ChanAI/1.0",
"Accept": "application/json, text/plain, */*",
"Accept-Encoding": "gzip, deflate, br, zstd",
"Content-Type": "application/json;charset=UTF-8",
"Cookie": request.headers.get("Authorization") || "",
},
});
return NextResponse.json(await response.json());
} catch (error) {
console.error("Error proxying request:", error);
return NextResponse.json({ error: "Internal Server Error" });
}
}
export async function POST(request: NextRequest) {
const { searchParams } = new URL(request.url);
const apiUrl = "https://thumbnails.roblox.com/v1/batch";
const url = `${apiUrl}?${searchParams.toString()}`;
try {
const body = await request.json()
const response = await fetch(url, {
method: "POST",
headers: {
"User-Agent": "OCbwoy3ChanAI/1.0",
"Accept": "application/json, text/plain, */*",
"Accept-Encoding": "gzip, deflate, br, zstd",
"Content-Type": "application/json;charset=UTF-8",
"Cookie": request.headers.get("Authorization") || "",
},
body: JSON.stringify(body),
});
return NextResponse.json(await response.json());
} catch (error) {
console.error("Error proxying request:", error);
return NextResponse.json({ error: "Internal Server Error" });
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -2,20 +2,98 @@
@tailwind components;
@tailwind utilities;
:root {
--background: #ffffff;
--foreground: #171717;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
color: var(--foreground);
background: var(--background);
font-family: Arial, Helvetica, sans-serif;
font-family: Arial, Helvetica, sans-serif;
}
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--ring: 0 0% 3.9%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--radius: 0.5rem;
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
.dark {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 0 0% 83.1%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
/* https://dev.to/derick1530/how-to-create-scrollable-element-in-tailwind-without-a-scrollbar-4mbd */
@layer utilities {
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: 0;
}
}

View File

@@ -1,101 +1,78 @@
import Image from "next/image";
"use client";
import { GameCard } from "@/components/gameCard";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
getOmniRecommendationsHome,
OmniRecommendation,
} from "@/lib/omniRecommendation";
import { loadThumbnails, ThumbnailRequest } from "@/lib/thumbnailLoader";
import { useEffect, useState } from "react";
export default function Home() {
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol className="list-inside list-decimal text-sm text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
<li className="mb-2">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-semibold">
app/page.tsx
</code>
.
</li>
<li>Save and see your changes instantly.</li>
</ol>
const [rec, setRec] = useState<OmniRecommendation | null>(null);
const SORTS_ALLOWED_IDS = [100000003, 100000001];
useEffect(() => {
(async () => {
const r = await getOmniRecommendationsHome();
console.log("[ROBLOX]", "got omni recommendation from api", r);
let th: ThumbnailRequest[] = [];
r.sorts.filter(a=>SORTS_ALLOWED_IDS.includes(a.topicId)).forEach(b=>{
(b.recommendationList || []).forEach(c=>{
th.push({
type: "GameThumbnail",
targetId: r.contentMetadata.Game[c.contentId.toString()].rootPlaceId,
format: "webp",
size: "384x216"
})
})
});
loadThumbnails(th).catch(a=>console.error(a))
setRec(r);
})();
}, []);
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-44"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
</div>
</main>
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org
</a>
</footer>
</div>
);
if (!rec) {
return (
<div className="p-4">
<Card>
<CardContent className="p-4">
<div className="h-[200px] flex items-center justify-center">
<div className="animate-pulse text-muted-foreground">
Loading...
</div>
</div>
</CardContent>
</Card>
</div>
);
}
return (
<div className="overflow-scroll no-scrollbar w-screen max-h-screen h-screen">
{"roblox x next.js when"}
<br/>
{"experimental (functional) roblox.com clone"}
<div className="p-4 space-y-8 no-scrollbar">
{rec.sorts
.filter((a) => SORTS_ALLOWED_IDS.includes(a.topicId))
.map((sort, idx) => (
<div key={idx}>
<h1 className="text-2xl">{sort.topic}</h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{(sort.recommendationList || []).map(
(recommendation, idxb) => {
const game =
rec.contentMetadata.Game[
recommendation.contentId.toString()
];
return <GameCard key={idxb} game={game} />;
}
)}
</div>
</div>
))}
</div>
</div>
);
}