diff --git a/web/next.config.ts b/web/next.config.ts index 4d4174c..d68a004 100644 --- a/web/next.config.ts +++ b/web/next.config.ts @@ -6,11 +6,11 @@ const nextConfig: NextConfig = { images: { remotePatterns: [ { - protocol: 'https', - hostname: 'api.ic3.space', - pathname: '/strafe/map-images/**', - port: '', - search: '', + protocol: "https", + hostname: "tr.rbxcdn.com", + pathname: "/**", + port: "", + search: "", }, ], }, diff --git a/web/src/app/mapfixes/_card.tsx b/web/src/app/_components/mapCard.tsx similarity index 79% rename from web/src/app/mapfixes/_card.tsx rename to web/src/app/_components/mapCard.tsx index a1c594a..3e4269a 100644 --- a/web/src/app/mapfixes/_card.tsx +++ b/web/src/app/_components/mapCard.tsx @@ -6,8 +6,9 @@ import { Rating } from "@mui/material"; interface SubmissionCardProps { displayName: string; assetId: number; - rating: number; + authorId: number; author: string; + rating: number; id: number; } @@ -18,7 +19,7 @@ export default function SubmissionCard(props: SubmissionCardProps) { <div className="content"> <div className="map-image"> {/* TODO: Grab image of model */} - <Image height={200} width={200} priority={true} src="https://api.ic3.space/strafe/map-images/11222350808" style={{ width: `100%` }} alt={props.displayName} /> + <Image width={230} height={230} layout="fixed" priority={true} src={`/thumbnails/asset/${props.assetId}`} alt={props.displayName} /> </div> <div className="details"> <div className="header"> @@ -29,7 +30,7 @@ export default function SubmissionCard(props: SubmissionCardProps) { </div> <div className="footer"> <div className="author"> - <Image className="avatar" width={28} height={28} priority={true} src="https://api.ic3.space/strafe/map-images/11222350808" alt={props.author}/> + <Image className="avatar" width={28} height={28} priority={true} src={`/thumbnails/user/${props.authorId}`} alt={props.author}/> <span>{props.author}</span> </div> </div> diff --git a/web/src/app/mapfixes/(styles)/page/card.scss b/web/src/app/_components/styles/mapCard.scss similarity index 93% rename from web/src/app/mapfixes/(styles)/page/card.scss rename to web/src/app/_components/styles/mapCard.scss index dce43eb..cfcb9d2 100644 --- a/web/src/app/mapfixes/(styles)/page/card.scss +++ b/web/src/app/_components/styles/mapCard.scss @@ -1,4 +1,4 @@ -@use "../../../globals.scss"; +@use "../../globals.scss"; .submissionCard { display: flex; @@ -9,7 +9,7 @@ flex-direction: column; justify-content: space-between; height: 100%; - min-width: 180px; + min-width: 230px; max-width: 340px; } @@ -43,8 +43,6 @@ overflow: hidden; > img { - width: 100%; - height: 100%; object-fit: cover; } } diff --git a/web/src/app/mapfixes/(styles)/page.scss b/web/src/app/mapfixes/(styles)/page.scss index bf98951..daf65f9 100644 --- a/web/src/app/mapfixes/(styles)/page.scss +++ b/web/src/app/mapfixes/(styles)/page.scss @@ -1,4 +1,4 @@ -@forward "./page/card.scss"; +@forward "../../_components/styles/mapCard.scss"; @use "../../globals.scss"; diff --git a/web/src/app/mapfixes/page.tsx b/web/src/app/mapfixes/page.tsx index cf6eccb..743062d 100644 --- a/web/src/app/mapfixes/page.tsx +++ b/web/src/app/mapfixes/page.tsx @@ -2,9 +2,11 @@ import React, { useState, useEffect } from "react"; import { MapfixInfo } from "../ts/Mapfix"; -import MapfixCard from "./_card"; +import MapfixCard from "../_components/mapCard"; import Webpage from "@/app/_components/webpage"; +// TODO: MAKE MAPFIX & SUBMISSIONS USE THE SAME COMPONENTS :angry: (currently too lazy) + import "./(styles)/page.scss"; export default function MapfixInfoPage() { @@ -100,6 +102,7 @@ export default function MapfixInfoPage() { assetId={mapfix.AssetID} displayName={mapfix.DisplayName} author={mapfix.Creator} + authorId={mapfix.Submitter} rating={mapfix.StatusID} /> ))} diff --git a/web/src/app/submissions/(styles)/page.scss b/web/src/app/submissions/(styles)/page.scss index bf98951..d54fd38 100644 --- a/web/src/app/submissions/(styles)/page.scss +++ b/web/src/app/submissions/(styles)/page.scss @@ -1,4 +1,4 @@ -@forward "./page/card.scss"; +@forward "../../_components/styles/mapCard.scss"; @use "../../globals.scss"; @@ -27,7 +27,7 @@ a { @media (max-width: 768px) { .grid { - grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); } } diff --git a/web/src/app/submissions/(styles)/page/card.scss b/web/src/app/submissions/(styles)/page/card.scss deleted file mode 100644 index dce43eb..0000000 --- a/web/src/app/submissions/(styles)/page/card.scss +++ /dev/null @@ -1,87 +0,0 @@ -@use "../../../globals.scss"; - -.submissionCard { - display: flex; - background-color: #2020207c; - border: 1px solid #97979783; - border-radius: 10px; - box-sizing: border-box; - flex-direction: column; - justify-content: space-between; - height: 100%; - min-width: 180px; - max-width: 340px; -} - -.content { - display: flex; - flex-grow: 1; - flex-direction: column; - justify-content: space-between; -} - -.details { - padding: 2px 4px; -} - -.header { - display: flex; - justify-content: space-between; - align-items: center; - margin: 3px 0px; -} - -.footer { - display: flex; - justify-content: space-between; - align-items: center; - margin: 3px 0px; -} - -.map-image { - border-radius: 10px 10px 0 0; - overflow: hidden; - - > img { - width: 100%; - height: 100%; - object-fit: cover; - } -} - -.displayName { - font-size: 1rem; - font-weight: bold; - overflow: hidden; - max-width: 70%; - text-overflow: ellipsis; - white-space: nowrap; -} - -.rating { - flex-shrink: 0; -} - -.author { - display: flex; - align-items: center; - gap: 0.5rem; - flex-grow: 1; - min-width: 0; -} - -.author span { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.rating { - margin-left: auto; - flex-shrink: 0; -} - -.avatar { - border-radius: 50%; - object-fit: cover; -} \ No newline at end of file diff --git a/web/src/app/submissions/[submissionId]/(styles)/page/map.scss b/web/src/app/submissions/[submissionId]/(styles)/page/map.scss index ede388e..12469f0 100644 --- a/web/src/app/submissions/[submissionId]/(styles)/page/map.scss +++ b/web/src/app/submissions/[submissionId]/(styles)/page/map.scss @@ -5,8 +5,12 @@ display: flex; justify-content: center; align-items: center; - width: 350px; - height: 350px; + width: 100%; + height: auto; + margin-left: auto; + margin-right: auto; + border-radius: 12px; + overflow: hidden; > p { text-align: center; diff --git a/web/src/app/submissions/[submissionId]/(styles)/page/review.scss b/web/src/app/submissions/[submissionId]/(styles)/page/review.scss index 08fc5c0..2ca4539 100644 --- a/web/src/app/submissions/[submissionId]/(styles)/page/review.scss +++ b/web/src/app/submissions/[submissionId]/(styles)/page/review.scss @@ -40,8 +40,7 @@ gap: 25px; img { - width: 100%; - height: 350px; + height: 100%; object-fit: contain } } \ No newline at end of file diff --git a/web/src/app/submissions/[submissionId]/_map.tsx b/web/src/app/submissions/[submissionId]/_map.tsx deleted file mode 100644 index e364f70..0000000 --- a/web/src/app/submissions/[submissionId]/_map.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { SubmissionInfo } from "@/app/ts/Submission" - -interface AssetID { - id: SubmissionInfo["AssetID"] -} - -function MapImage() { - return <p>Fetching map image...</p> -} - -export { - type AssetID, - MapImage -} \ No newline at end of file diff --git a/web/src/app/submissions/[submissionId]/_mapImage.tsx b/web/src/app/submissions/[submissionId]/_mapImage.tsx new file mode 100644 index 0000000..96f9059 --- /dev/null +++ b/web/src/app/submissions/[submissionId]/_mapImage.tsx @@ -0,0 +1,28 @@ +import Image from "next/image"; +import { SubmissionInfo } from "@/app/ts/Submission"; + +interface AssetID { + id: SubmissionInfo["AssetID"]; +} + +function MapImage({ id }: AssetID) { + if (!id) { + return <p>Missing asset ID</p>; + } + + const imageUrl = `/thumbnails/asset/${id}`; + + return ( + <Image + src={imageUrl} + alt="Map Thumbnail" + layout="responsive" + width={512} + height={512} + priority={true} + className="map-image" + /> + ); +} + +export { type AssetID, MapImage }; diff --git a/web/src/app/submissions/[submissionId]/page.tsx b/web/src/app/submissions/[submissionId]/page.tsx index 9dae81f..c78ba8b 100644 --- a/web/src/app/submissions/[submissionId]/page.tsx +++ b/web/src/app/submissions/[submissionId]/page.tsx @@ -2,7 +2,7 @@ import { SubmissionInfo, SubmissionStatusToString } from "@/app/ts/Submission"; import type { CreatorAndReviewStatus } from "./_comments"; -import { MapImage } from "./_map"; +import { MapImage } from "./_mapImage"; import { useParams } from "next/navigation"; import ReviewButtons from "./_reviewButtons"; import { Rating } from "@mui/material"; @@ -15,7 +15,8 @@ import { useState, useEffect } from "react"; import "./(styles)/page.scss"; interface ReviewId { - submissionId: string + submissionId: string; + assetId: number; } function Ratings() { @@ -43,7 +44,7 @@ function RatingArea(submission: ReviewId) { return ( <aside className="review-area"> <section className="map-image-area"> - <MapImage/> + <MapImage id={submission.assetId}/> </section> <Ratings/> <ReviewButtons submissionId={submission.submissionId}/> @@ -96,7 +97,7 @@ export default function SubmissionInfoPage() { <Webpage> <main className="map-page-main"> <section className="review-section"> - <RatingArea submissionId={dynamicId.submissionId}/> + <RatingArea assetId={submission.AssetID} submissionId={dynamicId.submissionId}/> <TitleAndComments name={submission.DisplayName} creator={submission.Creator} review={submission.StatusID} status_message={submission.StatusMessage} asset_id={submission.AssetID} comments={[]}/> </section> </main> diff --git a/web/src/app/submissions/_card.tsx b/web/src/app/submissions/_card.tsx deleted file mode 100644 index 29a6793..0000000 --- a/web/src/app/submissions/_card.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React from "react"; -import Image from "next/image"; -import Link from "next/link"; -import { Rating } from "@mui/material"; - -interface SubmissionCardProps { - displayName: string; - assetId: number; - rating: number; - author: string; - id: number; -} - -export default function SubmissionCard(props: SubmissionCardProps) { - return ( - <Link href={`/submissions/${props.id}`}> - <div className="submissionCard"> - <div className="content"> - <div className="map-image"> - {/* TODO: Grab image of model */} - <Image height={200} width={200} priority={true} src="https://api.ic3.space/strafe/map-images/11222350808" style={{ width: `100%` }} alt={props.displayName} /> - </div> - <div className="details"> - <div className="header"> - <span className="displayName">{props.displayName}</span> - <div className="rating"> - <Rating value={props.rating} readOnly size="small" /> - </div> - </div> - <div className="footer"> - <div className="author"> - <Image className="avatar" width={28} height={28} priority={true} src="https://api.ic3.space/strafe/map-images/11222350808" alt={props.author}/> - <span>{props.author}</span> - </div> - </div> - </div> - </div> - </div> - </Link> - ); -} \ No newline at end of file diff --git a/web/src/app/submissions/page.tsx b/web/src/app/submissions/page.tsx index 2b803a7..05d3ce7 100644 --- a/web/src/app/submissions/page.tsx +++ b/web/src/app/submissions/page.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect } from "react"; import { SubmissionInfo } from "../ts/Submission"; -import SubmissionCard from "./_card"; +import SubmissionCard from "../_components/mapCard"; import Webpage from "@/app/_components/webpage"; import "./(styles)/page.scss"; @@ -100,6 +100,7 @@ export default function SubmissionInfoPage() { assetId={submission.AssetID} displayName={submission.DisplayName} author={submission.Creator} + authorId={submission.Submitter} rating={submission.StatusID} /> ))} diff --git a/web/src/app/thumbnails/asset/[assetId]/route.ts b/web/src/app/thumbnails/asset/[assetId]/route.ts new file mode 100644 index 0000000..b7992f4 --- /dev/null +++ b/web/src/app/thumbnails/asset/[assetId]/route.ts @@ -0,0 +1,69 @@ +import { NextRequest, NextResponse } from 'next/server'; + +export async function GET( + request: NextRequest, + context: { params: Promise<{ assetId: number }> } +): Promise<NextResponse> { + const { assetId } = await context.params; + + if (!assetId) { + return NextResponse.json( + { error: 'Missing asset ID' }, + { status: 400 } + ); + } + + let finalAssetId = assetId; + + try { + const mediaResponse = await fetch( + `https://publish.roblox.com/v1/assets/${assetId}/media` + ); + if (mediaResponse.ok) { + const mediaData = await mediaResponse.json(); + if (mediaData.data && mediaData.data.length > 0) { + finalAssetId = mediaData.data[0].toString(); + } + } + } catch { } + + try { + const response = await fetch( + `https://thumbnails.roblox.com/v1/assets?format=png&size=512x512&assetIds=${finalAssetId}` + ); + + if (!response.ok) { + throw new Error('Failed to fetch thumbnail JSON'); + } + + const data = await response.json(); + + const imageUrl = data.data[0]?.imageUrl; + if (!imageUrl) { + return NextResponse.json( + { error: 'No image URL found in the response' }, + { status: 404 } + ); + } + + const imageResponse = await fetch(imageUrl); + if (!imageResponse.ok) { + throw new Error('Failed to fetch the image'); + } + + const arrayBuffer = await imageResponse.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + + return new NextResponse(buffer, { + headers: { + 'Content-Type': 'image/png', + 'Content-Length': buffer.length.toString(), + }, + }); + } catch { + return NextResponse.json( + { error: 'Failed to fetch or process thumbnail' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/web/src/app/thumbnails/user/[userId]/route.ts b/web/src/app/thumbnails/user/[userId]/route.ts new file mode 100644 index 0000000..47bef3c --- /dev/null +++ b/web/src/app/thumbnails/user/[userId]/route.ts @@ -0,0 +1,55 @@ +import { NextRequest, NextResponse } from 'next/server'; + +export async function GET( + request: NextRequest, + context: { params: Promise<{ userId: number }> } +): Promise<NextResponse> { + const { userId } = await context.params; // Await params to access userId + + if (!userId) { + return NextResponse.json( + { error: 'Missing userId parameter' }, + { status: 400 } + ); + } + + try { + const response = await fetch( + `https://thumbnails.roblox.com/v1/users/avatar-headshot?userIds=${userId}&size=420x420&format=Png&isCircular=false` + ); + + if (!response.ok) { + throw new Error('Failed to fetch avatar headshot JSON'); + } + + const data = await response.json(); + + const imageUrl = data.data[0]?.imageUrl; + if (!imageUrl) { + return NextResponse.json( + { error: 'No image URL found in the response' }, + { status: 404 } + ); + } + + const imageResponse = await fetch(imageUrl); + if (!imageResponse.ok) { + throw new Error('Failed to fetch the image'); + } + + const arrayBuffer = await imageResponse.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + + return new NextResponse(buffer, { + headers: { + 'Content-Type': 'image/png', + 'Content-Length': buffer.length.toString(), + }, + }); + } catch { + return NextResponse.json( + { error: 'Failed to fetch or process avatar headshot' }, + { status: 500 } + ); + } +}