lifekline / components /layout /RightSidebar.tsx
xiaobo ren
Remove navigation items: 鐭ヨ瘑涓績, 妗堜緥搴? Telegram绀剧兢, login/register from left sidebar; Remove 鐑棬鎺ㄨ崘 and 鍔犲叆绀剧兢 from right sidebar
aa55dc1
import React, { useState, useEffect, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { Calendar, TrendingUp, Star, ChevronRight, Save, User, AlertCircle, Check, X } from 'lucide-react';
import { CTACard } from '../widgets/CTACard';
import { DailyFortuneCard } from '../fortune/DailyFortuneCard';
import { ProfileInfo } from '../fortune/ProfileQuickSwitch';
const PROFILES_STORAGE_KEY = 'lifekline_profiles';
const HISTORY_STORAGE_KEY = 'lifekline_history';
interface UserProfile {
id: string;
name: string;
gender: string;
birthYear: number;
yearPillar?: string;
monthPillar?: string;
dayPillar?: string;
hourPillar?: string;
}
interface CurrentCalculation {
name?: string;
birthYear?: string;
gender?: string;
yearPillar: string;
monthPillar: string;
dayPillar: string;
hourPillar: string;
}
// 当前档案显示卡片
const CurrentProfileCard: React.FC<{
profile: UserProfile | null;
profiles: ProfileInfo[];
onProfileChange: (profileId: string) => void;
onManageProfiles: () => void;
}> = ({ profile, profiles, onProfileChange, onManageProfiles }) => {
const [showDropdown, setShowDropdown] = useState(false);
if (!profile && profiles.length === 0) {
return (
<div className="bg-gradient-to-br from-indigo-50 to-purple-50 border border-indigo-200 rounded-xl p-4">
<div className="flex items-center gap-3 mb-3">
<div className="w-10 h-10 bg-indigo-100 rounded-full flex items-center justify-center flex-shrink-0">
<User className="w-5 h-5 text-indigo-400" />
</div>
<div className="flex-1 min-w-0">
<h3 className="font-bold text-gray-800 text-sm sm:text-base truncate">创建八字档案</h3>
<p className="text-xs text-gray-500 truncate">保存八字以查看运势分析</p>
</div>
</div>
<button
onClick={onManageProfiles}
className="w-full py-2 bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-medium rounded-lg transition-colors"
>
创建档案
</button>
</div>
);
}
if (!profile) {
return (
<div className="bg-gradient-to-br from-gray-50 to-slate-50 border border-gray-200 rounded-xl p-4">
<div className="flex items-center gap-3 mb-3">
<div className="w-10 h-10 bg-gray-100 rounded-full flex items-center justify-center flex-shrink-0">
<User className="w-5 h-5 text-gray-400" />
</div>
<div className="flex-1 min-w-0">
<h3 className="font-bold text-gray-800 text-sm sm:text-base truncate">选择档案</h3>
<p className="text-xs text-gray-500 truncate">请选择要查看运势的档案</p>
</div>
</div>
<div className="relative">
<button
onClick={() => setShowDropdown(!showDropdown)}
className="w-full py-2 px-3 bg-white border border-gray-200 rounded-lg text-sm text-left flex items-center justify-between hover:border-indigo-300 transition-colors"
>
<span className="text-gray-500 truncate">选择档案...</span>
<ChevronRight className={`w-4 h-4 text-gray-400 transition-transform flex-shrink-0 ${showDropdown ? 'rotate-90' : ''}`} />
</button>
{showDropdown && (
<div className="absolute z-20 w-full mt-1 bg-white border border-gray-200 rounded-lg shadow-lg py-1 max-h-60 overflow-y-auto">
{profiles.map((p) => (
<button
key={p.id}
onClick={() => {
onProfileChange(p.id);
setShowDropdown(false);
}}
className="w-full px-3 py-2 text-left text-sm hover:bg-indigo-50 transition-colors"
>
<div className="font-medium text-gray-800 truncate">{p.name}</div>
<div className="text-xs text-gray-500 truncate">
{p.yearPillar} {p.monthPillar} {p.dayPillar} {p.hourPillar}
</div>
</button>
))}
</div>
)}
</div>
</div>
);
}
return (
<div className="bg-gradient-to-br from-indigo-50 to-purple-50 border border-indigo-200 rounded-xl p-4">
<div className="flex items-center justify-between gap-2 mb-3">
<div className="flex items-center gap-3 flex-1 min-w-0">
<div className="w-10 h-10 bg-indigo-600 rounded-full flex items-center justify-center flex-shrink-0">
<User className="w-5 h-5 text-white" />
</div>
<div className="flex-1 min-w-0">
<h3 className="font-bold text-gray-800 text-sm sm:text-base truncate">{profile.name}</h3>
<p className="text-xs text-gray-500 truncate">当前查看档案</p>
</div>
</div>
<div className="relative flex-shrink-0">
<button
onClick={() => setShowDropdown(!showDropdown)}
className="p-2 hover:bg-white/50 rounded-lg transition-colors"
title="切换档案"
>
<ChevronRight className={`w-4 h-4 text-gray-500 transition-transform ${showDropdown ? 'rotate-90' : ''}`} />
</button>
{showDropdown && profiles.length > 1 && (
<div className="absolute z-20 right-0 w-48 mt-1 bg-white border border-gray-200 rounded-lg shadow-lg py-1 max-h-60 overflow-y-auto">
{profiles.filter(p => p.id !== profile.id).map((p) => (
<button
key={p.id}
onClick={() => {
onProfileChange(p.id);
setShowDropdown(false);
}}
className="w-full px-3 py-2 text-left text-sm hover:bg-indigo-50 transition-colors"
>
<div className="font-medium text-gray-800 truncate">{p.name}</div>
<div className="text-xs text-gray-500 truncate">
{p.yearPillar} {p.monthPillar} {p.dayPillar} {p.hourPillar}
</div>
</button>
))}
</div>
)}
</div>
</div>
{/* 四柱显示 */}
<div className="grid grid-cols-4 gap-1.5 sm:gap-2 mb-3">
<div className="text-center bg-white/60 rounded-lg py-1.5 sm:py-2 px-1">
<div className="text-xs text-gray-500 mb-0.5 truncate">年柱</div>
<div className="font-bold text-xs sm:text-sm text-indigo-700 font-serif-sc truncate">{profile.yearPillar}</div>
</div>
<div className="text-center bg-white/60 rounded-lg py-1.5 sm:py-2 px-1">
<div className="text-xs text-gray-500 mb-0.5 truncate">月柱</div>
<div className="font-bold text-xs sm:text-sm text-indigo-700 font-serif-sc truncate">{profile.monthPillar}</div>
</div>
<div className="text-center bg-white/60 rounded-lg py-1.5 sm:py-2 px-1">
<div className="text-xs text-gray-500 mb-0.5 truncate">日柱</div>
<div className="font-bold text-xs sm:text-sm text-indigo-700 font-serif-sc truncate">{profile.dayPillar}</div>
</div>
<div className="text-center bg-white/60 rounded-lg py-1.5 sm:py-2 px-1">
<div className="text-xs text-gray-500 mb-0.5 truncate">时柱</div>
<div className="font-bold text-xs sm:text-sm text-indigo-700 font-serif-sc truncate">{profile.hourPillar}</div>
</div>
</div>
<button
onClick={onManageProfiles}
className="w-full py-1.5 text-xs text-indigo-600 hover:bg-white/50 rounded-lg transition-colors"
>
管理我的档案
</button>
</div>
);
};
// 保存当前命盘提示组件
const SaveCurrentBaziPrompt: React.FC<{
currentBazi: CurrentCalculation;
onSave: (name: string) => void;
onDismiss: () => void;
}> = ({ currentBazi, onSave, onDismiss }) => {
const [showInput, setShowInput] = useState(false);
const [profileName, setProfileName] = useState(currentBazi.name || '');
const [saving, setSaving] = useState(false);
const handleSave = () => {
if (!profileName.trim()) return;
setSaving(true);
onSave(profileName.trim());
};
if (!showInput) {
return (
<div className="bg-gradient-to-br from-amber-50 to-orange-50 border border-amber-200 rounded-xl p-4">
<div className="flex items-start gap-3">
<div className="p-2 bg-amber-100 rounded-lg flex-shrink-0">
<AlertCircle className="w-5 h-5 text-amber-600" />
</div>
<div className="flex-1 min-w-0">
<h4 className="font-semibold text-gray-800 text-sm mb-1">保存当前命盘</h4>
<p className="text-xs text-gray-600 mb-3">
检测到您刚完成一次测算,是否将此八字保存为档案以便查看运势?
</p>
<div className="text-xs text-gray-500 mb-3 bg-white/50 rounded-lg p-2 overflow-x-auto">
<span className="font-medium">八字:</span>
<span className="whitespace-nowrap">
{currentBazi.yearPillar} {currentBazi.monthPillar} {currentBazi.dayPillar} {currentBazi.hourPillar}
</span>
</div>
<div className="flex gap-2">
<button
onClick={() => setShowInput(true)}
className="flex-1 py-2 bg-amber-500 hover:bg-amber-600 text-white text-sm font-medium rounded-lg flex items-center justify-center gap-1.5"
>
<Save className="w-4 h-4 flex-shrink-0" />
<span className="truncate">保存档案</span>
</button>
<button
onClick={onDismiss}
className="px-3 py-2 text-gray-500 hover:text-gray-700 text-sm flex-shrink-0"
>
暂不
</button>
</div>
</div>
</div>
</div>
);
}
return (
<div className="bg-gradient-to-br from-amber-50 to-orange-50 border border-amber-200 rounded-xl p-4">
<div className="flex items-center justify-between mb-3">
<h4 className="font-semibold text-gray-800 text-sm">为此命盘取个名字</h4>
<button onClick={() => setShowInput(false)} className="p-1 hover:bg-amber-100 rounded flex-shrink-0">
<X className="w-4 h-4 text-gray-500" />
</button>
</div>
<input
type="text"
value={profileName}
onChange={(e) => setProfileName(e.target.value)}
placeholder="如:我自己、小明、张三..."
className="w-full px-3 py-2 border border-amber-200 rounded-lg text-sm mb-3 focus:outline-none focus:ring-2 focus:ring-amber-300"
autoFocus
/>
<button
onClick={handleSave}
disabled={!profileName.trim() || saving}
className="w-full py-2 bg-amber-500 hover:bg-amber-600 disabled:bg-gray-300 text-white text-sm font-medium rounded-lg flex items-center justify-center gap-1.5"
>
{saving ? '保存中...' : <><Check className="w-4 h-4 flex-shrink-0" />确认保存</>}
</button>
</div>
);
};
interface DailyFortuneData {
date: string;
dayGanZhi: string;
lunarDate: string;
overallScore: number;
overallTrend: 'up' | 'down' | 'stable';
overallSummary: string;
career: { score: number; trend: 'up' | 'down' | 'stable'; description: string; advice: string; keyPoint: string };
wealth: { score: number; trend: 'up' | 'down' | 'stable'; description: string; advice: string; keyPoint: string };
relationship: { score: number; trend: 'up' | 'down' | 'stable'; description: string; advice: string; keyPoint: string };
health: { score: number; trend: 'up' | 'down' | 'stable'; description: string; advice: string; keyPoint: string };
luckyElements?: { colors: string[]; directions: string[]; numbers: number[]; zodiac: string[] };
}
interface RightSidebarProps {
isLoggedIn: boolean;
userInfo: { email: string; points: number } | null;
onLogin: () => void;
onLogout: () => void;
onGenerate: () => void;
isAnalysisPanelOpen?: boolean; // 当命盘分析报告展开时隐藏
}
// 月度运势卡片组件
const MonthlyFortuneCard: React.FC<{ profileId: string | null; onViewDetail: () => void }> = ({
profileId,
onViewDetail,
}) => {
const [fortune, setFortune] = useState<any>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const currentYear = new Date().getFullYear();
const currentMonth = new Date().getMonth() + 1;
const monthStr = new Date().toLocaleDateString('zh-CN', { month: 'long' });
// Reset state when profileId changes (no auto-fetch)
useEffect(() => {
setFortune(null);
setError(null);
}, [profileId]);
const fetchMonthlyFortune = async () => {
if (!profileId || loading) return;
setLoading(true);
setError(null);
try {
const response = await fetch(
`/api/fortune/monthly/${currentYear}/${currentMonth}?profileId=${profileId}`
);
if (response.ok) {
const data = await response.json();
setFortune(data.fortune);
} else {
setError('获取月度运势失败');
}
} catch (err) {
console.error('获取月度运势失败:', err);
setError('获取月度运势失败');
} finally {
setLoading(false);
}
};
if (!profileId) {
return (
<div className="bg-gradient-to-br from-blue-50 to-cyan-50 border border-blue-100 rounded-xl p-4">
<div className="flex items-center gap-2 mb-2">
<Calendar className="w-5 h-5 text-blue-500" />
<h3 className="font-semibold text-gray-800">{monthStr}运势</h3>
</div>
<p className="text-sm text-gray-500">选择档案查看本月运势</p>
</div>
);
}
if (loading) {
return (
<div className="bg-white border border-gray-200 rounded-xl p-4 animate-pulse">
<div className="flex items-center gap-2 mb-2">
<div className="w-5 h-5 bg-gray-200 rounded" />
<div className="h-5 bg-gray-200 rounded w-20" />
</div>
<div className="h-10 bg-gray-200 rounded" />
</div>
);
}
return (
<div className="bg-white border border-gray-200 rounded-xl p-4 hover:shadow-md transition-shadow">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<Calendar className="w-5 h-5 text-blue-500" />
<h3 className="font-semibold text-gray-800">{monthStr}运势</h3>
<span className="px-1.5 py-0.5 bg-green-100 text-green-600 text-xs rounded-full">免费</span>
</div>
</div>
{fortune ? (
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600">综合评分</span>
<span className="text-lg font-bold text-blue-600">{fortune.overallScore}</span>
</div>
<p className="text-xs text-gray-500 line-clamp-2">{fortune.theme}</p>
<button
onClick={onViewDetail}
className="w-full py-1.5 text-sm text-blue-600 hover:bg-blue-50 rounded-lg flex items-center justify-center gap-1"
>
查看详情 <ChevronRight className="w-4 h-4" />
</button>
</div>
) : error ? (
<div className="space-y-2">
<p className="text-sm text-gray-500">{error}</p>
<button
onClick={() => { fetchMonthlyFortune(); }}
className="w-full py-1.5 text-sm text-blue-600 hover:bg-blue-50 rounded-lg"
>
重试
</button>
</div>
) : (
<div className="space-y-2">
<p className="text-sm text-gray-500">查看本月运势走向和关键时间点</p>
<button
onClick={fetchMonthlyFortune}
className="w-full py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors"
>
获取本月运势
</button>
</div>
)}
</div>
);
};
// 年度运势卡片组件
const YearlyFortuneCard: React.FC<{ profileId: string | null; onViewDetail: () => void }> = ({
profileId,
onViewDetail,
}) => {
const [fortune, setFortune] = useState<any>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const currentYear = new Date().getFullYear();
// Reset state when profileId changes (no auto-fetch)
useEffect(() => {
setFortune(null);
setError(null);
}, [profileId]);
const fetchYearlyFortune = async () => {
if (!profileId || loading) return;
setLoading(true);
setError(null);
try {
const response = await fetch(`/api/fortune/yearly/${currentYear}?profileId=${profileId}`);
if (response.ok) {
const data = await response.json();
setFortune(data.fortune);
} else {
setError('获取年度运势失败');
}
} catch (err) {
console.error('获取年度运势失败:', err);
setError('获取年度运势失败');
} finally {
setLoading(false);
}
};
if (!profileId) {
return (
<div className="bg-gradient-to-br from-amber-50 to-orange-50 border border-amber-100 rounded-xl p-4">
<div className="flex items-center gap-2 mb-2">
<TrendingUp className="w-5 h-5 text-amber-500" />
<h3 className="font-semibold text-gray-800">{currentYear}年运势</h3>
</div>
<p className="text-sm text-gray-500">选择档案查看今年运势</p>
</div>
);
}
if (loading) {
return (
<div className="bg-white border border-gray-200 rounded-xl p-4 animate-pulse">
<div className="flex items-center gap-2 mb-2">
<div className="w-5 h-5 bg-gray-200 rounded" />
<div className="h-5 bg-gray-200 rounded w-24" />
</div>
<div className="h-10 bg-gray-200 rounded" />
</div>
);
}
const getTrendText = (trend: string) => {
switch (trend) {
case 'rising': return '上升期';
case 'stable': return '平稳期';
case 'volatile': return '波动期';
case 'declining': return '调整期';
default: return '平稳期';
}
};
return (
<div className="bg-white border border-gray-200 rounded-xl p-4 hover:shadow-md transition-shadow">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<TrendingUp className="w-5 h-5 text-amber-500" />
<h3 className="font-semibold text-gray-800">{currentYear}年运势</h3>
<span className="px-1.5 py-0.5 bg-green-100 text-green-600 text-xs rounded-full">免费</span>
</div>
</div>
{fortune ? (
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600">综合评分</span>
<div className="flex items-center gap-2">
<span className="text-lg font-bold text-amber-600">{fortune.overallScore}</span>
<span className="text-xs px-1.5 py-0.5 bg-amber-100 text-amber-700 rounded">
{getTrendText(fortune.overallTrend)}
</span>
</div>
</div>
<div className="text-xs text-gray-500">
<span className="text-green-600">吉月:</span> {fortune.favorableMonths?.slice(0, 3).join('、')}月
</div>
<button
onClick={onViewDetail}
className="w-full py-1.5 text-sm text-amber-600 hover:bg-amber-50 rounded-lg flex items-center justify-center gap-1"
>
查看详情 <ChevronRight className="w-4 h-4" />
</button>
</div>
) : error ? (
<div className="space-y-2">
<p className="text-sm text-gray-500">{error}</p>
<button
onClick={() => { fetchYearlyFortune(); }}
className="w-full py-1.5 text-sm text-amber-600 hover:bg-amber-50 rounded-lg"
>
重试
</button>
</div>
) : (
<div className="space-y-2">
<p className="text-sm text-gray-500">查看全年运势走向和吉凶月份</p>
<button
onClick={fetchYearlyFortune}
className="w-full py-2 bg-amber-600 hover:bg-amber-700 text-white text-sm font-medium rounded-lg transition-colors"
>
获取年度运势
</button>
</div>
)}
</div>
);
};
const RightSidebar: React.FC<RightSidebarProps> = ({
isLoggedIn,
userInfo,
onLogin,
onLogout: _onLogout,
onGenerate,
isAnalysisPanelOpen = false,
}) => {
const navigate = useNavigate();
const [currentProfile, setCurrentProfile] = useState<UserProfile | null>(null);
const [allProfiles, setAllProfiles] = useState<ProfileInfo[]>([]);
const [currentCalculation, setCurrentCalculation] = useState<CurrentCalculation | null>(null);
const [showSavePrompt, setShowSavePrompt] = useState(false);
const [savePromptDismissed, setSavePromptDismissed] = useState(false);
// Load current calculation from history
const loadCurrentCalculation = useCallback(() => {
try {
const historyData = localStorage.getItem(HISTORY_STORAGE_KEY);
if (historyData) {
const history = JSON.parse(historyData);
if (history.length > 0) {
const latest = history[0];
if (latest.input?.yearPillar && latest.input?.monthPillar && latest.input?.dayPillar && latest.input?.hourPillar) {
setCurrentCalculation({
name: latest.input.name,
birthYear: latest.input.birthYear,
gender: latest.input.gender,
yearPillar: latest.input.yearPillar,
monthPillar: latest.input.monthPillar,
dayPillar: latest.input.dayPillar,
hourPillar: latest.input.hourPillar,
});
}
}
}
} catch {
setCurrentCalculation(null);
}
}, []);
// Load all profiles from API (logged in) or localStorage (not logged in)
const loadProfiles = useCallback(async () => {
if (typeof window === 'undefined') return;
try {
if (isLoggedIn) {
// Fetch from API when logged in
const response = await fetch('/api/profiles', {
credentials: 'include',
});
if (response.ok) {
const data = await response.json();
const profileInfos: ProfileInfo[] = data.profiles.map((p: any) => ({
id: p.id,
name: p.name,
yearPillar: p.yearPillar || '',
monthPillar: p.monthPillar || '',
dayPillar: p.dayPillar || '',
hourPillar: p.hourPillar || '',
isDefault: p.isDefault,
}));
setAllProfiles(profileInfos);
// Set default profile as current if no current profile selected
const saved = localStorage.getItem('lifekline_current_profile');
if (!saved) {
const defaultProfile = profileInfos.find(p => p.isDefault);
if (defaultProfile) {
const fullProfile: UserProfile = {
id: defaultProfile.id,
name: defaultProfile.name,
gender: '',
birthYear: 0,
yearPillar: defaultProfile.yearPillar,
monthPillar: defaultProfile.monthPillar,
dayPillar: defaultProfile.dayPillar,
hourPillar: defaultProfile.hourPillar,
};
setCurrentProfile(fullProfile);
localStorage.setItem('lifekline_current_profile', JSON.stringify(fullProfile));
}
}
return;
}
}
// Fallback to localStorage when not logged in or API fails
const saved = localStorage.getItem(PROFILES_STORAGE_KEY);
if (saved) {
const profiles = JSON.parse(saved);
const profileInfos: ProfileInfo[] = profiles.map((p: UserProfile) => ({
id: p.id,
name: p.name,
yearPillar: p.yearPillar || '',
monthPillar: p.monthPillar || '',
dayPillar: p.dayPillar || '',
hourPillar: p.hourPillar || '',
}));
setAllProfiles(profileInfos);
}
} catch {
// Fallback to localStorage on error
try {
const saved = localStorage.getItem(PROFILES_STORAGE_KEY);
if (saved) {
const profiles = JSON.parse(saved);
const profileInfos: ProfileInfo[] = profiles.map((p: UserProfile) => ({
id: p.id,
name: p.name,
yearPillar: p.yearPillar || '',
monthPillar: p.monthPillar || '',
dayPillar: p.dayPillar || '',
hourPillar: p.hourPillar || '',
}));
setAllProfiles(profileInfos);
}
} catch {
setAllProfiles([]);
}
}
}, [isLoggedIn]);
// Load current profile from localStorage
const loadCurrentProfile = useCallback(() => {
if (typeof window !== 'undefined') {
const saved = localStorage.getItem('lifekline_current_profile');
if (saved) {
try {
setCurrentProfile(JSON.parse(saved));
} catch {
setCurrentProfile(null);
}
}
}
}, []);
useEffect(() => {
loadProfiles();
loadCurrentProfile();
loadCurrentCalculation();
}, [isLoggedIn]); // Only run on mount and when login status changes
// Separate effect for event listeners
useEffect(() => {
// Listen for storage changes (profile selection in other components)
const handleStorageChange = (e: StorageEvent) => {
if (e.key === 'lifekline_current_profile') {
loadCurrentProfile();
} else if (e.key === PROFILES_STORAGE_KEY) {
loadProfiles();
} else if (e.key === HISTORY_STORAGE_KEY) {
loadCurrentCalculation();
}
};
// Listen for custom event within same tab
const handleProfileChange = () => {
loadCurrentProfile();
loadProfiles();
};
window.addEventListener('storage', handleStorageChange);
window.addEventListener('profileChanged', handleProfileChange);
return () => {
window.removeEventListener('storage', handleStorageChange);
window.removeEventListener('profileChanged', handleProfileChange);
};
}, [loadProfiles, loadCurrentProfile, loadCurrentCalculation]);
// Show save prompt when we have a calculation but no profiles
useEffect(() => {
if (currentCalculation && allProfiles.length === 0 && !savePromptDismissed && !currentProfile) {
setShowSavePrompt(true);
} else {
setShowSavePrompt(false);
}
}, [currentCalculation, allProfiles.length, savePromptDismissed, currentProfile]);
// Handle profile change
const handleProfileChange = useCallback((profileId: string) => {
const profile = allProfiles.find(p => p.id === profileId);
if (profile) {
const fullProfile: UserProfile = {
id: profile.id,
name: profile.name,
gender: '',
birthYear: 0,
yearPillar: profile.yearPillar,
monthPillar: profile.monthPillar,
dayPillar: profile.dayPillar,
hourPillar: profile.hourPillar,
};
setCurrentProfile(fullProfile);
localStorage.setItem('lifekline_current_profile', JSON.stringify(fullProfile));
// Dispatch custom event for other components in same tab
window.dispatchEvent(new Event('profileChanged'));
}
}, [allProfiles]);
// Handle manage profiles click
const handleManageProfiles = useCallback(() => {
navigate('/dashboard?tab=profiles');
}, [navigate]);
// Handle save current bazi to profile
const handleSaveCurrentBazi = useCallback(async (name: string) => {
if (!currentCalculation) return;
try {
if (isLoggedIn) {
// Save to API when logged in
const response = await fetch('/api/profiles', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
name,
gender: currentCalculation.gender || 'male',
birthYear: parseInt(currentCalculation.birthYear || '1990', 10),
yearPillar: currentCalculation.yearPillar,
monthPillar: currentCalculation.monthPillar,
dayPillar: currentCalculation.dayPillar,
hourPillar: currentCalculation.hourPillar,
isDefault: true, // Set as default
}),
});
if (response.ok) {
const data = await response.json();
const newProfile: UserProfile = {
id: data.profile.id,
name: data.profile.name,
gender: data.profile.gender || '',
birthYear: data.profile.birthYear || 0,
yearPillar: data.profile.yearPillar,
monthPillar: data.profile.monthPillar,
dayPillar: data.profile.dayPillar,
hourPillar: data.profile.hourPillar,
};
// Set as current profile
setCurrentProfile(newProfile);
localStorage.setItem('lifekline_current_profile', JSON.stringify(newProfile));
// Refresh profiles list
loadProfiles();
setShowSavePrompt(false);
// Dispatch event
window.dispatchEvent(new Event('profileChanged'));
return;
}
}
// Fallback to localStorage when not logged in or API fails
const newProfile: UserProfile = {
id: `profile_${Date.now()}`,
name,
gender: currentCalculation.gender || '',
birthYear: parseInt(currentCalculation.birthYear || '0', 10),
yearPillar: currentCalculation.yearPillar,
monthPillar: currentCalculation.monthPillar,
dayPillar: currentCalculation.dayPillar,
hourPillar: currentCalculation.hourPillar,
};
const existingProfiles = JSON.parse(localStorage.getItem(PROFILES_STORAGE_KEY) || '[]');
localStorage.setItem(PROFILES_STORAGE_KEY, JSON.stringify([...existingProfiles, newProfile]));
// Set as current profile
localStorage.setItem('lifekline_current_profile', JSON.stringify(newProfile));
// Refresh state
loadProfiles();
setCurrentProfile(newProfile);
setShowSavePrompt(false);
// Dispatch event
window.dispatchEvent(new Event('profileChanged'));
} catch (err) {
console.error('Failed to save profile:', err);
}
}, [currentCalculation, isLoggedIn, loadProfiles]);
const handleViewDailyFortuneDetail = (fortune: DailyFortuneData) => {
// Navigate to daily fortune detail page
navigate(`/fortune/daily/${fortune.date}`);
};
const handleRequestEnhanced = () => {
// This will be handled by the DailyFortuneCard component
// It will show confirmation dialog and request AI enhancement
};
return (
<div className={`space-y-4 transition-opacity duration-200 ${isAnalysisPanelOpen ? 'opacity-0 pointer-events-none' : 'opacity-100'}`}>
{/* Current Profile Card - Priority 0 */}
<CurrentProfileCard
profile={currentProfile}
profiles={allProfiles}
onProfileChange={handleProfileChange}
onManageProfiles={handleManageProfiles}
/>
{/* Save Current Bazi Prompt - Show when no profiles but has calculation */}
{showSavePrompt && currentCalculation && (
<SaveCurrentBaziPrompt
currentBazi={currentCalculation}
onSave={handleSaveCurrentBazi}
onDismiss={() => setSavePromptDismissed(true)}
/>
)}
{/* Daily Fortune Card - Priority 1 */}
<DailyFortuneCard
profileId={currentProfile?.id || null}
profiles={allProfiles}
onProfileChange={handleProfileChange}
onManageProfiles={handleManageProfiles}
isLoggedIn={isLoggedIn}
userPoints={userInfo?.points || 0}
onViewDetail={handleViewDailyFortuneDetail}
onRequestEnhanced={handleRequestEnhanced}
onLogin={onLogin}
/>
{/* Monthly Fortune Card - Priority 2 */}
<MonthlyFortuneCard
profileId={currentProfile?.id || null}
onViewDetail={() => navigate('/fortune/monthly')}
/>
{/* Yearly Fortune Card - Priority 3 */}
<YearlyFortuneCard
profileId={currentProfile?.id || null}
onViewDetail={() => navigate('/fortune/yearly')}
/>
{/* CTA Card */}
<CTACard onGenerate={onGenerate} />
{/* Disclaimer */}
<div className="text-xs text-gray-400 text-center px-4 py-2">
仅供娱乐与文化研究,请勿迷信
</div>
</div>
);
};
export default RightSidebar;