Skip to content

Instantly share code, notes, and snippets.

@Neptunians
Created May 3, 2025 19:58
Show Gist options
  • Save Neptunians/20248eed0d35b3bfdb95a97ddc0dd49e to your computer and use it in GitHub Desktop.
Save Neptunians/20248eed0d35b3bfdb95a97ddc0dd49e to your computer and use it in GitHub Desktop.
import React, { useState, useEffect, useCallback, useContext, createContext, useMemo, useRef, useLayoutEffect, forwardRef } from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter, Routes, Route, Link, NavLink, Form, useNavigate, useLocation, useResolvedPath, useMatches, Outlet, createPath, parsePath } from 'react-router-dom';
import DOMPurify from 'dompurify'; // Assuming DOMPurify is imported
// --- Constants ---
const API_BASE_URL = "https://a-minecraft-movie-api.challs.umdctf.io"; // [cite: 2970]
// --- API Functions ---
// Function to initiate a session
async function startSessionIfNeeded() {
if (window.sessionNumber !== undefined) {
return; // Session already started
}
try {
const response = await fetch(`${API_BASE_URL}/start-session`, { // [cite: 2972]
method: "POST",
credentials: "include"
});
if (!response.ok) {
console.error("Failed to start session:", response.statusText);
return;
}
const { sessionNumber } = await response.json(); // [cite: 2973, 2974]
window.sessionNumber = sessionNumber; // [cite: 2974]
} catch (error) {
console.error("Error starting session:", error);
}
}
// --- Context ---
const AccountContext = createContext(undefined); // [cite: 2974]
function useAccount() {
const context = useContext(AccountContext); // [cite: 2974]
if (!context) {
throw new Error("useAccount must be used within an AccountContext.Provider"); // [cite: 2975]
}
return context; // [cite: 2976]
}
// --- Components ---
// Login/Register Form Component
function LoginRegisterForm() {
const [mode, setMode] = useState("login"); // 'login' or 'register' [cite: 2976]
const [username, setUsername] = useState(""); // [cite: 2976]
const [password, setPassword] = useState(""); // [cite: 2977]
const [error, setError] = useState(undefined); // [cite: 2977]
const [, setIsLoggedIn] = useAccount(); // [cite: 2977]
const navigate = useNavigate(); // [cite: 2977]
const handleSubmit = useCallback(async (event) => {
event.preventDefault(); // [cite: 2977]
setError(undefined); // Clear previous errors
const endpoint = mode === "login" ? "/login" : "/register"; // [cite: 2976, 2977]
try {
const response = await fetch(`${API_BASE_URL}${endpoint}`, { // [cite: 2977]
method: "POST",
headers: {
"Content-Type": "application/json" // [cite: 2977]
},
credentials: "include", // [cite: 2977]
body: JSON.stringify({ username, password }) // [cite: 2977, 2978]
});
if (!response.ok) { // [cite: 2977]
const errorText = await response.text();
setError(errorText); // [cite: 2977]
return;
}
setIsLoggedIn(true); // [cite: 2978]
navigate("/"); // [cite: 2978]
} catch (err) {
setError("An error occurred. Please try again.");
console.error("Login/Register error:", err);
}
}, [username, password, mode, setIsLoggedIn, navigate]); // [cite: 2978]
return (
<div className="flex justify-center items-center"> // [cite: 2979]
<div className="w-full max-w-md p-8 bg-white rounded-xl shadow-md"> // [cite: 2979]
<h2 className="text-2xl font-bold text-center text-[#52a535] mb-6"> // [cite: 2979]
{mode === "login" ? "Login" : "Register"} // [cite: 2979]
</h2>
<form className="space-y-4" onSubmit={handleSubmit}> // [cite: 2980]
<div>
<label className="block text-sm font-medium text-gray-700">Username</label> // [cite: 2981]
<input
id="username" // [cite: 2981]
type="text" // [cite: 2982]
value={username} // [cite: 2982]
onChange={(e) => setUsername(e.target.value)} // [cite: 2982]
className="mt-1 block w-full rounded-md border border-gray-300 shadow-sm px-3 py-2 focus:outline-none focus:ring-[#52a535] focus:border-[#52a535]" // [cite: 2982, 2983]
required // [cite: 2983]
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Password</label> // [cite: 2984]
<input
id="password" // [cite: 2984]
type="password" // [cite: 2985]
value={password} // [cite: 2985]
onChange={(e) => setPassword(e.target.value)} // [cite: 2985]
className="mt-1 block w-full rounded-md border border-gray-300 shadow-sm px-3 py-2 focus:outline-none focus:ring-[#52a535] focus:border-[#52a535]" // [cite: 2985]
required // [cite: 2986]
/>
</div>
{error && <p className="text-red-600 text-sm font-medium">{error}</p>} // [cite: 2986, 2987]
<button
type="submit"
className="cursor-pointer w-full py-2 px-4 bg-[#52a535] text-white rounded-md font-semibold hover:bg-green-600 transition" // [cite: 2987]
>
{mode === "login" ? "Log In" : "Sign Up"} // [cite: 2987, 2988]
</button>
</form>
<div className="mt-4 text-center"> // [cite: 2988]
<p className="text-sm text-gray-600"> // [cite: 2988]
{mode === "login" ? "Don't have an account?" : "Already registered?"} // [cite: 2989]
<button
type="button"
onClick={() => setMode(mode === "login" ? "register" : "login")} // [cite: 2989]
className="cursor-pointer ml-1 text-[#52a535] font-medium hover:underline" // [cite: 2990]
>
{mode === "login" ? "Register" : "Login"} // [cite: 2990]
</button>
</p>
</div>
</div>
</div>
);
}
// Navigation Bar Component
function AppNavBar() {
const [isLoggedIn, setIsLoggedIn] = useAccount(); // [cite: 2991]
useEffect(() => { // [cite: 2992]
// Check login status on mount
(async () => {
try {
const response = await fetch(`${API_BASE_URL}/me`, { credentials: "include" }); // [cite: 2992]
if (response.ok) {
setIsLoggedIn(true); // [cite: 2992]
}
} catch (error) {
console.error("Failed to fetch login status:", error);
// Handle error appropriately, maybe set logged in to false
}
})();
}, [setIsLoggedIn]); // [cite: 2992]
return (
<nav className="bg-[#52a535] shadow-lg p-4 mb-8"> // [cite: 2992]
<div className="flex items-center justify-between"> // [cite: 2992]
<Link to="/" className="text-xl font-semibold tracking-wide text-white cursor-pointer"> // [cite: 2993]
Minecraft Movie Fanclub
</Link>
<div className="space-x-6"> // [cite: 2993]
<NavLink to="/create-post" className="text-white hover:underline cursor-pointer"> // [cite: 2994]
Create a Post
</NavLink>
{isLoggedIn ? (
<NavLink to="/account" className="text-white hover:underline cursor-pointer"> // [cite: 2994, 2995]
Account
</NavLink>
) : (
<NavLink to="/login" className="text-white hover:underline cursor-pointer"> // [cite: 2995, 2996]
Login / Register
</NavLink>
)}
</div>
</div>
</nav>
);
}
// Post Card Component
function PostCard({ postId, title, username, likes }) {
return (
<Link
to={`/post?postId=${postId}`}
className="w-72 bg-white rounded-xl shadow-md border border-gray-200 hover:shadow-lg transition transform hover:scale-[1.01]" // [cite: 3199]
key={postId} // [cite: 3205]
>
<div className="bg-[#52a535] text-white px-4 py-2 rounded-t-xl font-bold"> // [cite: 3199]
{title}
</div>
<div className="p-4 space-y-2 text-sm text-gray-700"> // [cite: 3199]
<p><strong>By:</strong> {username}</p> // [cite: 3200]
<p><strong>Likes:</strong> {likes}</p> // [cite: 3200, 3201]
</div>
</Link>
);
}
// Top Posts Page Component
function TopPostsPage() {
const [posts, setPosts] = useState([]); // [cite: 3201]
const [error, setError] = useState(undefined); // [cite: 3201]
useEffect(() => {
fetch(`${API_BASE_URL}/top-posts`) // [cite: 3202]
.then(response => {
if (!response.ok) {
throw new Error("Failed to load posts"); // [cite: 3202]
}
return response.json(); // [cite: 3202]
})
.then(data => {
// Sort posts by likes descending
setPosts(data.sort((a, b) => b.likes - a.likes)); // [cite: 3202]
})
.catch(err => setError(err.message)); // [cite: 3203]
}, []); // [cite: 3203]
return (
<div className="p-6 bg-white min-h-screen"> // [cite: 3203]
<h1 className="text-3xl font-extrabold text-[#52a535] mb-6 text-center"> // [cite: 3203]
Top Minecraft Movie Posts!!!!!!
</h1>
{error && <p className="text-red-600 text-center mb-4">{error}</p>} // [cite: 3203, 3204]
<div className="flex flex-wrap justify-start gap-6 pl-4"> // [cite: 3204]
{posts.map(post => ( // [cite: 3204]
<PostCard // [cite: 3204]
key={post.postId} // [cite: 3205]
postId={post.postId} // [cite: 3204]
title={post.title} // [cite: 3204, 3205]
username={post.username} // [cite: 3205]
likes={post.likes} // [cite: 3205]
/>
))}
</div>
</div>
);
}
// View Post Page Component
function ViewPostPage() {
const location = useLocation(); // [cite: 3162]
const postId = new URLSearchParams(location.search).get("postId"); // [cite: 3162]
const [post, setPost] = useState(undefined); // [cite: 3162]
const [error, setError] = useState(undefined); // [cite: 3162]
const [socialError, setSocialError] = useState(undefined); // [cite: 3162]
const fetchPost = useCallback(async () => { // [cite: 3162]
if (!postId || !Ym(postId)) return; // Avoid fetching if postId is invalid [cite: 3166]
setError(undefined);
setSocialError(undefined);
try {
const response = await fetch(`${API_BASE_URL}/post?postId=${postId}`); // [cite: 3162]
if (!response.ok) { // [cite: 3163]
setError(await response.text()); // [cite: 3163]
return;
}
setPost(await response.json()); // [cite: 3163]
} catch (err) {
setError("Failed to fetch post."); // [cite: 3163]
}
}, [postId]); // [cite: 3164]
const handleLikeDislike = useCallback(async (likesChange) => { // [cite: 3164]
await startSessionIfNeeded(); // [cite: 3164]
setSocialError(undefined); // Clear previous social errors
if (window.sessionNumber === undefined) {
setSocialError("Session not started. Cannot like/dislike.");
return;
}
try {
const response = await fetch(`${API_BASE_URL}/legacy-social`, { // [cite: 3164]
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded" // [cite: 3164]
},
body: `sessionNumber=${window.sessionNumber}&postId=${postId}&likes=${likesChange}`, // [cite: 3165]
credentials: "include" // [cite: 3165]
});
if (!response.ok) { // [cite: 3165]
setSocialError(await response.text()); // [cite: 3165]
return;
}
await fetchPost(); // Refresh post data after like/dislike [cite: 3165]
} catch (err) {
setSocialError("Failed to update likes.");
console.error("Like/dislike error:", err);
}
}, [postId, fetchPost]); // [cite: 3165]
useEffect(() => { // [cite: 3166]
if (postId && Ym(postId)) { // [cite: 3166]
fetchPost(); // [cite: 3166]
}
}, [postId, fetchPost]); // [cite: 3166]
if (postId != null && !Ym(postId)) { // [cite: 3166]
return (
<div className="flex justify-center items-center">
<p className="text-lg font-semibold text-red-600">Invalid post id!</p> // [cite: 3166, 3167]
</div>
);
}
if (error) { // [cite: 3168]
return (
<div className="flex justify-center items-center">
<p className="text-lg font-semibold text-red-600">Error: {error}</p> // [cite: 3168]
</div>
);
}
if (!post) { // [cite: 3169]
return (
<div className="flex justify-center items-center">
<p className="text-lg font-semibold text-gray-600">Loading...</p> // [cite: 3169]
</div>
);
}
// Sanitize content - Allow specific iframe attributes needed for YouTube embeds
const sanitizedContent = DOMPurify.sanitize(post.content, { // [cite: 3170]
ADD_TAGS: ["iframe"], // [cite: 3170]
ADD_ATTR: ["allow", "allowfullscreen", "frameborder", "scrolling", "src", "width", "height"] // [cite: 3170]
});
return (
<div className="flex justify-center items-center"> // [cite: 3171]
<div className="w-xl rounded-xl shadow-md overflow-hidden"> // [cite: 3171]
{/* Post Header */}
<div className="bg-[#52a535] p-4"> // [cite: 3171]
<h2 className="text-white text-xl font-bold">{post.title}</h2> // [cite: 3171, 3172]
<p className="text-white text-sm"> // [cite: 3172]
Posted by <span className="font-semibold">@{post.username}</span> // [cite: 3172, 3173]
</p>
</div>
{/* Post Body */}
<div className="p-6 bg-white space-y-4"> // [cite: 3174]
{/* Use dangerouslySetInnerHTML for sanitized HTML */}
<div
className="text-gray-700" // [cite: 3174]
dangerouslySetInnerHTML={{ __html: sanitizedContent }} // [cite: 3174, 3175]
/>
{/* Admin Like Indicator */}
{post.likedByAdmin && ( // [cite: 3175]
<div className="bg-yellow-100 text-yellow-800 text-sm font-medium px-4 py-2 rounded-md shadow"> // [cite: 3175]
🌟 This post was liked by an admin!
</div>
)}
{/* Social Error Message */}
{socialError && ( // [cite: 3176]
<p className="text-sm text-red-600 font-medium">{socialError}</p> // [cite: 3176]
)}
{/* TODO Comment */}
<div dangerouslySetInnerHTML={{ __html: "" }} /> // [cite: 3177]
{/* Like/Dislike Section */}
<div className="flex items-center justify-between"> // [cite: 3178]
<span className="text-sm text-gray-600">{post.likes} Likes</span> // [cite: 3178]
<div className="flex items-center space-x-2"> // [cite: 3179]
<button
className="cursor-pointer px-3 py-1 text-sm font-medium text-white bg-[#52a535] rounded-md hover:bg-green-600 transition" // [cite: 3179]
onClick={() => handleLikeDislike(1)} // [cite: 3180]
>
👍 Like
</button>
<button
id="dislike-button" // [cite: 3180, 3181]
className="cursor-pointer px-3 py-1 text-sm font-medium text-white bg-red-500 rounded-md hover:bg-red-600 transition" // [cite: 3181]
onClick={() => handleLikeDislike(-1)} // [cite: 3181]
>
👎 Dislike
</button> // [cite: 3181, 3182]
</div>
</div>
</div>
</div>
</div>
);
}
// Create Post Page Component
function CreatePostPage() {
const [title, setTitle] = useState(""); // [cite: 3182]
const [content, setContent] = useState(""); // [cite: 3182, 3183]
const [error, setError] = useState(undefined); // [cite: 3183]
const navigate = useNavigate(); // [cite: 3183]
const handleSubmit = useCallback(async (event) => { // [cite: 3183]
event.preventDefault(); // [cite: 3183]
setError(undefined); // Clear previous errors
try {
const response = await fetch(`${API_BASE_URL}/create-post`, { // [cite: 3184]
method: "POST",
headers: {
"Content-Type": "application/json" // [cite: 3184]
},
credentials: "include", // [cite: 3184]
body: JSON.stringify({ title, content }) // [cite: 3184, 3185]
});
if (!response.ok) { // [cite: 3186]
setError(await response.text()); // [cite: 3186]
return; // [cite: 3187]
}
const postId = await response.text(); // [cite: 3187]
navigate(`/post?postId=${postId}`); // [cite: 3188]
} catch (err) {
setError("An error occurred while creating the post.");
console.error("Create post error:", err);
}
}, [title, content, navigate]); // [cite: 3188]
return (
<div className="flex justify-center items-center"> // [cite: 3189]
<form
className="w-full max-w-md rounded-xl shadow-md overflow-hidden bg-white" // [cite: 3189]
onSubmit={handleSubmit} // [cite: 3189]
>
<div className="bg-[#52a535] p-4"> // [cite: 3189]
<h2 className="text-white text-xl font-bold">Create a Post</h2> // [cite: 3190]
</div>
<div className="p-6 space-y-4"> // [cite: 3190]
{error && <p className="text-sm text-red-600 font-medium">{error}</p>} // [cite: 3191]
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Title</label> // [cite: 3192]
<input
type="text" // [cite: 3193]
className="w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none focus:ring-[#52a535] focus:border-[#52a535]" // [cite: 3193]
value={title} // [cite: 3193]
onChange={(e) => setTitle(e.target.value)} // [cite: 3193]
required // [cite: 3194]
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Content</label> // [cite: 3194, 3195]
<textarea
className="w-full h-32 px-3 py-2 border rounded-md shadow-sm resize-none focus:outline-none focus:ring-[#52a535] focus:border-[#52a535]" // [cite: 3195]
value={content} // [cite: 3196]
onChange={(e) => setContent(e.target.value)} // [cite: 3196]
required // [cite: 3196]
/>
</div>
<div className="text-right"> // [cite: 3197]
<button
type="submit"
className="cursor-pointer px-4 py-2 bg-[#52a535] text-white text-sm font-medium rounded-md hover:bg-green-600 transition" // [cite: 3197, 3198]
>
📤 Submit Post
</button>
</div>
</div>
</form>
</div>
);
}
// Account Page Component
function AccountPage() {
const [accountInfo, setAccountInfo] = useState(undefined); // [cite: 3205]
const [error, setError] = useState(undefined); // [cite: 3205]
useEffect(() => { // [cite: 3206]
fetch(`${API_BASE_URL}/me`, { credentials: "include" }) // [cite: 3206]
.then(response => {
if (!response.ok) {
throw new Error("Failed to load account info"); // [cite: 3206]
}
return response.json(); // [cite: 3206]
})
.then(setAccountInfo) // [cite: 3207]
.catch(err => setError(err.message)); // [cite: 3207]
}, []); // [cite: 3207]
return (
<div className="p-6 bg-white min-h-screen"> // [cite: 3207]
<h1 className="text-3xl font-extrabold text-[#52a535] mb-6 text-center"> // [cite: 3207]
Account Summary
</h1>
{error && <p className="text-red-600 text-center mb-4">{error}</p>} // [cite: 3207, 3208]
{accountInfo && ( // [cite: 3208]
<>
<div className="text-center text-gray-800 mb-8 space-y-2"> // [cite: 3208]
<p className="text-xl font-semibold">Username: {accountInfo.username}</p> // [cite: 3208, 3209]
<p className="text-lg"> // [cite: 3209]
Current Session Number: {window.sessionNumber !== undefined ? window.sessionNumber : "undefined"} {/* [cite: 3209, 3210] */}
</p>
{/* Display flag if present */}
{"flag" in accountInfo && accountInfo.flag && ( // [cite: 3210]
<div className="mt-4 text-green-700 font-medium"> // [cite: 3210]
<p>Wow, an admin liked your post! ⛏️</p> // [cite: 3210, 3211]
<p>Your flag is: {accountInfo.flag}</p> // [cite: 3211]
</div>
)}
</div>
<h2 className="text-2xl font-bold text-[#52a535] mb-4 pl-4">Your Posts</h2> // [cite: 3212]
{accountInfo.posts.length === 0 ? ( // [cite: 3212]
<p className="pl-4 text-gray-600 italic">You haven't posted anything yet.</p> // [cite: 3213]
) : (
<div className="flex flex-wrap justify-start gap-6 pl-4"> // [cite: 3213]
{accountInfo.posts.map(post => ( // [cite: 3213]
<PostCard // [cite: 3213]
key={post.postId} // [cite: 3214]
postId={post.postId} // [cite: 3214]
title={post.title} // [cite: 3214]
username={accountInfo.username} // [cite: 3214]
likes={post.likes} // [cite: 3214]
/>
))}
</div> // [cite: 3214, 3215]
)}
</>
)}
</div> // [cite: 3215]
);
}
// Main Application Component
function App() {
const [isLoggedIn, setIsLoggedIn] = useState(false); // [cite: 3215]
// DOMPurify hook to sanitize iframes (only allow specific youtube embeds)
useEffect(() => { // [cite: 3216]
DOMPurify.addHook("uponSanitizeElement", (node, data) => { // [cite: 3216]
if (data.tagName === "iframe" && node instanceof Element) { // [cite: 3216]
const src = node.getAttribute("src") || "";
// Allow only specific googleusercontent URLs that likely proxy YouTube
if (!src.startsWith("https://www.youtube.com/embed/")) { // [cite: 3216]
// Attempt to remove the node if the src is not allowed
// Note: DOMPurify hooks run *during* sanitization. Direct removal might
// interfere. A better approach might be to forbid the element entirely
// if the src doesn't match, but this mimics the original logic.
try {
if (node.parentNode) {
node.parentNode.removeChild(node); // [cite: 3216]
}
} catch (e) {
console.warn("Could not remove invalid iframe during sanitization:", e);
}
}
}
});
// Cleanup hook if necessary, though DOMPurify hooks are usually global
// return () => DOMPurify.removeHook('uponSanitizeElement');
}, []); // [cite: 3217]
return (
<div className="min-h-screen bg-gray-50 text-gray-900"> // [cite: 3216, 3217]
{/* Wrap with AccountContext Provider */}
<AccountContext.Provider value={[isLoggedIn, setIsLoggedIn]}> // [cite: 3217]
<AppNavBar /> // [cite: 3217]
<Routes> // [cite: 3217]
<Route path="/" element={<TopPostsPage />} /> // [cite: 3217, 3218]
<Route path="/login" element={<LoginRegisterForm />} /> // [cite: 3218]
<Route path="/post" element={<ViewPostPage />} /> // [cite: 3218, 3219]
<Route path="/create-post" element={<CreatePostPage />} /> // [cite: 3219]
<Route path="/account" element={<AccountPage />} /> // [cite: 3219, 3220]
</Routes>
</AccountContext.Provider>
</div>
);
}
// --- Entry Point ---
const rootElement = document.getElementById("root");
if (!rootElement) {
throw new Error("Failed to find the root element");
}
const root = ReactDOM.createRoot(rootElement);
root.render( // [cite: 3220]
<React.StrictMode> // [cite: 3220]
<BrowserRouter> // [cite: 3220]
<App /> // [cite: 3221]
</BrowserRouter>
</React.StrictMode>
); // [cite: 3221]
// Initial session start attempt
startSessionIfNeeded(); // [cite: 2971]
// Note: The initial module preloading logic and React/Router library source code
// have been omitted as requested. DOMPurify's source is also omitted,
// assuming it's included via import.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment