model/user thumbnails
This commit is contained in:
parent
e9f79241f1
commit
8a28d6cfcf
web
next.config.ts
src/app
@ -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: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -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>
|
@ -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;
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
@forward "./page/card.scss";
|
||||
@forward "../../_components/styles/mapCard.scss";
|
||||
|
||||
@use "../../globals.scss";
|
||||
|
||||
|
@ -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}
|
||||
/>
|
||||
))}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
@ -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;
|
||||
|
@ -40,8 +40,7 @@
|
||||
gap: 25px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 350px;
|
||||
height: 100%;
|
||||
object-fit: contain
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
28
web/src/app/submissions/[submissionId]/_mapImage.tsx
Normal file
28
web/src/app/submissions/[submissionId]/_mapImage.tsx
Normal file
@ -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 };
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
@ -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}
|
||||
/>
|
||||
))}
|
||||
|
69
web/src/app/thumbnails/asset/[assetId]/route.ts
Normal file
69
web/src/app/thumbnails/asset/[assetId]/route.ts
Normal file
@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
55
web/src/app/thumbnails/user/[userId]/route.ts
Normal file
55
web/src/app/thumbnails/user/[userId]/route.ts
Normal file
@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user