Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 30 additions & 3 deletions client/src/components/MvpTracker/MvpTracker.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useTheme } from "../../contexts/ThemeContext";
import { useMvps } from "../../hooks/useMvps";
import { useState } from "react";
import { useState, useEffect } from "react";
import useImages from "../../hooks/useImages";

const MvpTracker = () => {
const {
Expand Down Expand Up @@ -219,6 +220,29 @@ const MvpCard = ({
const [selectedLocation, setSelectedLocation] = useState(0);
const location = mvp.locations[selectedLocation];
const hasMultipleLocations = mvp.locations.length > 1;
const { getServerImage } = useImages();
const [imageSrc, setImageSrc] = useState<string | null>(null);
const externalImgUrl = `https://ratemyserver.net/mobs/${mvp.id}.gif`;

useEffect(() => {
let mounted = true;
// Try server first, fall back to external URL
(async () => {
try {
const url = await getServerImage(`${mvp.id}.gif`);
if (!mounted) return;
if (url) setImageSrc(url);
else setImageSrc(externalImgUrl);
} catch (e) {
if (!mounted) return;
setImageSrc(externalImgUrl);
}
})();

return () => {
mounted = false;
};
}, [mvp.id]);

const getStatusIndicatorGradient = (status) => {
if (status === "alive") {
Expand Down Expand Up @@ -270,7 +294,7 @@ const MvpCard = ({
)}

<img
src={`https://db.irowiki.org/image/monster/${mvp.id}.png`}
src={imageSrc ?? externalImgUrl}
alt={mvp.name}
className="pixelated absolute z-10"
style={{
Expand All @@ -280,7 +304,10 @@ const MvpCard = ({
top: "45%",
transform: "translate(-50%, -50%) scale(2.5)",
}}
onError={(e) => (e.target.style.display = "none")}
onError={(e) => {
const target = e.target as HTMLImageElement;
target.style.display = "none";
}}
/>

{currentTheme !== "cute" && (
Expand Down
19 changes: 19 additions & 0 deletions client/src/hooks/useImages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export const API_BASE = import.meta.env.VITE_API_BASE || 'http://localhost:8000';

export async function getServerImage(key: string): Promise<string | null> {
try {
const res = await fetch(`${API_BASE}/images/${encodeURIComponent(key)}`);
if (res.ok) {
const data = await res.json();
return data.url;
}
return null;
} catch (e) {
console.error('getServerImage error', e);
return null;
}
}

export default function useImages() {
return { getServerImage };
}
58 changes: 58 additions & 0 deletions server/app/routes/images.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from firebase_admin import storage
from firebase_functions import https_fn
import json
import urllib.request


def get_image(req: https_fn.Request, headers: dict, image_name: str) -> https_fn.Response:

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if image not available in bucket, dont return 404. fetch image from RMS and call save_image on it. then return the bucket URL. @RagnaJDC

"""Check if an image exists in storage and return its public URL."""
try:
bucket = storage.bucket()
blob = bucket.blob(f"mobs/{image_name}")
if blob.exists():
# Ensure it's public (may already be)
try:
blob.make_public()
except Exception:
pass
return https_fn.Response(json.dumps({"url": blob.public_url}), headers=headers)
else:
return https_fn.Response(json.dumps({"error": "Not found"}), status=404, headers=headers)
except Exception as e:
return https_fn.Response(json.dumps({"error": str(e)}), status=400, headers=headers)


def save_image(req: https_fn.Request, headers: dict) -> https_fn.Response:
"""Fetch an external image and save it into Firebase Storage under mobs/<key>.

Expects JSON body: { "externalUrl": "https://...", "key": "123.gif" }
Returns { url: publicUrl } on success.
"""
try:
data = req.get_json()
external = data.get("externalUrl")
key = data.get("key")
if not external or not key:
return https_fn.Response(json.dumps({"error": "externalUrl and key required"}), status=400, headers=headers)

# Fetch external image
try:
resp = urllib.request.urlopen(external, timeout=10)
content_type = resp.headers.get_content_type()
img_bytes = resp.read()
except Exception as e:
return https_fn.Response(json.dumps({"error": f"Failed to fetch external image: {e}"}), status=400, headers=headers)

# Upload to storage
bucket = storage.bucket()
blob = bucket.blob(f"mobs/{key}")
blob.upload_from_string(img_bytes, content_type=content_type or "application/octet-stream")
try:
blob.make_public()
except Exception:
pass

return https_fn.Response(json.dumps({"url": blob.public_url}), headers=headers)

except Exception as e:
return https_fn.Response(json.dumps({"error": str(e)}), status=400, headers=headers)
13 changes: 12 additions & 1 deletion server/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
# Add the current directory to sys.path to allow importing 'app' module
sys.path.append(os.path.dirname(os.path.abspath(__file__)))

from app.routes import mvp, user
from app.routes import mvp, user, images

# Initialize Firebase Admin
# Use GOOGLE_APPLICATION_CREDENTIALS env var or default credentials
Expand Down Expand Up @@ -88,6 +88,17 @@ def api(req: https_fn.Request) -> https_fn.Response:
if req.method == 'POST':
return user.upload_avatar(req, headers, user_id)

# --- IMAGES ---
# GET /images/<name> -> check storage and return public url
if path.startswith('/images'):
# POST /images -> save external image into storage
if path == '/images' and req.method == 'POST':
return images.save_image(req, headers)
# GET /images/<name>
if path.startswith('/images/') and req.method == 'GET':
image_name = path.split('/')[-1]
return images.get_image(req, headers, image_name)

# /users/<id>
if path.startswith('/users/'):
user_id = path.split('/')[-1]
Expand Down