Files
tcg_proxy_maker/index.html
2026-04-11 02:03:38 +01:00

449 lines
18 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useRef, useCallback } from "react";
const CARD_TYPES = ["Creature", "Instant", "Sorcery", "Enchantment", "Artifact", "Land", "Planeswalker", "Battle"];
const COLORS = [
{ id: "W", label: "White", symbol: "☀", bg: "#f9f6d5", border: "#c8b87a", text: "#333" },
{ id: "U", label: "Blue", symbol: "💧", bg: "#c6d9e8", border: "#3a6b9c", text: "#fff" },
{ id: "B", label: "Black", symbol: "💀", bg: "#1a1a1a", border: "#5a4a6e", text: "#d0c8e0" },
{ id: "R", label: "Red", symbol: "🔥", bg: "#e87040", border: "#8b2500", text: "#fff" },
{ id: "G", label: "Green", symbol: "🌲", bg: "#3a7a48", border: "#1a4025", text: "#d4f5d0" },
{ id: "C", label: "Colorless", symbol: "◇", bg: "#c0bdb5", border: "#7a7068", text: "#333" },
{ id: "M", label: "Multi", symbol: "★", bg: "linear-gradient(135deg,#f9f6d5,#c6d9e8,#1a1a1a,#e87040,#3a7a48)", border: "#b8960c", text: "#fff" },
];
const RARITY_COLORS = { Common: "#aaa", Uncommon: "#a0c0d0", Rare: "#d4af37", "Mythic Rare": "#e07020" };
const DEFAULT_CARD = {
name: "Blazing Champion",
manaCost: "{2}{R}{R}",
type: "Creature",
subtype: "Human Warrior",
color: "R",
rarity: "Rare",
power: "4",
toughness: "3",
rulesText: "Haste\nWhen Blazing Champion enters the battlefield, it deals 2 damage to any target.",
flavorText: "\"No retreat. No mercy. Only fire.\"",
artist: "Proxy Artist",
setSymbol: "★",
loyalty: "",
};
function CardPreview({ card, imageUrl }) {
const colorObj = COLORS.find(c => c.id === card.color) || COLORS[5];
const isMulti = card.color === "M";
const isLand = card.type === "Land";
const isPW = card.type === "Planeswalker";
const frameBg = isMulti
? "linear-gradient(160deg,#f9f6d5 0%,#c6d9e8 25%,#1a1a1a 50%,#e87040 75%,#3a7a48 100%)"
: colorObj.bg;
const rarityColor = RARITY_COLORS[card.rarity] || "#aaa";
return (
<div style={{
width: 280,
minHeight: 390,
borderRadius: 16,
border: `4px solid ${colorObj.border}`,
background: frameBg,
boxShadow: `0 0 0 2px #111, 0 0 30px ${colorObj.border}55, 0 8px 32px #0008`,
fontFamily: "'Palatino Linotype', Palatino, 'Book Antiqua', serif",
color: colorObj.text,
display: "flex",
flexDirection: "column",
overflow: "hidden",
position: "relative",
userSelect: "none",
}}>
{/* Header */}
<div style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "7px 10px 4px",
background: `${colorObj.border}cc`,
borderBottom: `2px solid ${colorObj.border}`,
}}>
<div style={{ fontWeight: 700, fontSize: 15, letterSpacing: 0.3, textShadow: "0 1px 2px #0005", flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
{card.name || "Card Name"}
</div>
<div style={{ fontSize: 13, fontFamily: "monospace", marginLeft: 8, whiteSpace: "nowrap", opacity: 0.95, letterSpacing: 1 }}>
{card.manaCost || ""}
</div>
</div>
{/* Art Frame */}
<div style={{
margin: "6px 8px",
height: 140,
borderRadius: 8,
border: `2px solid ${colorObj.border}`,
overflow: "hidden",
background: imageUrl ? "transparent" : `linear-gradient(135deg, ${colorObj.border}44, ${colorObj.border}99)`,
display: "flex",
alignItems: "center",
justifyContent: "center",
position: "relative",
}}>
{imageUrl ? (
<img src={imageUrl} alt="card art" style={{ width: "100%", height: "100%", objectFit: "cover" }} />
) : (
<div style={{ opacity: 0.4, fontSize: 48, textAlign: "center" }}>
{colorObj.symbol}
</div>
)}
{/* Set & rarity */}
<div style={{
position: "absolute", bottom: 4, right: 6,
fontSize: 11, color: rarityColor, textShadow: "0 1px 3px #000a",
fontWeight: 700,
}}>
{card.setSymbol || "★"} {card.rarity ? card.rarity[0] : ""}
</div>
</div>
{/* Type Line */}
<div style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "3px 10px",
background: `${colorObj.border}99`,
fontSize: 12,
fontStyle: "italic",
borderTop: `1px solid ${colorObj.border}66`,
borderBottom: `1px solid ${colorObj.border}66`,
}}>
<span>{[card.type, card.subtype].filter(Boolean).join(" — ") || "Type"}</span>
<span style={{ fontSize: 14 }}>{card.setSymbol || "★"}</span>
</div>
{/* Text Box */}
<div style={{
flex: 1,
margin: "5px 8px 5px",
background: "#f5f0e044",
borderRadius: 6,
border: `1px solid ${colorObj.border}55`,
padding: "7px 9px",
minHeight: 80,
display: "flex",
flexDirection: "column",
gap: 6,
}}>
<div style={{ fontSize: 12, lineHeight: 1.45, whiteSpace: "pre-wrap" }}>
{card.rulesText || ""}
</div>
{card.flavorText && (
<div style={{ fontSize: 11, fontStyle: "italic", opacity: 0.75, borderTop: `1px solid ${colorObj.border}44`, paddingTop: 5, lineHeight: 1.35 }}>
{card.flavorText}
</div>
)}
</div>
{/* Footer */}
<div style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "4px 10px 6px",
fontSize: 10,
opacity: 0.7,
borderTop: `1px solid ${colorObj.border}44`,
}}>
<span>✦ {card.artist || "Unknown"}</span>
{(card.type === "Creature" || card.type === "Battle") && (
<div style={{
background: colorObj.border,
color: "#fff",
borderRadius: 4,
padding: "1px 7px",
fontWeight: 700,
fontSize: 13,
boxShadow: "0 1px 4px #0006",
}}>
{card.power}/{card.toughness}
</div>
)}
{isPW && card.loyalty && (
<div style={{
background: "#1a5ab8",
color: "#fff",
borderRadius: "50%",
width: 26, height: 26,
display: "flex", alignItems: "center", justifyContent: "center",
fontWeight: 700, fontSize: 14,
boxShadow: "0 1px 4px #0006",
}}>
{card.loyalty}
</div>
)}
</div>
</div>
);
}
function Field({ label, children }) {
return (
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
<label style={{ fontSize: 11, fontWeight: 600, color: "#8a7a6a", letterSpacing: 0.5, textTransform: "uppercase" }}>{label}</label>
{children}
</div>
);
}
const inputStyle = {
background: "#1e1a14",
border: "1px solid #3a3020",
borderRadius: 6,
color: "#e8dfc8",
padding: "7px 10px",
fontSize: 13,
fontFamily: "inherit",
outline: "none",
width: "100%",
boxSizing: "border-box",
transition: "border-color 0.2s",
};
export default function App() {
const [card, setCard] = useState(DEFAULT_CARD);
const [imageUrl, setImageUrl] = useState("");
const [imageInput, setImageInput] = useState("");
const [tab, setTab] = useState("basic");
const previewRef = useRef();
const fileInputRef = useRef();
const update = useCallback((field, val) => setCard(c => ({ ...c, [field]: val })), []);
const handleImageFile = (e) => {
const file = e.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (ev) => setImageUrl(ev.target.result);
reader.readAsDataURL(file);
};
const handleImageUrl = () => {
setImageUrl(imageInput.trim());
};
const handlePrint = () => window.print();
const isPW = card.type === "Planeswalker";
const isCreature = card.type === "Creature" || card.type === "Battle";
return (
<div style={{
minHeight: "100vh",
background: "#0e0b07",
fontFamily: "'Palatino Linotype', Palatino, 'Book Antiqua', serif",
color: "#e8dfc8",
display: "flex",
flexDirection: "column",
}}>
{/* Header */}
<div style={{
background: "linear-gradient(90deg,#1a0f00,#2e1a00,#1a0f00)",
borderBottom: "2px solid #5a3a10",
padding: "18px 28px",
display: "flex",
alignItems: "center",
gap: 14,
}}>
<div style={{ fontSize: 30 }}>🃏</div>
<div>
<div style={{ fontSize: 22, fontWeight: 700, letterSpacing: 1, color: "#d4af37" }}>MTG Proxy Maker</div>
<div style={{ fontSize: 12, color: "#8a7060", letterSpacing: 0.3 }}>Design · Print · Play</div>
</div>
<button onClick={handlePrint} style={{
marginLeft: "auto",
background: "linear-gradient(135deg,#8b6010,#d4af37,#8b6010)",
color: "#1a0f00",
border: "none",
borderRadius: 8,
padding: "9px 22px",
fontWeight: 700,
fontSize: 14,
cursor: "pointer",
letterSpacing: 0.5,
fontFamily: "inherit",
boxShadow: "0 2px 8px #d4af3744",
}}>
🖨 Print
</button>
</div>
<div style={{ display: "flex", flex: 1, padding: "24px 28px", gap: 32, flexWrap: "wrap" }}>
{/* Editor Panel */}
<div style={{ flex: "1 1 360px", minWidth: 300, maxWidth: 480, display: "flex", flexDirection: "column", gap: 16 }}>
{/* Tabs */}
<div style={{ display: "flex", gap: 4, borderBottom: "1px solid #3a3020", paddingBottom: 4 }}>
{["basic", "art", "stats"].map(t => (
<button key={t} onClick={() => setTab(t)} style={{
background: tab === t ? "#3a2a10" : "transparent",
border: tab === t ? "1px solid #d4af37" : "1px solid transparent",
color: tab === t ? "#d4af37" : "#8a7060",
borderRadius: 6,
padding: "5px 14px",
cursor: "pointer",
fontSize: 13,
fontFamily: "inherit",
fontWeight: tab === t ? 700 : 400,
textTransform: "capitalize",
letterSpacing: 0.3,
}}>{t}</button>
))}
</div>
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
{tab === "basic" && <>
<Field label="Card Name">
<input style={inputStyle} value={card.name} onChange={e => update("name", e.target.value)} placeholder="e.g. Blazing Champion" />
</Field>
<Field label="Mana Cost">
<input style={inputStyle} value={card.manaCost} onChange={e => update("manaCost", e.target.value)} placeholder="{2}{R}{R}" />
</Field>
<div style={{ display: "flex", gap: 10 }}>
<Field label="Type">
<select style={{ ...inputStyle }} value={card.type} onChange={e => update("type", e.target.value)}>
{CARD_TYPES.map(t => <option key={t} value={t}>{t}</option>)}
</select>
</Field>
<Field label="Subtype">
<input style={inputStyle} value={card.subtype} onChange={e => update("subtype", e.target.value)} placeholder="e.g. Wizard" />
</Field>
</div>
<Field label="Color">
<div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
{COLORS.map(c => (
<button key={c.id} onClick={() => update("color", c.id)} style={{
width: 36, height: 36, borderRadius: "50%",
background: c.id === "M" ? "linear-gradient(135deg,gold,blue,black)" : c.bg,
border: card.color === c.id ? "3px solid #d4af37" : `2px solid ${c.border}`,
cursor: "pointer",
fontWeight: 700, fontSize: 15,
boxShadow: card.color === c.id ? "0 0 8px #d4af37" : "none",
transition: "all 0.15s",
}} title={c.label}>{c.symbol}</button>
))}
</div>
</Field>
<Field label="Rarity">
<select style={{ ...inputStyle }} value={card.rarity} onChange={e => update("rarity", e.target.value)}>
{Object.keys(RARITY_COLORS).map(r => <option key={r} value={r}>{r}</option>)}
</select>
</Field>
<Field label="Rules Text">
<textarea style={{ ...inputStyle, minHeight: 80, resize: "vertical" }} value={card.rulesText} onChange={e => update("rulesText", e.target.value)} placeholder="Card abilities..." />
</Field>
<Field label="Flavor Text">
<textarea style={{ ...inputStyle, minHeight: 50, resize: "vertical", fontStyle: "italic" }} value={card.flavorText} onChange={e => update("flavorText", e.target.value)} placeholder='"A whisper in the dark..."' />
</Field>
<Field label="Artist">
<input style={inputStyle} value={card.artist} onChange={e => update("artist", e.target.value)} placeholder="Artist name" />
</Field>
</>}
{tab === "art" && <>
<Field label="Upload Card Art">
<input ref={fileInputRef} type="file" accept="image/*" onChange={handleImageFile} style={{ display: "none" }} />
<button onClick={() => fileInputRef.current?.click()} style={{
...inputStyle, cursor: "pointer", textAlign: "center", background: "#2a1e0e",
border: "2px dashed #5a3a10", padding: "20px", fontSize: 14,
}}>
📁 Click to upload image
</button>
</Field>
<div style={{ color: "#8a7060", fontSize: 12, textAlign: "center" }}>— or paste an image URL —</div>
<Field label="Image URL">
<div style={{ display: "flex", gap: 6 }}>
<input style={{ ...inputStyle, flex: 1 }} value={imageInput} onChange={e => setImageInput(e.target.value)} placeholder="https://..." />
<button onClick={handleImageUrl} style={{
background: "#3a2a10", border: "1px solid #d4af37", color: "#d4af37",
borderRadius: 6, padding: "0 12px", cursor: "pointer", fontFamily: "inherit", fontSize: 13,
}}>Set</button>
</div>
</Field>
{imageUrl && (
<div>
<img src={imageUrl} alt="preview" style={{ width: "100%", borderRadius: 8, border: "1px solid #3a3020", maxHeight: 180, objectFit: "cover" }} />
<button onClick={() => { setImageUrl(""); setImageInput(""); }} style={{
marginTop: 6, background: "transparent", border: "1px solid #5a3020", color: "#c06040",
borderRadius: 6, padding: "4px 12px", cursor: "pointer", fontSize: 12, fontFamily: "inherit", width: "100%",
}}>Remove Image</button>
</div>
)}
<Field label="Set Symbol">
<input style={inputStyle} value={card.setSymbol} onChange={e => update("setSymbol", e.target.value)} placeholder="★ or set code" maxLength={4} />
</Field>
</>}
{tab === "stats" && <>
{isCreature && <>
<div style={{ display: "flex", gap: 10 }}>
<Field label="Power">
<input style={inputStyle} value={card.power} onChange={e => update("power", e.target.value)} placeholder="4" />
</Field>
<Field label="Toughness">
<input style={inputStyle} value={card.toughness} onChange={e => update("toughness", e.target.value)} placeholder="3" />
</Field>
</div>
</>}
{isPW && (
<Field label="Starting Loyalty">
<input style={inputStyle} value={card.loyalty} onChange={e => update("loyalty", e.target.value)} placeholder="4" />
</Field>
)}
{!isCreature && !isPW && (
<div style={{ color: "#8a7060", fontSize: 13, fontStyle: "italic", padding: "12px 0" }}>
No stats for this card type. Switch to Creature or Planeswalker to edit stats.
</div>
)}
<div style={{ borderTop: "1px solid #3a3020", paddingTop: 12 }}>
<div style={{ fontSize: 12, color: "#8a7060", marginBottom: 8 }}>Quick Reset</div>
<button onClick={() => setCard(DEFAULT_CARD)} style={{
background: "#2a1e0e", border: "1px solid #5a3a10", color: "#c8a060",
borderRadius: 6, padding: "7px 16px", cursor: "pointer", fontFamily: "inherit", fontSize: 13,
}}>↩ Reset to Default</button>
</div>
</>}
</div>
</div>
{/* Preview */}
<div style={{ flex: "1 1 300px", display: "flex", flexDirection: "column", alignItems: "center", gap: 20 }}>
<div style={{ fontSize: 12, color: "#8a7060", letterSpacing: 1, textTransform: "uppercase" }}>Preview</div>
<div ref={previewRef} id="card-preview" style={{ transform: "scale(1)", transformOrigin: "top center" }}>
<CardPreview card={card} imageUrl={imageUrl} />
</div>
<div style={{ fontSize: 11, color: "#5a4a30", textAlign: "center", maxWidth: 280 }}>
Use the Print button to print on card stock.<br />
Cut to 2.5" × 3.5" for standard size.
</div>
</div>
</div>
<style>{`
@media print {
body * { visibility: hidden; }
#card-preview, #card-preview * { visibility: visible; }
#card-preview {
position: fixed; left: 50%; top: 50%;
transform: translate(-50%,-50%) scale(2.5);
transform-origin: center center;
}
}
input:focus, textarea:focus, select:focus {
border-color: #d4af37 !important;
box-shadow: 0 0 0 2px #d4af3733;
}
select option { background: #1e1a14; color: #e8dfc8; }
* { box-sizing: border-box; }
`}</style>
</div>
);
}