import { useState, useEffect, useRef, useCallback } from "react";
// ═══ 占星術データ ═══
const SIGNS = ['牡羊座','牡牛座','双子座','蟹座','獅子座','乙女座','天秤座','蠍座','射手座','山羊座','水瓶座','魚座'];
const SSYM = ['♈','♉','♊','♋','♌','♍','♎','♏','♐','♑','♒','♓'];
const PSYM = {Sun:'☉',Moon:'☽',Mercury:'☿',Venus:'♀',Mars:'♂',Jupiter:'♃',Saturn:'♄',Uranus:'♅',Neptune:'♆',Pluto:'♇'};
const PJP = {Sun:'太陽',Moon:'月',Mercury:'水星',Venus:'金星',Mars:'火星',Jupiter:'木星',Saturn:'土星',Uranus:'天王星',Neptune:'海王星',Pluto:'冥王星'};
const PCOL = {Sun:'#c89840',Moon:'#7090c0',Mercury:'#40a890',Venus:'#d07890',Mars:'#d06050',Jupiter:'#c09050',Saturn:'#8090b0',Uranus:'#50b8c0',Neptune:'#6870d8',Pluto:'#a060b8'};
const PACC = {Sun:'linear-gradient(90deg,#c89840,#e8c070)',Moon:'linear-gradient(90deg,#7090c0,#a0b8e0)',Mercury:'linear-gradient(90deg,#40a890,#70d0b0)',Venus:'linear-gradient(90deg,#d07890,#f0a8b8)',Mars:'linear-gradient(90deg,#d06050,#f08070)',Jupiter:'linear-gradient(90deg,#c09050,#e0b870)',Saturn:'linear-gradient(90deg,#8090b0,#a8b8d0)',Uranus:'linear-gradient(90deg,#50b8c0,#80d8e0)',Neptune:'linear-gradient(90deg,#6870d8,#9090f0)',Pluto:'linear-gradient(90deg,#a060b8,#c888d8)'};
const FORTUNE_COLORS = [
{bar:'linear-gradient(90deg,#c8a87a,#a8885a)', dot:'#c8a87a'},
{bar:'linear-gradient(90deg,#7aa4d4,#4a6fa5)', dot:'#4a6fa5'},
{bar:'linear-gradient(90deg,#e09aaa,#c86478)', dot:'#c86478'},
{bar:'linear-gradient(90deg,#7abd8a,#4a8c5c)', dot:'#4a8c5c'},
];
// ═══ 天文計算 ═══
function toJD(y,m,d,h,mn){ if(m<=2){y--;m+=12;} const A=Math.floor(y/100),B=2-A+Math.floor(A/4); return Math.floor(365.25*(y+4716))+Math.floor(30.6001*(m+1))+d+B-1524.5+(h+mn/60)/24; }
function calcDeg(jd,p){ const T=(jd-2451545)/36525; const base={Sun:280.46646+36000.76983*T,Moon:218.3165+481267.8813*T,Mercury:252.2509+149472.6746*T,Venus:181.9798+58517.8157*T,Mars:355.4330+19140.2993*T,Jupiter:34.3515+3034.9057*T,Saturn:50.0775+1221.8760*T,Uranus:314.0550+428.4748*T,Neptune:304.3487+218.4071*T,Pluto:238.9260+145.2078*T}; return((base[p]||0)%360+360)%360; }
function dts(deg){ const i=Math.floor(((deg%360)+360)%360/30); return{idx:i,within:(((deg%360)+360)%360%30).toFixed(1),sign:SIGNS[i],sym:SSYM[i]}; }
function buildPos(y,m,d,hh,mn){
const jd=toJD(y,m,d,hh||12,mn||0);
const PL=['Sun','Moon','Mercury','Venus','Mars','Jupiter','Saturn','Uranus','Neptune','Pluto'];
const pos={}; PL.forEach(p=>pos[p]=calcDeg(jd,p));
pos.Ascendant=((pos.Sun+90+(hh-6)*15)%360+360)%360;
return pos;
}
// ═══ Claude API ═══
async function callClaude(prompt) {
const res = await fetch("https://api.anthropic.com/v1/messages", {
method:"POST", headers:{"Content-Type":"application/json"},
body: JSON.stringify({ model:"claude-sonnet-4-20250514", max_tokens:1800,
messages:[{role:"user", content:prompt}] })
});
const data = await res.json();
if(data.error) throw new Error(data.error.message);
const text = data.content?.map(c=>c.text||"").join("")||"";
const m = text.match(/\{[\s\S]*\}/);
if(!m) throw new Error("レスポンスの解析に失敗しました");
return JSON.parse(m[0]);
}
// ═══ ホイールキャンバス ═══
function ChartCanvas({ profile }) {
const ref = useRef(null);
useEffect(()=>{
if(!ref.current||!profile)return;
const cv=ref.current, ctx=cv.getContext("2d");
const W=360,CX=W/2,CY=W/2,R=155,BD="#e8e4de";
ctx.clearRect(0,0,W,W);
// 外周
ctx.beginPath(); ctx.arc(CX,CY,R,0,Math.PI*2); ctx.fillStyle="#fafaf8"; ctx.fill(); ctx.strokeStyle=BD; ctx.lineWidth=1; ctx.stroke();
// 12星座帯
for(let i=0;i<12;i++){
const a1=(i*30-90)*Math.PI/180, a2=((i+1)*30-90)*Math.PI/180;
ctx.beginPath(); ctx.moveTo(CX+Math.cos(a1)*R*.67,CY+Math.sin(a1)*R*.67);
ctx.arc(CX,CY,R*.67,a1,a2); ctx.arc(CX,CY,R,a2,a1,true); ctx.closePath();
ctx.fillStyle=`hsla(${i*30},28%,97%,.9)`; ctx.fill(); ctx.strokeStyle=BD; ctx.lineWidth=.7; ctx.stroke();
const am=(i*30+15-90)*Math.PI/180;
ctx.fillStyle="#9a9288"; ctx.font="11px serif"; ctx.textAlign="center"; ctx.textBaseline="middle";
ctx.fillText(SSYM[i], CX+Math.cos(am)*R*.84, CY+Math.sin(am)*R*.84);
}
// ハウス線
for(let i=0;i<12;i++){
const a=(i*30-90)*Math.PI/180;
ctx.beginPath(); ctx.moveTo(CX+Math.cos(a)*R*.43,CY+Math.sin(a)*R*.43); ctx.lineTo(CX+Math.cos(a)*R*.67,CY+Math.sin(a)*R*.67);
ctx.strokeStyle=BD; ctx.lineWidth=.7; ctx.stroke();
}
// 内円
ctx.beginPath(); ctx.arc(CX,CY,R*.43,0,Math.PI*2); ctx.fillStyle="#fff"; ctx.fill(); ctx.strokeStyle=BD; ctx.lineWidth=1; ctx.stroke();
// 惑星
const PL=["Sun","Moon","Mercury","Venus","Mars","Jupiter","Saturn","Uranus","Neptune","Pluto"], pp=[];
PL.forEach(p=>{
const deg=profile.pos[p], ang=(deg-90)*Math.PI/180, pr=R*.55;
const px=CX+Math.cos(ang)*pr, py=CY+Math.sin(ang)*pr;
pp.push({p,px,py,deg});
ctx.beginPath(); ctx.arc(px,py,4,0,Math.PI*2); ctx.fillStyle=PCOL[p]; ctx.fill();
ctx.fillStyle=PCOL[p]; ctx.font="bold 10px serif"; ctx.textAlign="center"; ctx.textBaseline="middle";
ctx.fillText(PSYM[p], px, py-11);
});
// アスペクト線
const ASPS=[{d:0,c:"rgba(200,168,122,.55)",w:1.5},{d:120,c:"rgba(74,140,92,.4)",w:1},{d:60,c:"rgba(74,111,165,.3)",w:.8},{d:90,c:"rgba(200,100,100,.3)",w:.8},{d:180,c:"rgba(160,120,60,.28)",w:.8}];
for(let i=0;i<pp.length;i++) for(let j=i+1;j<pp.length;j++){
let df=Math.abs(pp[i].deg-pp[j].deg); if(df>180)df=360-df;
for(const a of ASPS){ if(Math.abs(df-a.d)<7){ ctx.beginPath(); ctx.moveTo(pp[i].px,pp[i].py); ctx.lineTo(pp[j].px,pp[j].py); ctx.strokeStyle=a.c; ctx.lineWidth=a.w; ctx.stroke(); break; }}
}
// ASC線
const aa=(profile.pos.Ascendant-90)*Math.PI/180;
ctx.beginPath(); ctx.moveTo(CX-Math.cos(aa)*R*.67,CY-Math.sin(aa)*R*.67); ctx.lineTo(CX+Math.cos(aa)*R*.67,CY+Math.sin(aa)*R*.67);
ctx.strokeStyle="rgba(74,111,165,.5)"; ctx.lineWidth=1.3; ctx.setLineDash([4,3]); ctx.stroke(); ctx.setLineDash([]);
// 中央名前
ctx.fillStyle="#9a9288"; ctx.font="10px sans-serif"; ctx.textAlign="center"; ctx.textBaseline="middle";
ctx.fillText(profile.name, CX, CY);
}, [profile]);
return <canvas ref={ref} width={360} height={360} style={{maxWidth:"100%",display:"block"}} />;
}
// ═══ 星座タグ(クリック可能) ═══
function SignTag({ planet, pos, onOpen }) {
const s = dts(pos[planet]);
return (
<button onClick={()=>onOpen(planet, s.sign, s.sym)}
style={{fontSize:".76rem",padding:"3px 9px",borderRadius:20,background:"rgba(200,168,122,.12)",color:"#a8885a",border:"1px solid rgba(200,168,122,.4)",cursor:"pointer",fontWeight:500,transition:"all .15s",whiteSpace:"nowrap"}}
onMouseEnter={e=>{e.currentTarget.style.background="rgba(200,168,122,.28)";e.currentTarget.style.transform="scale(1.06)";}}
onMouseLeave={e=>{e.currentTarget.style.background="rgba(200,168,122,.12)";e.currentTarget.style.transform="scale(1)";}}
title="クリックで詳細表示"
>{s.sym} {s.sign}</button>
);
}
// ═══ 星座ポップアップ ═══
function SignPopup({ planet, signName, signSym, onClose }) {
const [data, setData] = useState(null);
const [err, setErr] = useState(null);
useEffect(()=>{
setData(null); setErr(null);
callClaude(
`西洋占星術において、${PJP[planet]}が${signName}にある人の特徴を日本語で教えてください。\n` +
`JSONのみで返してください(説明・コードブロック不要):\n` +
`{"element":"火","quality":"活動宮","ruler":"火星","keywords":["情熱的","行動力","直感","リーダーシップ","短気"],"personality":"性格の特徴(100〜130字)","strength":"強み(60〜80字)","weakness":"弱み・課題(60〜80字)","advice":"アドバイス(60〜80字)"}`
).then(setData).catch(e=>setErr(e.message));
}, [planet, signName]);
return (
<div onClick={e=>e.target===e.currentTarget&&onClose()} style={{position:"fixed",inset:0,zIndex:9999,background:"rgba(26,24,20,.42)",backdropFilter:"blur(3px)",display:"flex",alignItems:"center",justifyContent:"center",padding:20}}>
<div style={{background:"#fff",borderRadius:20,maxWidth:480,width:"100%",boxShadow:"0 24px 80px rgba(26,24,20,.22)",overflow:"hidden"}}>
{/* ヘッダー */}
<div style={{position:"relative",padding:"26px 26px 18px",borderBottom:"1px solid #ede9e2"}}>
<div style={{position:"absolute",top:0,left:0,right:0,height:3,background:PACC[planet]||"#c8a87a"}} />
<div style={{fontSize:".7rem",letterSpacing:".18em",textTransform:"uppercase",color:"#9a9288",marginBottom:5}}>{PJP[planet]}星座</div>
<div style={{display:"flex",alignItems:"center",gap:12}}>
<span style={{fontSize:"2rem"}}>{signSym}</span>
<div>
<div style={{fontFamily:"Georgia,serif",fontSize:"1.5rem",color:"#1a1814"}}>{signName}</div>
<div style={{fontSize:".82rem",color:"#9a9288",marginTop:3}}>の特徴</div>
</div>
</div>
<button onClick={onClose} style={{position:"absolute",top:14,right:14,width:28,height:28,borderRadius:"50%",background:"#f2f0ed",border:"none",cursor:"pointer",color:"#9a9288",fontSize:".9rem"}}>✕</button>
</div>
{/* ボディ */}
<div style={{padding:"20px 26px 26px",maxHeight:"68vh",overflowY:"auto"}}>
{!data&&!err&&<div style={{textAlign:"center",padding:"30px 0"}}><div style={{width:32,height:32,margin:"0 auto 12px",border:"2px solid #e8e4de",borderTopColor:"#c8a87a",borderRadius:"50%",animation:"spin 1s linear infinite"}} /><div style={{fontSize:".85rem",color:"#9a9288"}}>読み込んでいます…</div></div>}
{err&&<div style={{color:"#a04040",fontSize:".85rem",padding:"16px 0"}}>取得に失敗しました:{err}</div>}
{data&&<>
<div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:9,marginBottom:13}}>
{[["エレメント",data.element],["クオリティ",data.quality],["支配星",data.ruler],["キーワード",(data.keywords||[]).slice(0,3).join(" · ")]].map(([l,v])=>(
<div key={l} style={{background:"#f8f7f5",borderRadius:9,padding:"10px 13px",border:"1px solid #ede9e2"}}>
<div style={{fontSize:".66rem",letterSpacing:".12em",textTransform:"uppercase",color:"#9a9288",marginBottom:3}}>{l}</div>
<div style={{fontSize:".87rem",color:"#1a1814"}}>{v||"—"}</div>
</div>
))}
</div>
<div style={{display:"flex",flexWrap:"wrap",gap:5,marginBottom:14}}>
{(data.keywords||[]).map(k=><span key={k} style={{fontSize:".75rem",padding:"3px 9px",borderRadius:20,background:"rgba(200,168,122,.12)",color:"#a8885a",border:"1px solid rgba(200,168,122,.4)"}}>{k}</span>)}
</div>
{[["性格の特徴",data.personality],["強み",data.strength],["弱み・課題",data.weakness],["アドバイス",data.advice]].map(([l,v])=>(
<div key={l} style={{marginBottom:13}}>
<div style={{fontSize:".71rem",letterSpacing:".13em",textTransform:"uppercase",color:"#9a9288",marginBottom:5,display:"flex",alignItems:"center",gap:8}}>
{l}<span style={{flex:1,height:1,background:"#e8e4de",display:"block"}} />
</div>
<div style={{fontSize:".875rem",color:"#4a463e",lineHeight:1.85,fontWeight:300}}>{v}</div>
</div>
))}
</>}
</div>
</div>
</div>
);
}
// ═══ チャートパネル(本質・性格分析付き) ═══
function ChartPanel({ profile, onOpenPopup }) {
const [essence, setEssence] = useState(null);
const [essLoading, setEssLoading] = useState(false);
const [essErr, setEssErr] = useState(null);
useEffect(()=>{
if(!profile) return;
setEssence(null); setEssLoading(true); setEssErr(null);
const sun=dts(profile.pos.Sun), moon=dts(profile.pos.Moon), asc=dts(profile.pos.Ascendant);
const merc=dts(profile.pos.Mercury), ven=dts(profile.pos.Venus), mars=dts(profile.pos.Mars);
callClaude(
`あなたは西洋占星術の専門家です。以下の出生ホロスコープをもとに、この人の本質・性格・人生テーマを詳しく日本語で鑑定してください。\n\n` +
`【ホロスコープデータ】\n` +
`名前: ${profile.name}\n` +
`太陽: ${sun.sign} (${sun.within}°)\n` +
`月: ${moon.sign} (${moon.within}°)\n` +
`アセンダント: ${asc.sign}\n` +
`水星: ${merc.sign} / 金星: ${ven.sign} / 火星: ${mars.sign}\n\n` +
`JSONのみで返してください(説明・コードブロック不要):\n` +
`{"essence":"この人の本質・魂の傾向(150〜200字)","personality":"日常的な性格・振る舞い(150〜200字)","strength":"持って生まれた才能と強み(120〜150字)","challenge":"人生の課題と成長テーマ(100〜130字)","love":"恋愛・対人関係のスタイル(100〜130字)","career":"向いている仕事・才能の活かし方(100〜130字)","keyword":["キーワード1","キーワード2","キーワード3","キーワード4","キーワード5"]}`
).then(d=>{ setEssence(d); setEssLoading(false); }).catch(e=>{ setEssErr(e.message); setEssLoading(false); });
}, [profile]);
if(!profile) return (
<div style={{textAlign:"center",padding:"76px 20px"}}>
<span style={{fontSize:"2.2rem",opacity:.18,display:"block",marginBottom:14}}>⬡</span>
<div style={{fontSize:".93rem",color:"#9a9288"}}>出生情報を入力してホロスコープを生成してください</div>
</div>
);
const PL1=["Sun","Moon","Mercury","Venus","Mars"];
const PL2=["Jupiter","Saturn","Uranus","Neptune","Pluto"];
return (
<div>
{/* チャートラベル */}
<SectLabel>{profile.name} のネイタルチャート — {profile.dateStr}</SectLabel>
{/* チャート本体 */}
<div style={{display:"grid",gridTemplateColumns:"1fr 290px",gap:20,marginBottom:28}}>
<div style={{background:"#fff",border:"1px solid #ede9e2",borderRadius:16,padding:22,boxShadow:"0 2px 24px rgba(26,24,20,.07)",display:"flex",alignItems:"center",justifyContent:"center"}}>
<ChartCanvas profile={profile} />
</div>
<div style={{display:"flex",flexDirection:"column",gap:13}}>
{[["個人天体",PL1],["社会・世代天体",PL2]].map(([title,pl])=>(
<PlanetCard key={title} title={title}>
{pl.map(p=>(
<div key={p} style={{display:"flex",alignItems:"center",justifyContent:"space-between",padding:"5px 0",borderBottom:"1px solid #ede9e2"}}>
<div style={{fontSize:".85rem",color:"#4a463e",display:"flex",alignItems:"center",gap:7}}>
<span style={{color:PCOL[p],fontSize:".95rem",width:16,textAlign:"center"}}>{PSYM[p]}</span>{PJP[p]}
</div>
<SignTag planet={p} pos={profile.pos} onOpen={onOpenPopup} />
</div>
))}
</PlanetCard>
))}
<PlanetCard title="アングル">
{[["ASC(上昇点)",profile.pos.Ascendant],["MC(天頂)",(profile.pos.Ascendant+270)%360]].map(([lbl,deg],i)=>{
const s=dts(deg);
return <div key={lbl} style={{display:"flex",alignItems:"center",justifyContent:"space-between",padding:"5px 0",borderBottom:i===0?"1px solid #ede9e2":"none"}}>
<div style={{fontSize:".85rem",color:"#4a463e"}}>{lbl}</div>
<span style={{fontSize:".76rem",padding:"3px 9px",borderRadius:20,background:"rgba(200,168,122,.12)",color:"#a8885a",border:"1px solid rgba(200,168,122,.4)",fontWeight:500}}>{s.sym} {s.sign}</span>
</div>;
})}
</PlanetCard>
<div style={{fontSize:".72rem",color:"#9a9288",textAlign:"center",lineHeight:1.7,padding:"4px 0"}}>
星座タグをクリックすると<br/>その惑星×星座の特徴が表示されます
</div>
</div>
</div>
{/* ── 本質・性格分析セクション ── */}
<SectLabel>ホロスコープが語る あなたの本質</SectLabel>
{essLoading && (
<div style={{textAlign:"center",padding:"48px 0"}}>
<div style={{width:38,height:38,margin:"0 auto 14px",border:"2px solid #e8e4de",borderTopColor:"#c8a87a",borderRadius:"50%",animation:"spin 1s linear infinite"}} />
<div style={{fontSize:".88rem",color:"#9a9288"}}>星々からあなたの本質を読み取っています…</div>
</div>
)}
{essErr && <div style={{padding:"14px 18px",background:"#fef8f8",border:"1px solid #f0d0d0",borderRadius:9,fontSize:".83rem",color:"#a04040"}}>取得に失敗しました:{essErr}</div>}
{essence && (
<div>
{/* キーワードバッジ */}
{essence.keyword?.length > 0 && (
<div style={{display:"flex",flexWrap:"wrap",gap:8,marginBottom:22}}>
{essence.keyword.map((k,i)=>(
<span key={i} style={{fontSize:".82rem",padding:"5px 14px",borderRadius:20,background:"rgba(200,168,122,.1)",color:"#a8885a",border:"1px solid rgba(200,168,122,.35)",fontWeight:500}}>{k}</span>
))}
</div>
)}
{/* 分析カード */}
<div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:14}}>
{[
{key:"essence", label:"本質・魂の傾向", bar:"linear-gradient(90deg,#c89840,#e8c070)", dot:"#c89840", full:true},
{key:"personality", label:"日常的な性格", bar:"linear-gradient(90deg,#7090c0,#a0b8e0)", dot:"#7090c0", full:true},
{key:"strength", label:"才能と強み", bar:"linear-gradient(90deg,#40a890,#70d0b0)", dot:"#40a890"},
{key:"challenge", label:"人生の課題", bar:"linear-gradient(90deg,#d07890,#f0a8b8)", dot:"#d07890"},
{key:"love", label:"恋愛・対人関係",bar:"linear-gradient(90deg,#e09aaa,#c86478)", dot:"#c86478"},
{key:"career", label:"仕事・才能の活かし方",bar:"linear-gradient(90deg,#7abd8a,#4a8c5c)",dot:"#4a8c5c"},
].map(({key,label,bar,dot,full})=>(
<div key={key} style={{background:"#fff",border:"1px solid #ede9e2",borderRadius:14,padding:"20px 22px",boxShadow:"0 2px 20px rgba(26,24,20,.06)",position:"relative",overflow:"hidden",gridColumn:full?"1/-1":"auto"}}>
<div style={{position:"absolute",top:0,left:0,right:0,height:3,background:bar}} />
<div style={{fontSize:".7rem",letterSpacing:".15em",textTransform:"uppercase",color:"#9a9288",marginBottom:8,display:"flex",alignItems:"center",gap:7}}>
<span style={{width:5,height:5,borderRadius:"50%",background:dot,display:"inline-block"}} />{label}
</div>
<div style={{fontSize:".875rem",color:"#4a463e",lineHeight:1.88,fontWeight:300}}>{essence[key]}</div>
</div>
))}
</div>
</div>
)}
</div>
);
}
// ═══ 運勢パネル(日付指定) ═══
function FortunePanel({ profile }) {
const today = new Date().toISOString().split("T")[0];
const [targetDate, setTargetDate] = useState(today);
const [readings, setReadings] = useState(null);
const [loading, setLoading] = useState(false);
const [err, setErr] = useState(null);
const [queried, setQueried] = useState(null); // 最後に取得した日付
const load = useCallback(async (dateStr) => {
if(!profile) return;
setLoading(true); setReadings(null); setErr(null);
const sun=dts(profile.pos.Sun), moon=dts(profile.pos.Moon), asc=dts(profile.pos.Ascendant);
const d=new Date(dateStr);
const dow=["日","月","火","水","木","金","土"][d.getDay()];
const label=dateStr===today?"今日":dateStr<today?"過去の日付":"未来の日付";
try {
const result = await callClaude(
`あなたは西洋占星術の専門家です。以下の出生データと指定日をもとに、その日の運勢を日本語で鑑定してください。\n\n` +
`【出生データ】\n名前: ${profile.name}\n生年月日: ${profile.dateStr} ${profile.timeStr}\n出生地: ${profile.place}\n太陽: ${sun.sign} (${sun.within}°)\n月: ${moon.sign}\nアセンダント: ${asc.sign}\n\n` +
`【鑑定対象日】${dateStr} (${dow}曜日) ※${label}\n\n` +
`この日のトランジット(移動天体)の影響も考慮して、4分野の運勢をJSONのみで返してください(説明・コードブロック不要):\n` +
`{"date":"${dateStr}","summary":"一言総評(30字以内)","readings":[{"area":"総合運","title":"見出し(15字以内)","text":"鑑定文(150〜200字)","stars":4,"lucky":["ラッキーカラー:○○","ラッキーナンバー:○"]},{"area":"恋愛運","title":"見出し","text":"鑑定文(130字前後)","stars":3,"lucky":[]},{"area":"仕事・金運","title":"見出し","text":"鑑定文(130字前後)","stars":5,"lucky":[]},{"area":"健康運","title":"見出し","text":"鑑定文(100字前後)","stars":4,"lucky":[]}]}`
);
setReadings(result); setQueried(dateStr);
} catch(e) { setErr(e.message); }
finally { setLoading(false); }
}, [profile, today]);
// 初回・プロフィール変化時に今日の運勢を取得
useEffect(()=>{ if(profile) load(today); }, [profile]);
if(!profile) return (
<div style={{textAlign:"center",padding:"76px 20px"}}>
<span style={{fontSize:"2.2rem",opacity:.18,display:"block",marginBottom:14}}>◈</span>
<div style={{fontSize:".93rem",color:"#9a9288"}}>出生情報を入力して運勢を確認してください</div>
</div>
);
return (
<div>
{/* 日付選択エリア */}
<SectLabel>鑑定する日付を選択</SectLabel>
<div style={{background:"#fff",border:"1px solid #ede9e2",borderRadius:14,padding:"22px 26px",boxShadow:"0 2px 20px rgba(26,24,20,.06)",marginBottom:28,display:"flex",alignItems:"flex-end",gap:14,flexWrap:"wrap"}}>
<div style={{flex:1,minWidth:200}}>
<label style={{fontSize:".72rem",letterSpacing:".13em",textTransform:"uppercase",color:"#9a9288",fontWeight:500,display:"block",marginBottom:7}}>日付</label>
<input type="date" value={targetDate} onChange={e=>setTargetDate(e.target.value)}
style={{width:"100%",background:"#f8f7f5",border:"1.5px solid #e8e4de",borderRadius:9,padding:"11px 14px",color:"#1a1814",fontFamily:"inherit",fontSize:".93rem",outline:"none",transition:"border-color .2s"}}
onFocus={e=>{e.target.style.borderColor="#c8a87a";e.target.style.boxShadow="0 0 0 3px rgba(200,168,122,.14)";}}
onBlur={e=>{e.target.style.borderColor="#e8e4de";e.target.style.boxShadow="none";}}
/>
</div>
<div style={{display:"flex",gap:8,flexWrap:"wrap"}}>
{[["今日",today],["明日",new Date(Date.now()+86400000).toISOString().split("T")[0]],["明後日",new Date(Date.now()+172800000).toISOString().split("T")[0]]].map(([l,v])=>(
<button key={l} onClick={()=>setTargetDate(v)} style={{padding:"7px 14px",borderRadius:7,background:targetDate===v?"rgba(200,168,122,.15)":"var(--white,#fff)",border:`1.5px solid ${targetDate===v?"#c8a87a":"#e8e4de"}`,color:targetDate===v?"#a8885a":"#9a9288",cursor:"pointer",fontSize:".8rem",transition:"all .2s",fontFamily:"inherit"}}>{l}</button>
))}
</div>
<button onClick={()=>load(targetDate)} disabled={loading}
style={{padding:"11px 24px",background:"#1a1814",color:"#fff",border:"none",borderRadius:9,cursor:loading?"not-allowed":"pointer",fontSize:".83rem",fontWeight:500,letterSpacing:".08em",display:"flex",alignItems:"center",gap:8,transition:"background .2s",opacity:loading?.6:1,whiteSpace:"nowrap",fontFamily:"inherit"}}
onMouseEnter={e=>!loading&&(e.currentTarget.style.background="#4a463e")}
onMouseLeave={e=>{e.currentTarget.style.background="#1a1814";}}
>
<span>✦</span> 運勢を鑑定する
</button>
</div>
{/* ローディング */}
{loading && (
<div style={{textAlign:"center",padding:"56px 0"}}>
<div style={{width:38,height:38,margin:"0 auto 14px",border:"2px solid #e8e4de",borderTopColor:"#c8a87a",borderRadius:"50%",animation:"spin 1s linear infinite"}} />
<div style={{fontSize:".88rem",color:"#9a9288"}}>星々に問いかけています…</div>
</div>
)}
{/* エラー */}
{err && <div style={{padding:"14px 18px",background:"#fef8f8",border:"1px solid #f0d0d0",borderRadius:9,fontSize:".83rem",color:"#a04040"}}>エラー:{err}</div>}
{/* 鑑定結果 */}
{readings && !loading && (
<div>
{/* 日付ヘッダー */}
<div style={{display:"flex",alignItems:"center",justifyContent:"space-between",marginBottom:18,flexWrap:"wrap",gap:10}}>
<div>
<div style={{fontFamily:"Georgia,serif",fontSize:"1.25rem",color:"#1a1814"}}>
{queried} の運勢
</div>
{readings.summary && (
<div style={{fontSize:".88rem",color:"#a8885a",marginTop:4,fontStyle:"italic"}}>「{readings.summary}」</div>
)}
</div>
<div style={{fontSize:".76rem",color:"#9a9288",background:"#f8f7f5",border:"1px solid #e8e4de",borderRadius:7,padding:"5px 12px"}}>
{profile.name} · 太陽{dts(profile.pos.Sun).sym}{dts(profile.pos.Sun).sign}
</div>
</div>
{/* 運勢カード */}
<div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:14}}>
{(readings.readings||[]).map((r,i)=>(
<div key={i} style={{background:"#fff",border:"1px solid #ede9e2",borderRadius:14,padding:"20px 22px",boxShadow:"0 2px 20px rgba(26,24,20,.06)",position:"relative",overflow:"hidden",gridColumn:i===0?"1/-1":"auto",transition:"box-shadow .22s,transform .22s"}}
onMouseEnter={e=>{e.currentTarget.style.boxShadow="0 8px 40px rgba(26,24,20,.12)";e.currentTarget.style.transform="translateY(-2px)";}}
onMouseLeave={e=>{e.currentTarget.style.boxShadow="0 2px 20px rgba(26,24,20,.06)";e.currentTarget.style.transform="translateY(0)";}}
>
<div style={{position:"absolute",top:0,left:0,right:0,height:3,background:FORTUNE_COLORS[i].bar}} />
<div style={{fontSize:".69rem",letterSpacing:".15em",textTransform:"uppercase",color:"#9a9288",marginBottom:7,display:"flex",alignItems:"center",gap:7}}>
<span style={{width:5,height:5,borderRadius:"50%",background:FORTUNE_COLORS[i].dot,display:"inline-block"}} />{r.area}
</div>
<div style={{fontFamily:"Georgia,serif",fontSize:"1.05rem",color:"#1a1814",marginBottom:10,lineHeight:1.35}}>{r.title}</div>
<div style={{fontSize:".855rem",color:"#4a463e",lineHeight:1.88,fontWeight:300}}>{r.text}</div>
<div style={{marginTop:12,display:"flex",gap:2,alignItems:"center"}}>
{"★".repeat(r.stars||3).split("").map((_,j)=><span key={j} style={{color:"#c8a87a",fontSize:".88rem"}}>★</span>)}
{"☆".repeat(5-(r.stars||3)).split("").map((_,j)=><span key={j} style={{color:"#e8e4de",fontSize:".88rem"}}>☆</span>)}
<span style={{fontSize:".72rem",color:"#9a9288",marginLeft:7}}>{r.stars||3} / 5</span>
</div>
{r.lucky?.length>0 && (
<div style={{marginTop:11,display:"flex",gap:6,flexWrap:"wrap"}}>
{r.lucky.map((l,j)=><span key={j} style={{fontSize:".73rem",padding:"3px 9px",borderRadius:20,background:"rgba(200,168,122,.12)",color:"#a8885a",border:"1px solid rgba(200,168,122,.4)"}}>{l}</span>)}
</div>
)}
</div>
))}
</div>
</div>
)}
</div>
);
}
// ═══ 共通コンポーネント ═══
function SectLabel({children}) {
return (
<div style={{fontSize:".71rem",letterSpacing:".18em",textTransform:"uppercase",color:"#9a9288",margin:"36px 0 18px",display:"flex",alignItems:"center",gap:12}}>
{children}<span style={{flex:1,height:1,background:"#e8e4de",display:"block"}} />
</div>
);
}
function PlanetCard({title,children}) {
return (
<div style={{background:"#fff",border:"1px solid #ede9e2",borderRadius:12,padding:"14px 16px",boxShadow:"0 1px 8px rgba(26,24,20,.04)"}}>
<div style={{fontSize:".69rem",letterSpacing:".14em",textTransform:"uppercase",color:"#9a9288",marginBottom:9,fontWeight:500}}>{title}</div>
{children}
</div>
);
}
// ═══ メインアプリ ═══
export default function App() {
const [tab, setTab] = useState("birth");
const [name, setName] = useState("");
const [date, setDate] = useState("1990-06-15");
const [time, setTime] = useState("14:30");
const [place, setPlace] = useState("東京");
const [profile, setProfile] = useState(null);
const [popup, setPopup] = useState(null);
const [preview, setPreview] = useState(null);
useEffect(()=>{
if(!date) return;
const [y,m,d]=date.split("-").map(Number);
setPreview(dts(calcDeg(toJD(y,m,d,12,0),"Sun")));
},[date]);
function generate() {
if(!date){alert("生年月日を入力してください");return;}
const [y,m,d]=date.split("-").map(Number);
const [hh,mn]=(time||"12:00").split(":").map(Number);
const pos=buildPos(y,m,d,hh||12,mn||0);
setProfile({name:name||"あなた",dateStr:date,timeStr:time,place,pos,y,m,d,hh,mn});
setTab("chart");
}
const TABS=[["birth","出生情報"],["chart","チャート"],["fortune","運勢"]];
return (
<div style={{background:"#f8f7f5",minHeight:"100vh",fontFamily:"'DM Sans',sans-serif",fontWeight:300,WebkitFontSmoothing:"antialiased"}}>
<style>{`
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@300;400;500&display=swap');
@keyframes spin{to{transform:rotate(360deg);}}
@keyframes rise{from{opacity:0;transform:translateY(12px);}to{opacity:1;transform:translateY(0);}}
*{box-sizing:border-box;} button{font-family:inherit;}
@media(max-width:620px){
.chart-grid{grid-template-columns:1fr !important;}
.essence-grid{grid-template-columns:1fr !important;}
.fortune-grid{grid-template-columns:1fr !important;}
.birth-grid{grid-template-columns:1fr !important;}
}
`}</style>
<div style={{maxWidth:860,margin:"0 auto",padding:"0 20px 100px"}}>
{/* ヘッダー */}
<div style={{padding:"46px 0 22px",display:"flex",alignItems:"flex-end",justifyContent:"space-between",borderBottom:"1px solid #e8e4de"}}>
<div>
<div style={{fontFamily:"Georgia,serif",fontSize:"2.2rem",fontWeight:400,letterSpacing:".06em",color:"#1a1814"}}>
AS<em style={{color:"#a8885a",fontStyle:"italic"}}>tra</em>
</div>
<div style={{fontSize:".78rem",color:"#9a9288",letterSpacing:".14em",textTransform:"uppercase",marginTop:6}}>西洋占星術 — 星読みホロスコープ</div>
</div>
<div style={{opacity:.28,fontSize:"1.1rem",letterSpacing:6,paddingBottom:4}}>☉ ☽ ☿ ♀ ♂</div>
</div>
{/* ナビ */}
<div style={{display:"flex",borderBottom:"1px solid #e8e4de"}}>
{TABS.map(([k,l])=>(
<button key={k} onClick={()=>setTab(k)} style={{padding:"14px 22px",background:"none",border:"none",fontSize:".82rem",fontWeight:400,letterSpacing:".1em",textTransform:"uppercase",color:tab===k?"#1a1814":"#9a9288",cursor:"pointer",position:"relative",transition:"color .2s"}}>
{l}
{tab===k&&<span style={{position:"absolute",bottom:-1,left:0,right:0,height:2,background:"#a8885a",display:"block"}} />}
</button>
))}
</div>
{/* パネル:出生情報 */}
{tab==="birth" && (
<div style={{animation:"rise .4s cubic-bezier(.16,1,.3,1)"}}>
<SectLabel>あなたのデータを入力</SectLabel>
<div style={{background:"#fff",border:"1px solid #ede9e2",borderRadius:16,padding:"32px 32px 36px",boxShadow:"0 2px 24px rgba(26,24,20,.07)"}}>
<div style={{fontFamily:"Georgia,serif",fontSize:"1.35rem",marginBottom:7,lineHeight:1.4}}>
星はあなたが生まれた<em style={{color:"#a8885a",fontStyle:"italic"}}>瞬間</em>を覚えている。
</div>
<div style={{fontSize:".86rem",color:"#9a9288",marginBottom:28,lineHeight:1.75}}>出生日時と場所を入力すると、あなただけのホロスコープを生成します。</div>
<div className="birth-grid" style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:"17px 22px",marginBottom:20}}>
{[
["お名前","text",name,setName,"山田 太郎",null],
["生年月日","date",date,setDate,null,null],
["出生時刻","time",time,setTime,null,"不明の場合は 12:00 を入力"],
["出生地","text",place,setPlace,"東京",null],
].map(([lbl,type,val,setter,ph,hint])=>(
<div key={lbl} style={{display:"flex",flexDirection:"column",gap:6}}>
<label style={{fontSize:".72rem",letterSpacing:".13em",textTransform:"uppercase",color:"#9a9288",fontWeight:500}}>{lbl}</label>
<input type={type} value={val} onChange={e=>setter(e.target.value)} placeholder={ph||""}
style={{background:"#f8f7f5",border:"1.5px solid #e8e4de",borderRadius:9,padding:"11px 14px",color:"#1a1814",fontFamily:"inherit",fontSize:".93rem",fontWeight:300,outline:"none",transition:"border-color .2s,box-shadow .2s"}}
onFocus={e=>{e.target.style.borderColor="#c8a87a";e.target.style.boxShadow="0 0 0 3px rgba(200,168,122,.14)";}}
onBlur={e=>{e.target.style.borderColor="#e8e4de";e.target.style.boxShadow="none";}}
/>
{hint&&<span style={{fontSize:".73rem",color:"#9a9288"}}>{hint}</span>}
</div>
))}
</div>
{preview&&(
<div style={{marginTop:4,marginBottom:20,padding:"13px 17px",background:"rgba(200,168,122,.1)",border:"1px solid rgba(200,168,122,.38)",borderRadius:9,display:"flex",alignItems:"center",gap:12}}>
<span style={{fontSize:"1.7rem"}}>{preview.sym}</span>
<div>
<div style={{fontFamily:"Georgia,serif",fontSize:"1rem"}}>{preview.sign}</div>
<div style={{fontSize:".77rem",color:"#9a9288",marginTop:2}}>太陽星座: {preview.sign} ({preview.within}°)</div>
</div>
</div>
)}
<button onClick={generate}
style={{width:"100%",padding:"14px",background:"#1a1814",color:"#fff",border:"none",borderRadius:9,cursor:"pointer",fontSize:".85rem",fontWeight:500,letterSpacing:".12em",textTransform:"uppercase",display:"flex",alignItems:"center",justifyContent:"center",gap:9,transition:"background .2s,transform .15s"}}
onMouseEnter={e=>e.currentTarget.style.background="#4a463e"}
onMouseLeave={e=>e.currentTarget.style.background="#1a1814"}
><span>✦</span> ホロスコープを生成する</button>
</div>
{profile&&(
<div style={{display:"flex",alignItems:"center",gap:10,padding:"10px 15px",background:"#fff",border:"1px solid #ede9e2",borderRadius:9,marginTop:18,boxShadow:"0 1px 6px rgba(26,24,20,.05)"}}>
<div style={{width:30,height:30,borderRadius:"50%",background:"rgba(200,168,122,.12)",border:"1px solid rgba(200,168,122,.4)",display:"flex",alignItems:"center",justifyContent:"center",fontSize:".85rem",flexShrink:0}}>☉</div>
<div>
<div style={{fontSize:".86rem",fontWeight:500}}>{profile.name}</div>
<div style={{fontSize:".76rem",color:"#9a9288",marginTop:1}}>{profile.dateStr} · {profile.place} · 太陽: {dts(profile.pos.Sun).sym}{dts(profile.pos.Sun).sign}</div>
</div>
</div>
)}
</div>
)}
{/* パネル:チャート */}
{tab==="chart" && (
<div style={{animation:"rise .4s cubic-bezier(.16,1,.3,1)"}}>
<ChartPanel profile={profile} onOpenPopup={(planet,signName,signSym)=>setPopup({planet,signName,signSym})} />
</div>
)}
{/* パネル:運勢 */}
{tab==="fortune" && (
<div style={{animation:"rise .4s cubic-bezier(.16,1,.3,1)"}}>
<SectLabel>運勢鑑定</SectLabel>
<FortunePanel profile={profile} />
</div>
)}
</div>
{/* ポップアップ */}
{popup&&<SignPopup planet={popup.planet} signName={popup.signName} signSym={popup.signSym} onClose={()=>setPopup(null)} />}
</div>
);
}