Update index.html

This commit is contained in:
Teddy-1024
2026-04-11 02:52:37 +01:00
committed by GitHub
parent 6f9c362f37
commit 84bb0c4460

View File

@@ -1,448 +1,453 @@
import { useState, useRef, useCallback } from "react"; <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MTG Proxy Maker</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
min-height: 100vh;
background: #0e0b07;
font-family: 'Palatino Linotype', Palatino, 'Book Antiqua', serif;
color: #e8dfc8;
}
header {
background: linear-gradient(90deg,#1a0f00,#2e1a00,#1a0f00);
border-bottom: 2px solid #5a3a10;
padding: 16px 24px;
display: flex;
align-items: center;
gap: 14px;
}
header .logo { font-size: 28px; }
header h1 { font-size: 20px; font-weight: 700; color: #d4af37; letter-spacing: 1px; }
header p { font-size: 12px; color: #8a7060; }
#printBtn {
margin-left: auto;
background: linear-gradient(135deg,#8b6010,#d4af37,#8b6010);
color: #1a0f00;
border: none;
border-radius: 8px;
padding: 9px 22px;
font-weight: 700;
font-size: 14px;
cursor: pointer;
font-family: inherit;
box-shadow: 0 2px 8px #d4af3744;
}
.main {
display: flex;
flex-wrap: wrap;
gap: 32px;
padding: 24px;
}
.editor {
flex: 1 1 360px;
max-width: 480px;
display: flex;
flex-direction: column;
gap: 14px;
}
.tabs {
display: flex;
gap: 4px;
border-bottom: 1px solid #3a3020;
padding-bottom: 4px;
}
.tab {
background: transparent;
border: 1px solid transparent;
color: #8a7060;
border-radius: 6px;
padding: 5px 14px;
cursor: pointer;
font-size: 13px;
font-family: inherit;
text-transform: capitalize;
}
.tab.active { background: #3a2a10; border-color: #d4af37; color: #d4af37; font-weight: 700; }
.tab-panel { display: none; flex-direction: column; gap: 12px; }
.tab-panel.active { display: flex; }
.field { display: flex; flex-direction: column; gap: 4px; }
.field label { font-size: 11px; font-weight: 600; color: #8a7a6a; letter-spacing: 0.5px; text-transform: uppercase; }
input, textarea, select {
background: #1e1a14; border: 1px solid #3a3020; border-radius: 6px;
color: #e8dfc8; padding: 7px 10px; font-size: 13px; font-family: inherit;
outline: none; width: 100%;
}
input:focus, textarea:focus, select:focus { border-color: #d4af37; box-shadow: 0 0 0 2px #d4af3733; }
select option { background: #1e1a14; color: #e8dfc8; }
textarea { resize: vertical; }
.row { display: flex; gap: 10px; }
.row .field { flex: 1; }
.color-btns { display: flex; gap: 6px; flex-wrap: wrap; }
.color-btn {
width: 36px; height: 36px; border-radius: 50%; border: 2px solid transparent;
cursor: pointer; font-size: 16px; display: flex; align-items: center; justify-content: center;
}
.color-btn.active { border: 3px solid #d4af37 !important; box-shadow: 0 0 8px #d4af37; }
.upload-btn {
background: #2a1e0e; border: 2px dashed #5a3a10; border-radius: 6px;
color: #e8dfc8; padding: 20px; font-size: 14px; font-family: inherit;
cursor: pointer; width: 100%; text-align: center;
}
.url-row { display: flex; gap: 6px; }
.url-row input { flex: 1; }
.set-btn {
background: #3a2a10; border: 1px solid #d4af37; color: #d4af37;
border-radius: 6px; padding: 0 12px; cursor: pointer; font-family: inherit; font-size: 13px;
}
#artPreview { display: none; flex-direction: column; gap: 6px; }
#artPreview img { width: 100%; border-radius: 8px; border: 1px solid #3a3020; max-height: 180px; object-fit: cover; }
.remove-btn {
background: transparent; border: 1px solid #5a3020; color: #c06040;
border-radius: 6px; padding: 4px 12px; cursor: pointer; font-size: 12px; font-family: inherit; width: 100%;
}
.reset-btn {
background: #2a1e0e; border: 1px solid #5a3a10; color: #c8a060;
border-radius: 6px; padding: 7px 16px; cursor: pointer; font-family: inherit; font-size: 13px;
}
.divider { border-top: 1px solid #3a3020; padding-top: 12px; }
.divider-label { font-size: 12px; color: #8a7060; margin-bottom: 8px; }
.no-stats { font-size: 13px; color: #8a7060; font-style: italic; padding: 12px 0; }
.or-divider { color: #8a7060; font-size: 12px; text-align: center; }
.preview-col { flex: 1 1 300px; display: flex; flex-direction: column; align-items: center; gap: 20px; }
.preview-label { font-size: 12px; color: #8a7060; letter-spacing: 1px; text-transform: uppercase; }
.preview-hint { font-size: 11px; color: #5a4a30; text-align: center; max-width: 280px; line-height: 1.6; }
const CARD_TYPES = ["Creature", "Instant", "Sorcery", "Enchantment", "Artifact", "Land", "Planeswalker", "Battle"]; /* Card styles */
const COLORS = [ #card {
{ id: "W", label: "White", symbol: "☀", bg: "#f9f6d5", border: "#c8b87a", text: "#333" }, width: 280px; min-height: 390px; border-radius: 16px; border: 4px solid #c8b87a;
{ id: "U", label: "Blue", symbol: "💧", bg: "#c6d9e8", border: "#3a6b9c", text: "#fff" }, background: #f9f6d5; box-shadow: 0 0 0 2px #111, 0 8px 32px #0008;
{ id: "B", label: "Black", symbol: "💀", bg: "#1a1a1a", border: "#5a4a6e", text: "#d0c8e0" }, display: flex; flex-direction: column; overflow: hidden; position: relative;
{ id: "R", label: "Red", symbol: "🔥", bg: "#e87040", border: "#8b2500", text: "#fff" }, color: #333;
{ id: "G", label: "Green", symbol: "🌲", bg: "#3a7a48", border: "#1a4025", text: "#d4f5d0" }, }
{ id: "C", label: "Colorless", symbol: "◇", bg: "#c0bdb5", border: "#7a7068", text: "#333" }, #cardHeader {
{ id: "M", label: "Multi", symbol: "★", bg: "linear-gradient(135deg,#f9f6d5,#c6d9e8,#1a1a1a,#e87040,#3a7a48)", border: "#b8960c", text: "#fff" }, display: flex; justify-content: space-between; align-items: center;
]; padding: 7px 10px 4px; border-bottom: 2px solid #c8b87a; background: #c8b87acc;
}
#cardName { font-weight: 700; font-size: 15px; letter-spacing: 0.3px; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#cardMana { font-size: 13px; font-family: monospace; margin-left: 8px; white-space: nowrap; letter-spacing: 1px; }
#artFrame {
margin: 6px 8px; height: 140px; border-radius: 8px; border: 2px solid #c8b87a;
overflow: hidden; display: flex; align-items: center; justify-content: center;
position: relative; background: linear-gradient(135deg,#c8b87a44,#c8b87a99);
}
#artFrame img { width: 100%; height: 100%; object-fit: cover; }
#artPlaceholder { opacity: 0.4; font-size: 48px; }
#setRarity { position: absolute; bottom: 4px; right: 6px; font-size: 11px; font-weight: 700; text-shadow: 0 1px 3px #000a; }
#typeLine {
display: flex; justify-content: space-between; align-items: center;
padding: 3px 10px; font-size: 12px; font-style: italic;
border-top: 1px solid #c8b87a66; border-bottom: 1px solid #c8b87a66; background: #c8b87a99;
}
#textBox {
flex: 1; margin: 5px 8px; background: #f5f0e044; border-radius: 6px;
border: 1px solid #c8b87a55; padding: 7px 9px; min-height: 80px;
display: flex; flex-direction: column; gap: 6px;
}
#rulesDisplay { font-size: 12px; line-height: 1.45; white-space: pre-wrap; }
#flavorDisplay {
font-size: 11px; font-style: italic; opacity: 0.75;
border-top: 1px solid #c8b87a44; padding-top: 5px; line-height: 1.35; display: none;
}
#cardFooter {
display: flex; justify-content: space-between; align-items: center;
padding: 4px 10px 6px; font-size: 10px; opacity: 0.7; border-top: 1px solid #c8b87a44;
}
#ptBox {
background: #c8b87a; color: #fff; border-radius: 4px; padding: 1px 7px;
font-weight: 700; font-size: 13px; box-shadow: 0 1px 4px #0006; display: none;
}
#loyaltyBox {
background: #1a5ab8; color: #fff; border-radius: 50%; width: 26px; height: 26px;
display: none; align-items: center; justify-content: center;
font-weight: 700; font-size: 14px; box-shadow: 0 1px 4px #0006;
}
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 { @media print {
body * { visibility: hidden; } body * { visibility: hidden; }
#card-preview, #card-preview * { visibility: visible; } #card, #card * { visibility: visible; }
#card-preview { #card { position: fixed; left: 50%; top: 50%; transform: translate(-50%,-50%) scale(2.5); }
position: fixed; left: 50%; top: 50%;
transform: translate(-50%,-50%) scale(2.5);
transform-origin: center center;
} }
} </style>
input:focus, textarea:focus, select:focus { </head>
border-color: #d4af37 !important; <body>
box-shadow: 0 0 0 2px #d4af3733;
} <header>
select option { background: #1e1a14; color: #e8dfc8; } <div class="logo">🃏</div>
* { box-sizing: border-box; } <div>
`}</style> <h1>MTG Proxy Maker</h1>
<p>Design · Print · Play</p>
</div> </div>
); <button id="printBtn" onclick="window.print()">🖨 Print</button>
</header>
<div class="main">
<div class="editor">
<div class="tabs">
<button class="tab active" onclick="switchTab('basic',this)">Basic</button>
<button class="tab" onclick="switchTab('art',this)">Art</button>
<button class="tab" onclick="switchTab('stats',this)">Stats</button>
</div>
<div class="tab-panel active" id="tab-basic">
<div class="field"><label>Card Name</label><input id="f-name" value="Blazing Champion" oninput="update()"></div>
<div class="field"><label>Mana Cost</label><input id="f-mana" value="{2}{R}{R}" oninput="update()"></div>
<div class="row">
<div class="field"><label>Type</label>
<select id="f-type" onchange="update()">
<option>Creature</option><option>Instant</option><option>Sorcery</option>
<option>Enchantment</option><option>Artifact</option><option>Land</option>
<option>Planeswalker</option><option>Battle</option>
</select>
</div>
<div class="field"><label>Subtype</label><input id="f-subtype" value="Human Warrior" oninput="update()"></div>
</div>
<div class="field">
<label>Color</label>
<div class="color-btns">
<button class="color-btn" data-color="W" style="background:#f9f6d5" onclick="setColor('W')" title="White"></button>
<button class="color-btn" data-color="U" style="background:#c6d9e8" onclick="setColor('U')" title="Blue">💧</button>
<button class="color-btn" data-color="B" style="background:#1a1a1a;color:#d0c8e0" onclick="setColor('B')" title="Black">💀</button>
<button class="color-btn active" data-color="R" style="background:#e87040" onclick="setColor('R')" title="Red">🔥</button>
<button class="color-btn" data-color="G" style="background:#3a7a48;color:#d4f5d0" onclick="setColor('G')" title="Green">🌲</button>
<button class="color-btn" data-color="C" style="background:#c0bdb5" onclick="setColor('C')" title="Colorless"></button>
<button class="color-btn" data-color="M" style="background:linear-gradient(135deg,gold,blue,black)" onclick="setColor('M')" title="Multi"></button>
</div>
</div>
<div class="field"><label>Rarity</label>
<select id="f-rarity" onchange="update()">
<option>Common</option><option>Uncommon</option><option selected>Rare</option><option>Mythic Rare</option>
</select>
</div>
<div class="field"><label>Rules Text</label>
<textarea id="f-rules" rows="4" oninput="update()">Haste
When Blazing Champion enters the battlefield, it deals 2 damage to any target.</textarea>
</div>
<div class="field"><label>Flavor Text</label>
<textarea id="f-flavor" rows="2" oninput="update()" style="font-style:italic">"No retreat. No mercy. Only fire."</textarea>
</div>
<div class="field"><label>Artist</label><input id="f-artist" value="Proxy Artist" oninput="update()"></div>
</div>
<div class="tab-panel" id="tab-art">
<div class="field">
<label>Upload Card Art</label>
<input type="file" id="fileInput" accept="image/*" style="display:none" onchange="handleFile(event)">
<button class="upload-btn" onclick="document.getElementById('fileInput').click()">📁 Click to upload image</button>
</div>
<div class="or-divider">— or paste an image URL —</div>
<div class="field">
<label>Image URL</label>
<div class="url-row">
<input id="urlInput" placeholder="https://...">
<button class="set-btn" onclick="setImageUrl()">Set</button>
</div>
</div>
<div id="artPreview">
<img id="artThumb" src="" alt="preview">
<button class="remove-btn" onclick="removeImage()">Remove Image</button>
</div>
<div class="field"><label>Set Symbol</label><input id="f-set" value="★" oninput="update()" maxlength="4"></div>
</div>
<div class="tab-panel" id="tab-stats">
<div id="creatureStats">
<div class="row">
<div class="field"><label>Power</label><input id="f-power" value="4" oninput="update()"></div>
<div class="field"><label>Toughness</label><input id="f-toughness" value="3" oninput="update()"></div>
</div>
</div>
<div id="pwStats" style="display:none">
<div class="field"><label>Starting Loyalty</label><input id="f-loyalty" value="4" oninput="update()"></div>
</div>
<div id="noStats" class="no-stats" style="display:none">No stats for this card type.</div>
<div class="divider">
<div class="divider-label">Quick Reset</div>
<button class="reset-btn" onclick="resetCard()">↩ Reset to Default</button>
</div>
</div>
</div>
<div class="preview-col">
<div class="preview-label">Preview</div>
<div id="card">
<div id="cardHeader">
<div id="cardName">Blazing Champion</div>
<div id="cardMana">{2}{R}{R}</div>
</div>
<div id="artFrame">
<div id="artPlaceholder">🔥</div>
<img id="cardArt" src="" alt="art" style="display:none;width:100%;height:100%;object-fit:cover">
<div id="setRarity" style="color:#d4af37">★ R</div>
</div>
<div id="typeLine">
<span id="typeDisplay">Creature — Human Warrior</span>
<span id="setDisplay"></span>
</div>
<div id="textBox">
<div id="rulesDisplay"></div>
<div id="flavorDisplay"></div>
</div>
<div id="cardFooter">
<span id="artistDisplay">✦ Proxy Artist</span>
<div id="ptBox">4/3</div>
<div id="loyaltyBox" style="display:none">4</div>
</div>
</div>
<div class="preview-hint">Use the Print button to print on card stock.<br>Cut to 2.5" × 3.5" for standard size.</div>
</div>
</div>
<script>
const COLORS = {
W: { bg:'#f9f6d5', border:'#c8b87a', text:'#333', symbol:'☀' },
U: { bg:'#c6d9e8', border:'#3a6b9c', text:'#fff', symbol:'💧' },
B: { bg:'#1a1a1a', border:'#5a4a6e', text:'#d0c8e0', symbol:'💀' },
R: { bg:'#e87040', border:'#8b2500', text:'#fff', symbol:'🔥' },
G: { bg:'#3a7a48', border:'#1a4025', text:'#d4f5d0', symbol:'🌲' },
C: { bg:'#c0bdb5', border:'#7a7068', text:'#333', symbol:'◇' },
M: { bg:'linear-gradient(160deg,#f9f6d5,#c6d9e8,#1a1a1a,#e87040,#3a7a48)', border:'#b8960c', text:'#fff', symbol:'★' },
};
const RARITY_COLORS = { Common:'#aaa', Uncommon:'#a0c0d0', Rare:'#d4af37', 'Mythic Rare':'#e07020' };
let currentColor = 'R';
let currentImageUrl = '';
function setColor(c) {
currentColor = c;
document.querySelectorAll('.color-btn').forEach(b => b.classList.remove('active'));
document.querySelector('[data-color="'+c+'"]').classList.add('active');
update();
} }
function update() {
const col = COLORS[currentColor];
const type = document.getElementById('f-type').value;
const rarity = document.getElementById('f-rarity').value;
const sym = document.getElementById('f-set').value || '★';
const card = document.getElementById('card');
card.style.borderColor = col.border;
card.style.background = col.bg;
card.style.color = col.text;
card.style.boxShadow = '0 0 0 2px #111, 0 0 30px '+col.border+'55, 0 8px 32px #0008';
const hdr = document.getElementById('cardHeader');
hdr.style.background = col.border+'cc';
hdr.style.borderBottomColor = col.border;
document.getElementById('cardName').textContent = document.getElementById('f-name').value || 'Card Name';
document.getElementById('cardMana').textContent = document.getElementById('f-mana').value;
const af = document.getElementById('artFrame');
af.style.borderColor = col.border;
af.style.background = currentImageUrl ? 'transparent' : 'linear-gradient(135deg,'+col.border+'44,'+col.border+'99)';
document.getElementById('artPlaceholder').textContent = col.symbol;
const rc = RARITY_COLORS[rarity]||'#aaa';
document.getElementById('setRarity').style.color = rc;
document.getElementById('setRarity').textContent = sym+' '+(rarity?rarity[0]:'C');
const tl = document.getElementById('typeLine');
tl.style.background = col.border+'99';
const sub = document.getElementById('f-subtype').value;
document.getElementById('typeDisplay').textContent = [type,sub].filter(Boolean).join(' — ');
document.getElementById('setDisplay').textContent = sym;
document.getElementById('textBox').style.borderColor = col.border+'55';
document.getElementById('rulesDisplay').textContent = document.getElementById('f-rules').value;
const flavor = document.getElementById('f-flavor').value;
const fd = document.getElementById('flavorDisplay');
if (flavor) { fd.textContent = flavor; fd.style.display='block'; } else { fd.style.display='none'; }
document.getElementById('cardFooter').style.borderTopColor = col.border+'44';
document.getElementById('artistDisplay').textContent = '✦ '+(document.getElementById('f-artist').value||'Unknown');
const ptBox = document.getElementById('ptBox');
const loyBox = document.getElementById('loyaltyBox');
ptBox.style.background = col.border;
const isCreature = type==='Creature'||type==='Battle';
const isPW = type==='Planeswalker';
if (isCreature) {
ptBox.style.display='inline-block';
ptBox.textContent = document.getElementById('f-power').value+'/'+document.getElementById('f-toughness').value;
loyBox.style.display='none';
} else if (isPW) {
loyBox.style.display='flex';
loyBox.textContent = document.getElementById('f-loyalty').value;
ptBox.style.display='none';
} else {
ptBox.style.display='none';
loyBox.style.display='none';
}
document.getElementById('creatureStats').style.display = isCreature?'block':'none';
document.getElementById('pwStats').style.display = isPW?'block':'none';
document.getElementById('noStats').style.display = (!isCreature&&!isPW)?'block':'none';
}
function switchTab(name, btn) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
btn.classList.add('active');
document.querySelectorAll('.tab-panel').forEach(p => {
p.classList.toggle('active', p.id==='tab-'+name);
});
}
function handleFile(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = ev => applyImage(ev.target.result);
reader.readAsDataURL(file);
}
function setImageUrl() {
const url = document.getElementById('urlInput').value.trim();
if (url) applyImage(url);
}
function applyImage(url) {
currentImageUrl = url;
const img = document.getElementById('cardArt');
img.src = url; img.style.display='block';
document.getElementById('artPlaceholder').style.display='none';
document.getElementById('artThumb').src = url;
document.getElementById('artPreview').style.display='flex';
update();
}
function removeImage() {
currentImageUrl = '';
const img = document.getElementById('cardArt');
img.src=''; img.style.display='none';
document.getElementById('artPlaceholder').style.display='block';
document.getElementById('artPreview').style.display='none';
document.getElementById('urlInput').value='';
document.getElementById('fileInput').value='';
update();
}
function resetCard() {
document.getElementById('f-name').value='Blazing Champion';
document.getElementById('f-mana').value='{2}{R}{R}';
document.getElementById('f-type').value='Creature';
document.getElementById('f-subtype').value='Human Warrior';
document.getElementById('f-rarity').value='Rare';
document.getElementById('f-rules').value='Haste\nWhen Blazing Champion enters the battlefield, it deals 2 damage to any target.';
document.getElementById('f-flavor').value='"No retreat. No mercy. Only fire."';
document.getElementById('f-artist').value='Proxy Artist';
document.getElementById('f-power').value='4';
document.getElementById('f-toughness').value='3';
document.getElementById('f-loyalty').value='4';
document.getElementById('f-set').value='★';
setColor('R');
removeImage();
}
update();
</script>
</body>
</html>