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: {
|
images: {
|
||||||
remotePatterns: [
|
remotePatterns: [
|
||||||
{
|
{
|
||||||
protocol: 'https',
|
protocol: "https",
|
||||||
hostname: 'api.ic3.space',
|
hostname: "tr.rbxcdn.com",
|
||||||
pathname: '/strafe/map-images/**',
|
pathname: "/**",
|
||||||
port: '',
|
port: "",
|
||||||
search: '',
|
search: "",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -6,8 +6,9 @@ import { Rating } from "@mui/material";
|
|||||||
interface SubmissionCardProps {
|
interface SubmissionCardProps {
|
||||||
displayName: string;
|
displayName: string;
|
||||||
assetId: number;
|
assetId: number;
|
||||||
rating: number;
|
authorId: number;
|
||||||
author: string;
|
author: string;
|
||||||
|
rating: number;
|
||||||
id: number;
|
id: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -18,7 +19,7 @@ export default function SubmissionCard(props: SubmissionCardProps) {
|
|||||||
<div className="content">
|
<div className="content">
|
||||||
<div className="map-image">
|
<div className="map-image">
|
||||||
{/* TODO: Grab image of model */}
|
{/* 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>
|
||||||
<div className="details">
|
<div className="details">
|
||||||
<div className="header">
|
<div className="header">
|
||||||
@ -29,7 +30,7 @@ export default function SubmissionCard(props: SubmissionCardProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="footer">
|
<div className="footer">
|
||||||
<div className="author">
|
<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>
|
<span>{props.author}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
@ -1,4 +1,4 @@
|
|||||||
@use "../../../globals.scss";
|
@use "../../globals.scss";
|
||||||
|
|
||||||
.submissionCard {
|
.submissionCard {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -9,7 +9,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-width: 180px;
|
min-width: 230px;
|
||||||
max-width: 340px;
|
max-width: 340px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,8 +43,6 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
> img {
|
> img {
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
@forward "./page/card.scss";
|
@forward "../../_components/styles/mapCard.scss";
|
||||||
|
|
||||||
@use "../../globals.scss";
|
@use "../../globals.scss";
|
||||||
|
|
||||||
|
@ -2,9 +2,11 @@
|
|||||||
|
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { MapfixInfo } from "../ts/Mapfix";
|
import { MapfixInfo } from "../ts/Mapfix";
|
||||||
import MapfixCard from "./_card";
|
import MapfixCard from "../_components/mapCard";
|
||||||
import Webpage from "@/app/_components/webpage";
|
import Webpage from "@/app/_components/webpage";
|
||||||
|
|
||||||
|
// TODO: MAKE MAPFIX & SUBMISSIONS USE THE SAME COMPONENTS :angry: (currently too lazy)
|
||||||
|
|
||||||
import "./(styles)/page.scss";
|
import "./(styles)/page.scss";
|
||||||
|
|
||||||
export default function MapfixInfoPage() {
|
export default function MapfixInfoPage() {
|
||||||
@ -100,6 +102,7 @@ export default function MapfixInfoPage() {
|
|||||||
assetId={mapfix.AssetID}
|
assetId={mapfix.AssetID}
|
||||||
displayName={mapfix.DisplayName}
|
displayName={mapfix.DisplayName}
|
||||||
author={mapfix.Creator}
|
author={mapfix.Creator}
|
||||||
|
authorId={mapfix.Submitter}
|
||||||
rating={mapfix.StatusID}
|
rating={mapfix.StatusID}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
@forward "./page/card.scss";
|
@forward "../../_components/styles/mapCard.scss";
|
||||||
|
|
||||||
@use "../../globals.scss";
|
@use "../../globals.scss";
|
||||||
|
|
||||||
@ -27,7 +27,7 @@ a {
|
|||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.grid {
|
.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;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 350px;
|
width: 100%;
|
||||||
height: 350px;
|
height: auto;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
> p {
|
> p {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@ -40,8 +40,7 @@
|
|||||||
gap: 25px;
|
gap: 25px;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: 100%;
|
height: 100%;
|
||||||
height: 350px;
|
|
||||||
object-fit: contain
|
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 { SubmissionInfo, SubmissionStatusToString } from "@/app/ts/Submission";
|
||||||
import type { CreatorAndReviewStatus } from "./_comments";
|
import type { CreatorAndReviewStatus } from "./_comments";
|
||||||
import { MapImage } from "./_map";
|
import { MapImage } from "./_mapImage";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import ReviewButtons from "./_reviewButtons";
|
import ReviewButtons from "./_reviewButtons";
|
||||||
import { Rating } from "@mui/material";
|
import { Rating } from "@mui/material";
|
||||||
@ -15,7 +15,8 @@ import { useState, useEffect } from "react";
|
|||||||
import "./(styles)/page.scss";
|
import "./(styles)/page.scss";
|
||||||
|
|
||||||
interface ReviewId {
|
interface ReviewId {
|
||||||
submissionId: string
|
submissionId: string;
|
||||||
|
assetId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Ratings() {
|
function Ratings() {
|
||||||
@ -43,7 +44,7 @@ function RatingArea(submission: ReviewId) {
|
|||||||
return (
|
return (
|
||||||
<aside className="review-area">
|
<aside className="review-area">
|
||||||
<section className="map-image-area">
|
<section className="map-image-area">
|
||||||
<MapImage/>
|
<MapImage id={submission.assetId}/>
|
||||||
</section>
|
</section>
|
||||||
<Ratings/>
|
<Ratings/>
|
||||||
<ReviewButtons submissionId={submission.submissionId}/>
|
<ReviewButtons submissionId={submission.submissionId}/>
|
||||||
@ -96,7 +97,7 @@ export default function SubmissionInfoPage() {
|
|||||||
<Webpage>
|
<Webpage>
|
||||||
<main className="map-page-main">
|
<main className="map-page-main">
|
||||||
<section className="review-section">
|
<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={[]}/>
|
<TitleAndComments name={submission.DisplayName} creator={submission.Creator} review={submission.StatusID} status_message={submission.StatusMessage} asset_id={submission.AssetID} comments={[]}/>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</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 React, { useState, useEffect } from "react";
|
||||||
import { SubmissionInfo } from "../ts/Submission";
|
import { SubmissionInfo } from "../ts/Submission";
|
||||||
import SubmissionCard from "./_card";
|
import SubmissionCard from "../_components/mapCard";
|
||||||
import Webpage from "@/app/_components/webpage";
|
import Webpage from "@/app/_components/webpage";
|
||||||
|
|
||||||
import "./(styles)/page.scss";
|
import "./(styles)/page.scss";
|
||||||
@ -100,6 +100,7 @@ export default function SubmissionInfoPage() {
|
|||||||
assetId={submission.AssetID}
|
assetId={submission.AssetID}
|
||||||
displayName={submission.DisplayName}
|
displayName={submission.DisplayName}
|
||||||
author={submission.Creator}
|
author={submission.Creator}
|
||||||
|
authorId={submission.Submitter}
|
||||||
rating={submission.StatusID}
|
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