diff --git a/convex/_generated/api.js b/convex/_generated/api.js
index 44bf985..9124e12 100644
--- a/convex/_generated/api.js
+++ b/convex/_generated/api.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+
/**
* Generated `api` utility.
*
diff --git a/convex/_generated/dataModel.d.ts b/convex/_generated/dataModel.d.ts
index f97fd19..f29136b 100644
--- a/convex/_generated/dataModel.d.ts
+++ b/convex/_generated/dataModel.d.ts
@@ -1,4 +1,4 @@
-/* eslint-disable */
+
/**
* Generated data model types.
*
diff --git a/convex/_generated/server.d.ts b/convex/_generated/server.d.ts
index bec05e6..a6e81e7 100644
--- a/convex/_generated/server.d.ts
+++ b/convex/_generated/server.d.ts
@@ -1,4 +1,4 @@
-/* eslint-disable */
+
/**
* Generated utilities for implementing server-side Convex query and mutation functions.
*
diff --git a/convex/_generated/server.js b/convex/_generated/server.js
index bf3d25a..8df3867 100644
--- a/convex/_generated/server.js
+++ b/convex/_generated/server.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+
/**
* Generated utilities for implementing server-side Convex query and mutation functions.
*
diff --git a/convex/dashboard.ts b/convex/dashboard.ts
index c045f64..d673a73 100644
--- a/convex/dashboard.ts
+++ b/convex/dashboard.ts
@@ -65,7 +65,7 @@ export const listEvents = query({
},
handler: async (ctx, args) => {
if (args.search && args.search.trim().length > 0) {
- let sq = ctx.db.query("events").withSearchIndex("search_name", (q) => {
+ const sq = ctx.db.query("events").withSearchIndex("search_name", (q) => {
const base = q.search("name", args.search!);
return args.type ? base.eq("type", args.type) : base;
});
diff --git a/package.json b/package.json
index 4da25cc..be11377 100644
--- a/package.json
+++ b/package.json
@@ -3,8 +3,9 @@
"private": true,
"version": "0.0.0",
"type": "module",
+ "engines": { "node": ">=20" },
"scripts": {
- "prebuild": "node -e \"const fs=require('fs'),p=require('path');fs.mkdirSync('dist',{recursive:true});const src='changelog.md',dst=p.join('public','changelog.md');if(fs.existsSync(src)){fs.mkdirSync('public',{recursive:true});fs.copyFileSync(src,dst);}\"",
+ "prebuild": "node scripts/generate_sitemap.mjs && node -e \"const fs=require('fs'),p=require('path');fs.mkdirSync('dist',{recursive:true});const src='changelog.md',dst=p.join('public','changelog.md');if(fs.existsSync(src)){fs.mkdirSync('public',{recursive:true});fs.copyFileSync(src,dst);}\"",
"dev": "concurrently \"pnpm run dev:vite\" \"pnpm run dev:server\"",
"dev:vite": "vite",
"dev:server": "node server.js",
diff --git a/public/robots.txt b/public/robots.txt
index 14267e9..b23451e 100644
--- a/public/robots.txt
+++ b/public/robots.txt
@@ -1,2 +1,3 @@
User-agent: *
-Allow: /
\ No newline at end of file
+Allow: /
+Sitemap: https://renderdragon.org/sitemap.xml
diff --git a/public/sitemap.xml b/public/sitemap.xml
new file mode 100644
index 0000000..fa5d65a
--- /dev/null
+++ b/public/sitemap.xml
@@ -0,0 +1 @@
+https://renderdragon.org/https://renderdragon.org/resourceshttps://renderdragon.org/blogshttps://renderdragon.org/guideshttps://renderdragon.org/faqhttps://renderdragon.org/contacthttps://renderdragon.org/showcasehttps://renderdragon.org/communityhttps://renderdragon.org/changelogshttps://renderdragon.org/utilitieshttps://renderdragon.org/generatorshttps://renderdragon.org/background-generatorhttps://renderdragon.org/text-generatorhttps://renderdragon.org/ai-title-helperhttps://renderdragon.org/youtube-downloaderhttps://renderdragon.org/player-rendererhttps://renderdragon.org/renderbothttps://renderdragon.org/native-applicationhttps://renderdragon.org/toshttps://renderdragon.org/privacyhttps://renderdragon.org/guides/scriptwritinghttps://renderdragon.org/guides/AIhttps://renderdragon.org/guides/questionshttps://renderdragon.org/guides/copyrighthttps://renderdragon.org/guides/thingstoaskhttps://renderdragon.org/guides/voice
diff --git a/scripts/export_resources.ts b/scripts/export_resources.ts
index 85b5748..48bd069 100644
--- a/scripts/export_resources.ts
+++ b/scripts/export_resources.ts
@@ -83,7 +83,7 @@ async function exportResources() {
});
return acc;
},
- {} as Record,
+ {} as Record>>,
);
const mcicons = await fetchMcicons();
@@ -112,7 +112,7 @@ async function exportResources() {
file: `resources/${file}`,
};
allResources.push(
- ...items.map((item: any) => ({
+ ...items.map((item: Record) => ({
...item,
category,
})),
diff --git a/scripts/generate_sitemap.mjs b/scripts/generate_sitemap.mjs
new file mode 100644
index 0000000..7dd8fab
--- /dev/null
+++ b/scripts/generate_sitemap.mjs
@@ -0,0 +1,42 @@
+import { mkdir, writeFile } from "node:fs/promises";
+
+const site = "https://renderdragon.org";
+const routes = [
+ "/", "/resources", "/blogs", "/guides", "/faq", "/contact", "/showcase",
+ "/community", "/changelogs", "/utilities", "/generators", "/background-generator",
+ "/text-generator", "/ai-title-helper", "/youtube-downloader", "/player-renderer",
+ "/renderbot", "/native-application", "/tos", "/privacy",
+ "/guides/scriptwriting", "/guides/AI", "/guides/questions", "/guides/copyright", "/guides/thingstoask", "/guides/voice",
+];
+
+const escapeXml = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
+const urls = routes.map((route) => `${site}${route}`);
+
+// Public profile and creator-pack slugs are added when build credentials are available.
+if (process.env.VITE_SUPABASE_URL && process.env.VITE_SUPABASE_PUBLISHABLE_KEY) {
+ const headers = { apikey: process.env.VITE_SUPABASE_PUBLISHABLE_KEY };
+ const fetchJson = async (path) => {
+ const controller = new AbortController();
+ const timeout = setTimeout(() => controller.abort(), 8000);
+ try {
+ const response = await fetch(`${process.env.VITE_SUPABASE_URL}/rest/v1/${path}`, { headers, signal: controller.signal });
+ return response.ok ? await response.json() : [];
+ } catch (error) {
+ console.warn(`Skipping sitemap enrichment for ${path}:`, error instanceof Error ? error.message : error);
+ return [];
+ } finally {
+ clearTimeout(timeout);
+ }
+ };
+ const [profiles, packs, blogs] = await Promise.all([
+ fetchJson("profiles?select=username&username=not.is.null"),
+ fetchJson("creator_packs?select=slug&status=eq.approved"),
+ fetchJson("blogs?select=slug&published=eq.true"),
+ ]);
+ for (const { username } of profiles) if (username) urls.push(`${site}/u/${escapeXml(username)}`);
+ for (const { slug } of packs) if (slug) urls.push(`${site}/creator-packs/${escapeXml(slug)}`);
+ for (const { slug } of blogs) if (slug) urls.push(`${site}/blogs/${escapeXml(slug)}`);
+}
+
+await mkdir("public", { recursive: true });
+await writeFile("public/sitemap.xml", `${urls.join("")}\n`);
diff --git a/src/App.tsx b/src/App.tsx
index 903a147..07edb08 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -14,6 +14,7 @@ import { IconLoader2 } from "@tabler/icons-react";
import DonateButton from "@/components/DonateButton";
import { AdBlockDetector } from "@/components/AdBlockDetector";
import Navbar from "@/components/Navbar";
+import Seo from "@/components/Seo";
const ExternalRedirect = ({ url }: { url: string }) => {
useEffect(() => { window.location.href = url; }, [url]);
@@ -25,9 +26,12 @@ const GlobalComponents = () => {
const location = useLocation();
const hideDonateButton = location.pathname.startsWith('/admin') ||
location.pathname.startsWith('/account');
+ const routeName = location.pathname === '/' ? 'Minecraft Creator Tools & Resources' :
+ location.pathname.split('/').filter(Boolean).map((part) => part.split('-').join(' ')).join(' / ');
return (
<>
+ letter.toUpperCase())} | RenderDragon`} description={`Explore ${routeName} on RenderDragon, free tools and resources for Minecraft content creators.`} path={location.pathname} />
{!hideDonateButton && }
>
diff --git a/src/components/AudioPlayer.tsx b/src/components/AudioPlayer.tsx
index 9ef3ce0..3135a2e 100644
--- a/src/components/AudioPlayer.tsx
+++ b/src/components/AudioPlayer.tsx
@@ -1,5 +1,4 @@
import { useState, useRef, useEffect, useCallback } from 'react';
-import WaveSurfer from 'wavesurfer.js';
import {
IconPlayerPlay,
IconPlayerPause,
@@ -23,14 +22,22 @@ const AudioPlayer = ({ src, className, isInView = true, allowPlayBeforeReady = f
const [duration, setDuration] = useState(0);
const [isLoading, setIsLoading] = useState(true);
const [isReady, setIsReady] = useState(false);
+ const [loadError, setLoadError] = useState(false);
+ const [retryKey, setRetryKey] = useState(0);
const containerRef = useRef(null);
- const wavesurfer = useRef(null);
+ const wavesurfer = useRef(null);
useEffect(() => {
if (!containerRef.current) return;
- const ws = WaveSurfer.create({
+ let ws: import('wavesurfer.js').default | null = null;
+ let isMounted = true;
+ setLoadError(false);
+ setIsReady(false);
+ import('wavesurfer.js').then(({ default: WaveSurfer }) => {
+ if (!isMounted || !containerRef.current) return;
+ ws = WaveSurfer.create({
container: containerRef.current,
waveColor: 'rgba(139, 92, 246, 0.2)', // Soft cow-purple
progressColor: '#8b5cf6', // Solid cow-purple
@@ -42,42 +49,52 @@ const AudioPlayer = ({ src, className, isInView = true, allowPlayBeforeReady = f
barGap: 3,
normalize: true,
hideScrollbar: true,
- });
-
- let isMounted = true;
- wavesurfer.current = ws;
+ });
+ wavesurfer.current = ws;
if (allowPlayBeforeReady) {
setIsLoading(false);
} else {
setIsLoading(true);
}
- ws.load(src).catch((err) => {
+ ws.load(src).catch((err) => {
if (err.name === 'AbortError') return;
console.error('WaveSurfer load error:', err);
- });
+ if (isMounted) {
+ setIsLoading(false);
+ setLoadError(true);
+ }
+ });
- ws.on('ready', () => {
+ ws.on('ready', () => {
if (!isMounted) return;
setDuration(ws.getDuration());
setIsLoading(false);
setIsReady(true);
- });
+ });
- ws.on('audioprocess', () => {
+ ws.on('audioprocess', () => {
if (!isMounted) return;
setCurrentTime(ws.getCurrentTime());
- });
+ });
- ws.on('play', () => isMounted && setIsPlaying(true));
- ws.on('pause', () => isMounted && setIsPlaying(false));
- ws.on('finish', () => isMounted && setIsPlaying(false));
+ ws.on('play', () => isMounted && setIsPlaying(true));
+ ws.on('pause', () => isMounted && setIsPlaying(false));
+ ws.on('finish', () => isMounted && setIsPlaying(false));
+ }).catch((error: unknown) => {
+ if (!isMounted) return;
+ console.error('Failed to load WaveSurfer:', error);
+ setIsLoading(false);
+ setLoadError(true);
+ });
return () => {
isMounted = false;
- ws.destroy();
+ setIsReady(false);
+ if (wavesurfer.current === ws) wavesurfer.current = null;
+ ws?.destroy();
};
- }, [src]);
+ }, [src, allowPlayBeforeReady, retryKey]);
// Handle visibility
useEffect(() => {
@@ -132,6 +149,12 @@ const AudioPlayer = ({ src, className, isInView = true, allowPlayBeforeReady = f
)}
+ {loadError && (
+
+ Audio preview unavailable.
+
+
+ )}
{/* Controls and Info */}
diff --git a/src/components/FeaturedResources.tsx b/src/components/FeaturedResources.tsx
index e646acd..413f6d8 100644
--- a/src/components/FeaturedResources.tsx
+++ b/src/components/FeaturedResources.tsx
@@ -7,6 +7,8 @@ import ResourceCard from "@/components/resources/ResourceCard";
import ResourceCardSkeleton from "./resources/ResourceCardSkeleton";
const FEATURED_CATEGORIES = ["music", "images", "sfx", "fonts"];
+type RawResource = Record;
+const stringValue = (value: unknown): string | undefined => typeof value === "string" && value.trim() ? value : undefined;
const FeaturedResources = () => {
const [featuredResources, setFeaturedResources] = useState([]);
@@ -32,18 +34,15 @@ const FeaturedResources = () => {
const rawItems = await catRes.json();
const resources: Resource[] = (Array.isArray(rawItems) ? rawItems : [])
.slice(0, 4)
- .map((item: any, idx: number) => ({
- id: item.id ?? `${catKeys[0]}-${idx}`,
- title: String(item?.title || "").trim() || `Resource ${idx + 1}`,
+ .map((item: RawResource, idx: number) => ({
+ id: typeof item.id === "number" || typeof item.id === "string" ? item.id : `${catKeys[0]}-${idx}`,
+ title: stringValue(item.title) || `Resource ${idx + 1}`,
category: catKeys[0] as Resource["category"],
- subcategory: item.subcategory || undefined,
- credit: item.credit || undefined,
- filetype: item.filetype || item.ext || undefined,
- download_url: item.download_url || item.url || undefined,
- preview_url: item.preview_url || undefined,
- image_url: item.image_url || undefined,
- software: item.software || undefined,
- description: item.description || undefined,
+ subcategory: stringValue(item.subcategory), credit: stringValue(item.credit),
+ filetype: stringValue(item.filetype) || stringValue(item.ext),
+ download_url: stringValue(item.download_url) || stringValue(item.url),
+ preview_url: stringValue(item.preview_url), image_url: stringValue(item.image_url),
+ software: stringValue(item.software), description: stringValue(item.description),
}));
setFeaturedResources(resources);
diff --git a/src/components/InfiniteMenu.tsx b/src/components/InfiniteMenu.tsx
index aec290e..d96c434 100644
--- a/src/components/InfiniteMenu.tsx
+++ b/src/components/InfiniteMenu.tsx
@@ -1097,8 +1097,6 @@ const InfiniteMenu: FC = ({ items = [] }) => {
if (!activeItem?.link) return;
if (activeItem.link.startsWith('http')) {
window.open(activeItem.link, '_blank', 'noopener,noreferrer');
- } else {
-
}
};
diff --git a/src/components/Seo.tsx b/src/components/Seo.tsx
new file mode 100644
index 0000000..07654cf
--- /dev/null
+++ b/src/components/Seo.tsx
@@ -0,0 +1,30 @@
+import { Helmet } from "react-helmet-async";
+
+const SITE_URL = "https://renderdragon.org";
+
+interface SeoProps {
+ title: string;
+ description: string;
+ path: string;
+ image?: string;
+}
+
+export default function Seo({ title, description, path, image = "/ogimg.png" }: SeoProps) {
+ const normalizedPath = path.replace(/\/+$/, "") || "/";
+ const canonical = `${SITE_URL}${normalizedPath === "/" ? "" : normalizedPath}`;
+ const imageUrl = image.startsWith("http") ? image : `${SITE_URL}${image}`;
+ return
+ {title}
+
+
+
+
+
+
+
+
+
+
+
+ ;
+}
diff --git a/src/components/VideoPlayer.tsx b/src/components/VideoPlayer.tsx
index 3d3be51..2feb972 100644
--- a/src/components/VideoPlayer.tsx
+++ b/src/components/VideoPlayer.tsx
@@ -1,6 +1,5 @@
-import React, { useEffect, useRef } from 'react';
-import videojs from 'video.js';
+import React, { useEffect, useRef, useState } from 'react';
import 'video.js/dist/video-js.css';
interface VideoPlayerProps {
@@ -10,6 +9,7 @@ interface VideoPlayerProps {
controls?: boolean;
className?: string;
}
+type VideoPlayerInstance = ReturnType;
const VideoPlayer: React.FC = ({
src,
@@ -19,24 +19,30 @@ const VideoPlayer: React.FC = ({
className = ""
}) => {
const videoRef = useRef(null);
- const playerRef = useRef(null);
+ const playerRef = useRef(null);
+ const [loadError, setLoadError] = useState(false);
+ const [retryKey, setRetryKey] = useState(0);
useEffect(() => {
- // Make sure Video.js player is only initialized once
- if (!playerRef.current) {
- const videoElement = document.createElement("video-js");
+ let cancelled = false;
+ let activePlayer: VideoPlayerInstance | null = null;
+ const videoElement = document.createElement("video-js");
+ setLoadError(false);
- videoElement.classList.add('vjs-big-play-centered');
- videoElement.classList.add('vjs-custom-skin');
- if (className) {
- className.split(' ').forEach(cls => videoElement.classList.add(cls));
- }
+ videoElement.classList.add('vjs-big-play-centered');
+ videoElement.classList.add('vjs-custom-skin');
+ if (className) {
+ className.split(' ').forEach(cls => videoElement.classList.add(cls));
+ }
- if (videoRef.current) {
- videoRef.current.appendChild(videoElement);
- }
+ const initializePlayer = async () => {
+ if (!videoRef.current || cancelled) return;
+ videoRef.current.appendChild(videoElement);
- const player = playerRef.current = videojs(videoElement, {
+ try {
+ const { default: videojs } = await import('video.js');
+ if (cancelled) return;
+ const player = videojs(videoElement, {
autoplay,
controls,
responsive: true,
@@ -46,35 +52,42 @@ const VideoPlayer: React.FC = ({
}, () => {
// Player is ready
});
+ if (cancelled) {
+ player.dispose();
+ return;
+ }
+ activePlayer = player;
+ playerRef.current = player;
player.on('error', () => {
const error = player.error();
console.warn('VideoJS Error:', error);
});
-
- } else {
- // Update src if it changes
- const player = playerRef.current;
- player.src({ src });
- if (poster) player.poster(poster);
- }
- }, [src, poster, autoplay, controls, className]);
-
- // Dispose the player on unmount
- useEffect(() => {
- const player = playerRef.current;
+ } catch (error) {
+ if (!cancelled) {
+ console.error('Failed to load Video.js:', error);
+ setLoadError(true);
+ }
+ }
+ };
+ initializePlayer();
return () => {
- if (player && !player.isDisposed()) {
- player.dispose();
+ cancelled = true;
+ if (activePlayer && !activePlayer.isDisposed()) {
+ activePlayer.dispose();
+ }
+ if (playerRef.current === activePlayer) {
playerRef.current = null;
}
- };
- }, [playerRef]);
+ videoElement.remove();
+ }
+ }, [src, poster, autoplay, controls, className, retryKey]);
return (
+ {loadError &&
Video preview unavailable.
}
);
};
diff --git a/src/components/admin/AdminCreatorPacksManager.tsx b/src/components/admin/AdminCreatorPacksManager.tsx
index c2da19e..1c089b4 100644
--- a/src/components/admin/AdminCreatorPacksManager.tsx
+++ b/src/components/admin/AdminCreatorPacksManager.tsx
@@ -22,17 +22,22 @@ const AdminCreatorPacksManager = () => {
const [selectedPackId, setSelectedPackId] = useState(null);
const [isDialogOpen, setIsDialogOpen] = useState(false);
- useEffect(() => {
- loadPendingPacks();
- }, []);
-
const loadPendingPacks = async () => {
setIsLoading(true);
- const packs = await fetchPendingPacks();
- setPendingPacks(packs);
- setIsLoading(false);
+ try {
+ const packs = await fetchPendingPacks();
+ setPendingPacks(packs);
+ } finally {
+ setIsLoading(false);
+ }
};
+ useEffect(() => {
+ loadPendingPacks();
+ // The loader is local to this component and only runs on mount.
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
const handleApprove = async (id: string) => {
const approvedPack = await reviewPack(id, 'approved');
if (approvedPack) {
diff --git a/src/components/admin/BlogEditor.tsx b/src/components/admin/BlogEditor.tsx
index da312d3..925f5a6 100644
--- a/src/components/admin/BlogEditor.tsx
+++ b/src/components/admin/BlogEditor.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect } from "react";
+import { useState, useEffect, useCallback } from "react";
import { supabase } from "@/integrations/supabase/client";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@@ -17,8 +17,8 @@ const slugify = (text: string) => {
.toLowerCase()
.trim()
.replace(/\s+/g, '-')
- .replace(/[^\w\-]+/g, '')
- .replace(/\-\-+/g, '-');
+ .replace(/[^\w-]+/g, '')
+ .replace(/--+/g, '-');
};
export default function BlogEditor() {
@@ -28,55 +28,46 @@ export default function BlogEditor() {
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
- // Admin Authorization Check
- const authorizedEmails = ['yamura@duck.com', 'theckie@protonmail.com', 'vovoplaygame3@gmail.com'];
- const isAuthorized = user && authorizedEmails.includes(user?.email || '');
-
- if (authLoading) return
;
-
- if (!user || !isAuthorized) {
- return ;
- }
-
+ // Hooks must stay above authorization returns so their order is stable.
const [title, setTitle] = useState("");
const [slug, setSlug] = useState("");
const [content, setContent] = useState("");
const [published, setPublished] = useState(false);
const [preview, setPreview] = useState(false);
- useEffect(() => {
- if (id) {
- loadBlog(id);
- }
- }, [id]);
-
- // Auto-generate slug from title if creating new
- useEffect(() => {
- if (!id && title) {
- setSlug(slugify(title));
- }
- }, [title, id]);
-
- const loadBlog = async (blogId: string) => {
+ const loadBlog = useCallback(async (blogId: string) => {
setLoading(true);
- const { data, error } = await supabase
- .from("blogs")
- .select("*")
- .eq("id", blogId)
- .single();
-
- if (error) {
- toast.error("Failed to load blog");
+ try {
+ const { data, error } = await supabase.from("blogs").select("*").eq("id", blogId).single();
+ if (error) {
+ toast.error("Failed to load blog");
+ console.error(error);
+ navigate("/admin");
+ } else if (data) {
+ setTitle(data.title); setSlug(data.slug); setContent(data.content || ""); setPublished(data.published || false);
+ }
+ } catch (error) {
console.error(error);
+ toast.error("Failed to load blog");
navigate("/admin");
- } else if (data) {
- setTitle(data.title);
- setSlug(data.slug);
- setContent(data.content || "");
- setPublished(data.published || false);
+ } finally {
+ setLoading(false);
}
- setLoading(false);
- };
+ }, [navigate]);
+
+ useEffect(() => { if (id) loadBlog(id); }, [id, loadBlog]);
+ useEffect(() => { if (!id && title) setSlug(slugify(title)); }, [title, id]);
+
+ // Admin Authorization Check
+ const authorizedEmails = ['yamura@duck.com', 'theckie@protonmail.com', 'vovoplaygame3@gmail.com'];
+ const isAuthorized = user && authorizedEmails.includes(user?.email || '');
+
+ if (authLoading) return
;
+
+ if (!user || !isAuthorized) {
+ return ;
+ }
+
const handleSave = async () => {
if (!title || !slug || !user) {
@@ -112,9 +103,9 @@ export default function BlogEditor() {
toast.success("Blog created successfully");
navigate("/admin"); // Redirect or clear form
}
- } catch (e: any) {
+ } catch (e: unknown) {
console.error("Error saving blog:", e);
- toast.error(`Error saving: ${e.message}`);
+ toast.error(`Error saving: ${e instanceof Error ? e.message : "Unknown error"}`);
} finally {
setSaving(false);
}
diff --git a/src/components/profile/FontPicker.tsx b/src/components/profile/FontPicker.tsx
index 3bfa6b7..ad90cd6 100644
--- a/src/components/profile/FontPicker.tsx
+++ b/src/components/profile/FontPicker.tsx
@@ -17,6 +17,19 @@ interface FontOption {
url: string;
}
+interface RawFontOption {
+ id?: unknown;
+ title?: unknown;
+ filename?: unknown;
+ url?: unknown;
+}
+
+const isFontOption = (font: RawFontOption): font is FontOption =>
+ typeof font.id === 'number' &&
+ typeof font.title === 'string' &&
+ typeof font.url === 'string' &&
+ font.url.startsWith('https://');
+
interface FontPickerProps {
value: string;
onFontChange: (fontFamily: string, fontUrl?: string) => void;
@@ -55,18 +68,15 @@ export const FontPicker: React.FC = ({ value, onFontChange }) =
const data = await res.json();
if (data && Array.isArray(data.files)) {
- const validated = data.files.filter((f: any) =>
- typeof f.id === 'number' &&
- typeof f.title === 'string' &&
- typeof f.url === 'string' &&
- f.url.startsWith('https://')
- );
+ const validated = (data.files as RawFontOption[])
+ .filter(isFontOption)
+ .map((font) => ({ ...font, filename: typeof font.filename === 'string' ? font.filename : font.title }));
setExternalFonts(validated);
}
return; // Success, exit the loop and function
- } catch (error: any) {
+ } catch (error: unknown) {
clearTimeout(timeoutId);
- if (error.name === 'AbortError') {
+ if (error instanceof DOMException && error.name === 'AbortError') {
toast.error("Font library connection timed out");
break; // Stop retrying on timeout as requested (or could continue, but usually timeouts are systemic)
}
diff --git a/src/components/profile/ImageUpload.tsx b/src/components/profile/ImageUpload.tsx
index 98ee38f..d608169 100644
--- a/src/components/profile/ImageUpload.tsx
+++ b/src/components/profile/ImageUpload.tsx
@@ -69,9 +69,9 @@ export const ImageUpload: React.FC = ({
onUpload(data.publicUrl);
toast.success('Image uploaded successfully');
- } catch (error: any) {
+ } catch (error: unknown) {
console.error('Upload failed:', error);
- toast.error(error.message || 'Failed to upload image');
+ toast.error(error instanceof Error ? error.message : 'Failed to upload image');
} finally {
setUploading(false);
if (fileInputRef.current) {
diff --git a/src/components/profile/ProfileEditor.tsx b/src/components/profile/ProfileEditor.tsx
index 2ed941a..c68bd58 100644
--- a/src/components/profile/ProfileEditor.tsx
+++ b/src/components/profile/ProfileEditor.tsx
@@ -171,6 +171,8 @@ const ProfileEditor: React.FC = () => {
if (user) {
loadProfile();
}
+ // Profile loading is an initial fetch; save effects below own later updates.
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [user]);
// Auto-save draft to local storage
@@ -219,11 +221,11 @@ const ProfileEditor: React.FC = () => {
toast.info("Restored your unsaved draft.");
} else {
setBio(data.bio || '');
- setLinks((data.links as any) || []);
- setThemeConfig((data.theme_config as any) || defaultThemeConfig);
+ setLinks((data.links as unknown as ProfileLink[]) || []);
+ setThemeConfig((data.theme_config as unknown as ProfileThemeConfig) || defaultThemeConfig);
}
}
- } catch (error: any) {
+ } catch (error: unknown) {
toast.error('Failed to load profile settings');
console.error(error);
} finally {
@@ -244,8 +246,8 @@ const ProfileEditor: React.FC = () => {
.from('profiles')
.update({
bio,
- links: links as any,
- theme_config: themeConfig as any,
+ links: links as unknown,
+ theme_config: themeConfig as unknown,
username: username.trim().toLowerCase(),
})
.eq('id', user.id)
@@ -262,8 +264,8 @@ const ProfileEditor: React.FC = () => {
localStorage.removeItem(`${DRAFT_KEY}_${user.id}`);
toast.success('Profile published successfully!');
setShowShare(true);
- } catch (error: any) {
- toast.error(error?.message || 'Failed to save profile');
+ } catch (error: unknown) {
+ toast.error(error instanceof Error ? error.message : 'Failed to save profile');
console.error(error);
} finally {
setSaving(false);
@@ -531,7 +533,7 @@ const ProfileEditor: React.FC = () => {
+ Loading generator...}>
@@ -66,6 +67,7 @@ const Generators = () => {
+
@@ -76,4 +78,4 @@ const Generators = () => {
);
};
-export default Generators;
\ No newline at end of file
+export default Generators;
diff --git a/src/pages/Profile.tsx b/src/pages/Profile.tsx
index 63f3e91..4161d50 100644
--- a/src/pages/Profile.tsx
+++ b/src/pages/Profile.tsx
@@ -22,10 +22,17 @@ type ProfileData = {
bio: string | null;
links: ProfileLink[] | null;
theme_config: ProfileThemeConfig | null;
- social_links: any | null;
+ social_links: Record | null;
verified: boolean | null;
};
+type SelectedProfile = Omit & {
+ social_links: unknown;
+};
+
+const isSocialLinks = (value: unknown): value is Record =>
+ value === null || (typeof value === 'object' && Object.values(value).every((item) => typeof item === 'string'));
+
const SocialIcon = ({ type, url }: { type: string, url: string }) => {
const iconProps = { className: "w-6 h-6 hover:scale-110 transition-transform" };
switch (type) {
@@ -58,7 +65,12 @@ const ProfilePage: React.FC = () => {
if (error) throw error;
if (active) {
- setProfile(data as any); // Cast because of JSON types
+ const row = data as SelectedProfile | null;
+ if (row && typeof row.id === 'string' && isSocialLinks(row.social_links)) {
+ setProfile({ ...row, email: null, social_links: row.social_links });
+ } else {
+ setProfile(null);
+ }
}
} catch (err) {
console.error(err);
diff --git a/src/pages/ResourcesHub.tsx b/src/pages/ResourcesHub.tsx
index ad9fe51..c345f7a 100644
--- a/src/pages/ResourcesHub.tsx
+++ b/src/pages/ResourcesHub.tsx
@@ -17,8 +17,7 @@ import CreatorPacksTab from '@/components/resources/CreatorPacksTab';
import MusicPacksTab from '@/components/resources/MusicPacksTab';
import MusicMoodFilter from '@/components/resources/MusicMoodFilter';
import MinecraftMusicFilter from '@/components/resources/MinecraftMusicFilter';
-import MinecraftChangelogPopup from '@/components/resources/MinecraftChangelogPopup';
-import GreenVoidPopup from '@/components/resources/GreenVoidPopup';
+import ResourceAnnouncementBanner from '@/components/resources/ResourceAnnouncementBanner';
import McSoundsBrowser from '@/components/resources/McSoundsBrowser';
import McIconsBrowser from '@/components/resources/McIconsBrowser';
import AuthDialog from '@/components/auth/AuthDialog';
@@ -107,7 +106,7 @@ const ResourcesHub = () => {
if (!resourceMoods) return false;
return selectedMoods.some(mood => resourceMoods.includes(mood));
});
- }, [filteredResources, isMusicView, selectedMoods, musicMoodsData]);
+ }, [filteredResources, isMusicView, isMinecraftMusicView, selectedMoods, musicMoodsData]);
const mcsoundsResourceCount = useMemo(() => {
if (!isMcSoundsView) return {};
@@ -383,6 +382,7 @@ const ResourcesHub = () => {
+ { handleCategoryChange('music'); window.scrollTo({ top: 0, behavior: 'smooth' }); }} />
{
open={authDialogOpen}
onOpenChange={setAuthDialogOpen}
/>
-
-
-
diff --git a/src/pages/Showcase.tsx b/src/pages/Showcase.tsx
index b974e6d..8c8a176 100644
--- a/src/pages/Showcase.tsx
+++ b/src/pages/Showcase.tsx
@@ -362,12 +362,14 @@ const ShowcasePage: React.FC = () => {
setFilePreview(null);
setOpen(false);
await queryClient.invalidateQueries({ queryKey: ['showcases'] });
- } catch (error: any) {
+ } catch (error: unknown) {
console.error("Failed to create showcase:", error);
toast({
variant: "destructive",
title: "Error",
- description: error.message || "Failed to create showcase. Please try again.",
+ description: error instanceof Error && error.message
+ ? error.message
+ : "Failed to create showcase. Please try again.",
});
} finally {
setSubmitting(false);
diff --git a/vite.config.ts b/vite.config.ts
index 9843025..8babdc8 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -61,8 +61,6 @@ export default defineConfig(({ mode }) => {
manualChunks: {
'vendor-framer': ['framer-motion'],
'vendor-icons': ['@tabler/icons-react'],
- 'vendor-video': ['video.js'],
- 'vendor-audio': ['wavesurfer.js'],
},
},
},
@@ -71,4 +69,4 @@ export default defineConfig(({ mode }) => {
'process.env': env
}
};
-});
\ No newline at end of file
+});