HoroScope / src /App.js
ntphuc149's picture
Upload 2 files
dcc8c62 verified
raw
history blame
27.2 kB
import React, { useState, useEffect, useRef } from "react";
import {
fetchHoroscopeData,
fallbackHoroscopeContent,
} from "./services/horoscopeService";
// Zodiac Icon Components using Tabler Icons SVGs
const ZodiacIcon = ({ sign, size = 24, className = "" }) => {
const iconPaths = {
taurus: [
"M6 3a6 6 0 0 0 12 0",
"M12 15m-6 0a6 6 0 1 0 12 0a6 6 0 1 0 -12 0",
],
sagittarius: ["M4 20l16 -16", "M13 4h7v7", "M6.5 12.5l5 5"],
pisces: ["M5 3a21 21 0 0 1 0 18", "M19 3a21 21 0 0 0 0 18", "M5 12l14 0"],
libra: ["M5 20l14 0", "M5 17h5v-.3a7 7 0 1 1 4 0v.3h5"],
gemini: [
"M3 3a21 21 0 0 0 18 0",
"M3 21a21 21 0 0 1 18 0",
"M7 4.5l0 15",
"M17 4.5l0 15",
],
leo: [
"M13 17a4 4 0 1 0 8 0",
"M6 16m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0",
"M11 7m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0",
"M7 7c0 3 2 5 2 9",
"M15 7c0 4 -2 6 -2 10",
],
virgo: [
"M3 4a2 2 0 0 1 2 2v9",
"M5 6a2 2 0 0 1 4 0v9",
"M9 6a2 2 0 0 1 4 0v10a7 5 0 0 0 7 5",
"M12 21a7 5 0 0 0 7 -5v-2a3 3 0 0 0 -6 0",
],
aquarius: [
"M3 10l3 -3l3 3l3 -3l3 3l3 -3l3 3",
"M3 17l3 -3l3 3l3 -3l3 3l3 -3l3 3",
],
aries: ["M12 5a5 5 0 1 0 -4 8", "M16 13a5 5 0 1 0 -4 -8", "M12 21l0 -16"],
cancer: [
"M6 12m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0",
"M18 12m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0",
"M3 12a10 6.5 0 0 1 14 -6.5",
"M21 12a10 6.5 0 0 1 -14 6.5",
],
scorpio: [
"M3 4a2 2 0 0 1 2 2v9",
"M5 6a2 2 0 0 1 4 0v9",
"M9 6a2 2 0 0 1 4 0v10a3 3 0 0 0 3 3h5l-3 -3m0 6l3 -3",
],
capricorn: [
"M4 4a3 3 0 0 1 3 3v9",
"M7 7a3 3 0 0 1 6 0v11a3 3 0 0 1 -3 3",
"M16 17m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0",
],
};
const paths = iconPaths[sign] || [];
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
{paths.map((path, index) => (
<path key={index} d={path} />
))}
</svg>
);
};
const HoroscopeApp = () => {
const [selectedSign, setSelectedSign] = useState("virgo");
const [selectedGender, setSelectedGender] = useState("female");
const [selectedPeriod, setSelectedPeriod] = useState("today");
const [selectedCategory, setSelectedCategory] = useState("overall");
const [isDarkMode, setIsDarkMode] = useState(false);
const [horoscopeData, setHoroscopeData] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const zodiacScrollRef = useRef(null);
const loadHoroscopeData = async (category, period) => {
setIsLoading(true);
setError(null);
try {
const data = await fetchHoroscopeData(category, period);
if (data && data.data) {
setHoroscopeData(data);
} else {
setHoroscopeData(null);
setError("No data available from GitHub, using fallback content");
}
} catch (err) {
setError("Failed to load horoscope data");
setHoroscopeData(null);
} finally {
setIsLoading(false);
}
};
const zodiacSigns = [
{
id: "aries",
name: "Aries",
dates: "Mar 21 - Apr 19",
emoji: "🐏",
symbol: "β™ˆ",
imageFile: "ARIES.png",
},
{
id: "taurus",
name: "Taurus",
dates: "April 20 - May 20",
emoji: "πŸ‚",
symbol: "♉",
imageFile: "TAURUS.png",
},
{
id: "gemini",
name: "Gemini",
dates: "May 21 - Jun 20",
emoji: "πŸ‘―",
symbol: "β™Š",
imageFile: "GEMINI.png",
},
{
id: "cancer",
name: "Cancer",
dates: "Jun 21 - Jul 22",
emoji: "πŸ¦€",
symbol: "β™‹",
imageFile: "CANCER.png",
},
{
id: "leo",
name: "Leo",
dates: "Jul 23 - Aug 22",
emoji: "🦁",
symbol: "β™Œ",
imageFile: "LEO.png",
},
{
id: "virgo",
name: "Virgo",
dates: "Aug 23 - Sep 22",
emoji: "πŸ‘Έ",
symbol: "♍",
imageFile: "VIRGO.png",
},
{
id: "libra",
name: "Libra",
dates: "Sep 23 - Oct 22",
emoji: "βš–οΈ",
symbol: "β™Ž",
imageFile: "LIBRA.png",
},
{
id: "scorpio",
name: "Scorpio",
dates: "Oct 23 - Nov 21",
emoji: "πŸ¦‚",
symbol: "♏",
imageFile: "SCORPIO.png",
},
{
id: "sagittarius",
name: "Sagittarius",
dates: "Nov 22 - Dec 21",
emoji: "🏹",
symbol: "♐",
imageFile: "SAGITTARIUS.png",
},
{
id: "capricorn",
name: "Capricorn",
dates: "Dec 22 - Jan 19",
emoji: "🐐",
symbol: "β™‘",
imageFile: "CAPRICORN.png",
},
{
id: "aquarius",
name: "Aquarius",
dates: "Jan 20 - Feb 18",
emoji: "🏺",
symbol: "β™’",
imageFile: "AQUARIUS.png",
},
{
id: "pisces",
name: "Pisces",
dates: "Feb 19 - Mar 20",
emoji: "🐠",
symbol: "β™“",
imageFile: "PISCES.png",
},
];
const periods = [
{ id: "today", name: "Today" },
{ id: "week", name: "Week" },
{ id: "month", name: "Month" },
{ id: "year", name: "Year" },
];
const categories = [
{
id: "overall",
name: "Overall",
icon: <i className="ri-dashboard-line"></i>,
},
{
id: "fortune",
name: "Fortune",
icon: <i className="ri-seedling-line"></i>,
},
{
id: "health",
name: "Health",
icon: <i className="ri-open-arm-line"></i>,
},
{ id: "love", name: "Love", icon: <i className="ri-hearts-line"></i> },
{
id: "finance",
name: "Finance",
icon: <i className="ri-wallet-3-line"></i>,
},
{
id: "relationship",
name: "Relationship",
icon: <i className="ri-team-line"></i>,
},
];
// const horoscopeContent = {
// overall: {
// today:
// "Today brings a harmonious blend of opportunities and challenges across all aspects of your life. Your natural instincts will guide you well.",
// week: "This week promises balanced growth in multiple areas. Trust your intuition as you navigate new experiences.",
// month:
// "A month of significant personal development awaits. Embrace change and new perspectives with confidence.",
// year: "This year marks a transformative period of growth, bringing profound insights and meaningful achievements.",
// },
// love: {
// today:
// "Love is in the air today! Your romantic side is highlighted, making it a perfect day for expressing your feelings.",
// week: "This week brings romantic opportunities and deepening connections with your loved ones.",
// month:
// "A month of passionate encounters and meaningful relationships awaits you.",
// year: "This year will be transformative for your love life, bringing lasting happiness.",
// },
// health: {
// today:
// "Your energy levels are high today. Focus on maintaining balance between work and rest.",
// week: "Pay attention to your physical wellbeing this week. Small changes can make big differences.",
// month:
// "This month emphasizes the importance of mental and physical health harmony.",
// year: "A year of wellness and vitality lies ahead. Make health your priority.",
// },
// relationship: {
// today:
// "Relationships flourish under today's cosmic energy. Communication is key to success.",
// week: "Strengthen your bonds with family and friends through meaningful conversations.",
// month:
// "Social connections expand this month, bringing new friendships and opportunities.",
// year: "Your social circle will grow significantly, enriching your life in unexpected ways.",
// },
// finance: {
// today:
// "Financial opportunities present themselves today. Stay alert for new possibilities.",
// week: "This week favors careful financial planning and smart investments.",
// month:
// "Money matters require attention this month. Budget wisely for future security.",
// year: "Financial growth and stability are highlighted throughout this year.",
// },
// fortune: {
// today:
// "Lucky energies surround you today. Trust your instincts and take calculated risks for unexpected rewards.",
// week: "Fortune favors the bold this week. Positive surprises and serendipitous encounters await.",
// month:
// "A month of abundance and good fortune unfolds. Opportunities align perfectly with your goals.",
// year: "This year brings remarkable luck and prosperity. Your efforts will be rewarded beyond expectations.",
// },
// };
// Function to get image path based on gender
const getImagePath = (sign) => {
if (!sign.imageFile) return null;
return `/img/${selectedGender}/${sign.imageFile}`;
};
const getCurrentSign = () => {
return zodiacSigns.find((sign) => sign.id === selectedSign);
};
const getHoroscopeText = () => {
// Hiển thα»‹ loading state
if (isLoading) {
return "Loading your horoscope...";
}
if (!horoscopeData || error) {
if (selectedPeriod !== "today") {
return "Upgrade to Premium to unlock weekly, monthly, and yearly horoscope insights. Get deeper cosmic guidance and detailed predictions for your future.";
}
return fallbackHoroscopeContent[selectedCategory][selectedPeriod];
}
const currentSign = getCurrentSign();
if (currentSign && horoscopeData.data[currentSign.id]) {
const signData = horoscopeData.data[currentSign.id];
return (
signData[selectedGender] ||
signData.male ||
signData.female ||
"No horoscope available for today."
);
}
return "No horoscope available for this sign.";
};
const getPeriodTitle = () => {
const titles = {
today: "Horoscope for Today",
week: "Horoscope for This Week",
month: "Horoscope for This Month",
year: "Horoscope for This Year",
};
return titles[selectedPeriod];
};
const toggleTheme = () => {
setIsDarkMode(!isDarkMode);
};
// Scroll to selected zodiac sign on mount and selection change
useEffect(() => {
if (zodiacScrollRef.current) {
const selectedElement = zodiacScrollRef.current.querySelector(
`[data-sign="${selectedSign}"]`
);
if (selectedElement) {
selectedElement.scrollIntoView({
behavior: "smooth",
block: "nearest",
inline: "center",
});
}
}
}, [selectedSign]);
// Load horoscope data when category or period changes
useEffect(() => {
loadHoroscopeData(selectedCategory, selectedPeriod);
}, [selectedCategory, selectedPeriod]);
// Handle zodiac sign selection with auto-center
const handleSignSelect = (signId) => {
setSelectedSign(signId);
};
return (
<div
className={`min-h-screen transition-all duration-500 ${
isDarkMode ? "text-white" : "text-gray-800"
}`}
style={{
background: isDarkMode
? "linear-gradient(135deg, #7E437B 0%, #0B194A 50%, #221C96 100%)"
: "linear-gradient(135deg, #A19BFF 0%, #FCC6EA 50%, #BADCFF 100%)",
}}
>
<div className="max-w-6xl mx-auto p-5">
{/* Header Controls */}
<div className="flex justify-between items-center mb-6 md:mb-8">
{/* Theme Toggle - Icon only for all screen sizes */}
<button
onClick={toggleTheme}
className={`flex items-center justify-center w-12 h-12 rounded-full font-medium transition-all duration-300 shadow-lg hover:scale-105 ${
isDarkMode
? "bg-white bg-opacity-10 text-white hover:bg-opacity-20"
: "bg-white bg-opacity-90 text-gray-700 hover:bg-opacity-100"
}`}
>
<i
className={`text-xl ${
isDarkMode ? "ri-sun-line" : "ri-moon-line"
}`}
></i>
</button>
{/* App Icon - Visible on mobile - Fixed centering */}
<div className="sm:hidden flex-1 flex justify-center">
<div className="inline-flex items-center justify-center w-12 h-12 bg-gradient-to-r from-blue-400 to-purple-500 rounded-full shadow-lg">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="icon icon-tabler icons-tabler-outline icon-tabler-crystal-ball"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M6.73 17.018a8 8 0 1 1 10.54 0" />
<path d="M5 19a2 2 0 0 0 2 2h10a2 2 0 1 0 0 -4h-10a2 2 0 0 0 -2 2z" />
<path d="M11 7a3 3 0 0 0 -3 3" />
</svg>
</div>
</div>
{/* Gender Selection - Toggle style on mobile, tabs on desktop */}
<button
onClick={() =>
setSelectedGender(selectedGender === "male" ? "female" : "male")
}
className={`flex items-center justify-center w-12 h-12 rounded-full font-medium transition-all duration-300 shadow-lg hover:scale-105 sm:hidden ${
isDarkMode
? "bg-white bg-opacity-10 text-white hover:bg-opacity-20"
: "bg-white bg-opacity-90 text-gray-700 hover:bg-opacity-100"
}`}
>
<i
className={
selectedGender === "male"
? "ri-women-line text-xl"
: "ri-men-line text-xl"
}
></i>
</button>
{/* Gender Selection - Desktop tabs */}
<div
className={`hidden sm:flex rounded-full p-1 shadow-lg ${
isDarkMode ? "bg-white bg-opacity-10" : "bg-white bg-opacity-90"
}`}
>
<button
onClick={() => setSelectedGender("female")}
className={`px-3 md:px-6 py-2 rounded-full font-medium transition-all ${
selectedGender === "female"
? "bg-pink-400 text-white shadow-md"
: isDarkMode
? "text-white hover:text-pink-300"
: "text-gray-600 hover:text-pink-400"
}`}
>
<span>Female</span>
</button>
<button
onClick={() => setSelectedGender("male")}
className={`px-3 md:px-6 py-2 rounded-full font-medium transition-all ${
selectedGender === "male"
? "bg-blue-400 text-white shadow-md"
: isDarkMode
? "text-white hover:text-blue-300"
: "text-gray-600 hover:text-blue-400"
}`}
>
<span>Male</span>
</button>
</div>
</div>
{/* App Title - Hidden on mobile */}
<div className="text-center mb-6 md:mb-10 hidden sm:block">
<div className="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-r from-blue-400 to-purple-500 rounded-full mb-4 shadow-lg">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="icon icon-tabler icons-tabler-outline icon-tabler-crystal-ball"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M6.73 17.018a8 8 0 1 1 10.54 0" />
<path d="M5 19a2 2 0 0 0 2 2h10a2 2 0 1 0 0 -4h-10a2 2 0 0 0 -2 2z" />
<path d="M11 7a3 3 0 0 0 -3 3" />
</svg>
</div>
<h1 className="text-4xl font-bold mb-2">Horoscope App</h1>
</div>
{/* Zodiac Signs Grid - Single Row */}
<div
className="w-full overflow-x-auto mb-6 md:mb-10"
ref={zodiacScrollRef}
>
<div className="flex justify-start md:justify-center gap-2 md:gap-3 pb-4 px-4 min-w-max py-2">
{zodiacSigns.map((sign) => (
<div
key={sign.id}
data-sign={sign.id}
onClick={() => handleSignSelect(sign.id)}
className="flex flex-col items-center cursor-pointer transition-all duration-300 hover:scale-105 flex-shrink-0 py-1"
>
<div
className={`rounded-full p-0.5 transition-all duration-300 ${
selectedSign === sign.id
? "w-24 h-24 md:w-28 md:h-28 shadow-lg"
: "w-20 h-20 md:w-24 md:h-24"
}`}
style={{
background:
selectedSign === sign.id
? "linear-gradient(135deg, #1D3249 0%, #96D1E2 100%)"
: "linear-gradient(135deg, #07090A 0%, #979999 100%)",
}}
>
{/* Background layer for separation */}
<div
className={`w-full h-full rounded-full p-0.5 ${
isDarkMode ? "bg-gray-900" : "bg-gray-50"
}`}
>
<div
className={`w-full h-full rounded-full flex items-center justify-center text-xl overflow-hidden ${
isDarkMode ? "bg-gray-800" : "bg-gray-100"
}`}
>
{getImagePath(sign) ? (
<img
src={getImagePath(sign)}
alt={sign.name}
className="w-full h-full object-cover rounded-full"
onError={(e) => {
e.target.style.display = "none";
e.target.nextSibling.style.display = "block";
}}
/>
) : (
<ZodiacIcon
sign={sign.id}
size={selectedSign === sign.id ? 30 : 28}
className="text-gray-600"
/>
)}
<span
style={{
display: getImagePath(sign) ? "none" : "none",
}}
>
{sign.emoji}
</span>
</div>
</div>
</div>
<p
className={`text-xs font-medium mt-3 text-center flex items-center justify-center gap-1 transition-all duration-300 ${
selectedSign === sign.id ? "font-bold" : ""
}`}
>
<ZodiacIcon
sign={sign.id}
size={selectedSign === sign.id ? 17 : 16}
className="text-current"
/>
{sign.name}
</p>
</div>
))}
</div>
</div>
{/* Period Selection */}
<div className="flex justify-center mb-6 md:mb-8">
<div
className={`flex rounded-full p-1 shadow-lg ${
isDarkMode ? "bg-white bg-opacity-10" : "bg-white bg-opacity-90"
}`}
>
{periods.map((period) => (
<button
key={period.id}
onClick={() => setSelectedPeriod(period.id)}
className={`px-4 md:px-6 py-2 md:py-3 rounded-full font-medium transition-all text-sm md:text-base flex items-center gap-1 ${
selectedPeriod === period.id
? "bg-blue-400 text-white shadow-md"
: isDarkMode
? "text-white hover:text-blue-300"
: "text-gray-600 hover:text-blue-400"
}`}
>
{period.name}
{period.id !== "today" && (
<i className="ri-lock-line text-xs opacity-70"></i>
)}
</button>
))}
</div>
</div>
{/* Category Selection */}
<div className="flex justify-center flex-wrap gap-2 md:gap-3 mb-6 md:mb-10">
{categories.map((category) => (
<button
key={category.id}
onClick={() => setSelectedCategory(category.id)}
className={`flex items-center gap-2 px-3 md:px-4 py-2 rounded-full font-medium transition-all duration-300 shadow-lg hover:scale-105 text-sm md:text-base ${
selectedCategory === category.id
? "bg-purple-400 text-white shadow-lg"
: isDarkMode
? "bg-white bg-opacity-10 text-white hover:bg-opacity-20"
: "bg-white bg-opacity-90 text-gray-600 hover:bg-opacity-100"
}`}
>
{category.icon}
{category.name}
</button>
))}
</div>
{/* Horoscope Content */}
<div
className={`rounded-3xl p-8 mb-4 shadow-2xl backdrop-blur-lg transition-all ${
isDarkMode
? "bg-white bg-opacity-10 border border-white border-opacity-20"
: "bg-white bg-opacity-95"
}`}
>
<div className="flex items-center gap-6 mb-8">
<div
className="w-20 h-20 rounded-full p-0.5 shadow-lg"
style={{
background: "linear-gradient(135deg, #1D3249 0%, #96D1E2 100%)",
}}
>
{/* Background layer for separation */}
<div
className={`w-full h-full rounded-full p-0.5 ${
isDarkMode ? "bg-gray-900" : "bg-gray-50"
}`}
>
<div
className={`w-full h-full rounded-full flex items-center justify-center text-2xl overflow-hidden ${
isDarkMode ? "bg-gray-800" : "bg-gray-100"
}`}
>
{getImagePath(getCurrentSign()) ? (
<img
src={getImagePath(getCurrentSign())}
alt={getCurrentSign()?.name}
className="w-full h-full object-cover rounded-full"
onError={(e) => {
e.target.style.display = "none";
e.target.nextSibling.style.display = "block";
}}
/>
) : (
<ZodiacIcon
sign={getCurrentSign()?.id}
size={32}
className="text-gray-600"
/>
)}
<span
style={{
display: getImagePath(getCurrentSign()) ? "none" : "none",
}}
>
{getCurrentSign()?.emoji}
</span>
</div>
</div>
</div>
<div className="flex-1">
<h2 className="text-3xl font-bold text-blue-400 flex items-center gap-2 mb-2">
{getPeriodTitle()}
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="icon icon-tabler icons-tabler-outline icon-tabler-trending-up"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M3 17l6 -6l4 4l8 -8" />
<path d="M14 7l7 0l0 7" />
</svg>
</h2>
<div className="flex items-center gap-2">
<ZodiacIcon
sign={getCurrentSign()?.id}
size={20}
className="text-current"
/>
<span className="font-semibold">
{getCurrentSign()?.name} ({getCurrentSign()?.dates})
</span>
</div>
</div>
</div>
<div className="text-lg leading-relaxed mb-8">
{isLoading ? (
<div className="flex items-center justify-center py-4">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-400"></div>
<span className="ml-2">Loading your horoscope...</span>
</div>
) : (
<p>{getHoroscopeText()}</p>
)}
{error && (
<div className="text-sm opacity-70 mt-2">
<i className="ri-information-line mr-1"></i>
Using cached content
</div>
)}
</div>{" "}
</div>
{/* Footer */}
<div className="text-center text-sm opacity-70 mt-4 space-y-2">
<p className="flex items-center justify-center gap-2">
<i className="ri-sparkling-line"></i>
Discover your daily horoscope and unlock the secrets of the stars
<i className="ri-sparkling-line"></i>
</p>
<p className="text-xs opacity-60">
Β© 2025 AstroLens Horoscope - All rights reserved.
</p>
</div>
</div>
</div>
);
};
export default HoroscopeApp;