Create index.html

This commit is contained in:
Teddy-1024
2026-04-11 02:03:38 +01:00
committed by GitHub
parent 087bced666
commit 6f9c362f37

448
index.html Normal file
View File

@@ -0,0 +1,448 @@
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>
);
}