ntphuc149 commited on
Commit
dcc8c62
·
verified ·
1 Parent(s): 182168a

Upload 2 files

Browse files
Files changed (2) hide show
  1. src/App.js +757 -738
  2. src/services/horoscopeService.js +138 -0
src/App.js CHANGED
@@ -1,738 +1,757 @@
1
- import React, { useState, useEffect, useRef } from "react";
2
-
3
- // Zodiac Icon Components using Tabler Icons SVGs
4
- const ZodiacIcon = ({ sign, size = 24, className = "" }) => {
5
- const iconPaths = {
6
- taurus: [
7
- "M6 3a6 6 0 0 0 12 0",
8
- "M12 15m-6 0a6 6 0 1 0 12 0a6 6 0 1 0 -12 0",
9
- ],
10
- sagittarius: ["M4 20l16 -16", "M13 4h7v7", "M6.5 12.5l5 5"],
11
- pisces: ["M5 3a21 21 0 0 1 0 18", "M19 3a21 21 0 0 0 0 18", "M5 12l14 0"],
12
- libra: ["M5 20l14 0", "M5 17h5v-.3a7 7 0 1 1 4 0v.3h5"],
13
- gemini: [
14
- "M3 3a21 21 0 0 0 18 0",
15
- "M3 21a21 21 0 0 1 18 0",
16
- "M7 4.5l0 15",
17
- "M17 4.5l0 15",
18
- ],
19
- leo: [
20
- "M13 17a4 4 0 1 0 8 0",
21
- "M6 16m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0",
22
- "M11 7m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0",
23
- "M7 7c0 3 2 5 2 9",
24
- "M15 7c0 4 -2 6 -2 10",
25
- ],
26
- virgo: [
27
- "M3 4a2 2 0 0 1 2 2v9",
28
- "M5 6a2 2 0 0 1 4 0v9",
29
- "M9 6a2 2 0 0 1 4 0v10a7 5 0 0 0 7 5",
30
- "M12 21a7 5 0 0 0 7 -5v-2a3 3 0 0 0 -6 0",
31
- ],
32
- aquarius: [
33
- "M3 10l3 -3l3 3l3 -3l3 3l3 -3l3 3",
34
- "M3 17l3 -3l3 3l3 -3l3 3l3 -3l3 3",
35
- ],
36
- aries: ["M12 5a5 5 0 1 0 -4 8", "M16 13a5 5 0 1 0 -4 -8", "M12 21l0 -16"],
37
- cancer: [
38
- "M6 12m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0",
39
- "M18 12m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0",
40
- "M3 12a10 6.5 0 0 1 14 -6.5",
41
- "M21 12a10 6.5 0 0 1 -14 6.5",
42
- ],
43
- scorpio: [
44
- "M3 4a2 2 0 0 1 2 2v9",
45
- "M5 6a2 2 0 0 1 4 0v9",
46
- "M9 6a2 2 0 0 1 4 0v10a3 3 0 0 0 3 3h5l-3 -3m0 6l3 -3",
47
- ],
48
- capricorn: [
49
- "M4 4a3 3 0 0 1 3 3v9",
50
- "M7 7a3 3 0 0 1 6 0v11a3 3 0 0 1 -3 3",
51
- "M16 17m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0",
52
- ],
53
- };
54
-
55
- const paths = iconPaths[sign] || [];
56
-
57
- return (
58
- <svg
59
- width={size}
60
- height={size}
61
- viewBox="0 0 24 24"
62
- fill="none"
63
- stroke="currentColor"
64
- strokeWidth="2"
65
- strokeLinecap="round"
66
- strokeLinejoin="round"
67
- className={className}
68
- >
69
- <path stroke="none" d="M0 0h24v24H0z" fill="none" />
70
- {paths.map((path, index) => (
71
- <path key={index} d={path} />
72
- ))}
73
- </svg>
74
- );
75
- };
76
-
77
- const HoroscopeApp = () => {
78
- const [selectedSign, setSelectedSign] = useState("virgo");
79
- const [selectedGender, setSelectedGender] = useState("female");
80
- const [selectedPeriod, setSelectedPeriod] = useState("today");
81
- const [selectedCategory, setSelectedCategory] = useState("overall");
82
- const [isDarkMode, setIsDarkMode] = useState(false);
83
- const zodiacScrollRef = useRef(null);
84
-
85
- const zodiacSigns = [
86
- {
87
- id: "aries",
88
- name: "Aries",
89
- dates: "Mar 21 - Apr 19",
90
- emoji: "🐏",
91
- symbol: "♈",
92
- imageFile: "ARIES.png",
93
- },
94
- {
95
- id: "taurus",
96
- name: "Taurus",
97
- dates: "April 20 - May 20",
98
- emoji: "🐂",
99
- symbol: "♉",
100
- imageFile: "TAURUS.png",
101
- },
102
- {
103
- id: "gemini",
104
- name: "Gemini",
105
- dates: "May 21 - Jun 20",
106
- emoji: "👯",
107
- symbol: "♊",
108
- imageFile: "GEMINI.png",
109
- },
110
- {
111
- id: "cancer",
112
- name: "Cancer",
113
- dates: "Jun 21 - Jul 22",
114
- emoji: "🦀",
115
- symbol: "♋",
116
- imageFile: "CANCER.png",
117
- },
118
- {
119
- id: "leo",
120
- name: "Leo",
121
- dates: "Jul 23 - Aug 22",
122
- emoji: "🦁",
123
- symbol: "♌",
124
- imageFile: "LEO.png",
125
- },
126
- {
127
- id: "virgo",
128
- name: "Virgo",
129
- dates: "Aug 23 - Sep 22",
130
- emoji: "👸",
131
- symbol: "♍",
132
- imageFile: "VIRGO.png",
133
- },
134
- {
135
- id: "libra",
136
- name: "Libra",
137
- dates: "Sep 23 - Oct 22",
138
- emoji: "⚖️",
139
- symbol: "♎",
140
- imageFile: "LIBRA.png",
141
- },
142
- {
143
- id: "scorpio",
144
- name: "Scorpio",
145
- dates: "Oct 23 - Nov 21",
146
- emoji: "🦂",
147
- symbol: "♏",
148
- imageFile: "SCORPIO.png",
149
- },
150
- {
151
- id: "sagittarius",
152
- name: "Sagittarius",
153
- dates: "Nov 22 - Dec 21",
154
- emoji: "🏹",
155
- symbol: "♐",
156
- imageFile: "SAGITTARIUS.png",
157
- },
158
- {
159
- id: "capricorn",
160
- name: "Capricorn",
161
- dates: "Dec 22 - Jan 19",
162
- emoji: "🐐",
163
- symbol: "♑",
164
- imageFile: "CAPRICORN.png",
165
- },
166
- {
167
- id: "aquarius",
168
- name: "Aquarius",
169
- dates: "Jan 20 - Feb 18",
170
- emoji: "🏺",
171
- symbol: "♒",
172
- imageFile: "AQUARIUS.png",
173
- },
174
- {
175
- id: "pisces",
176
- name: "Pisces",
177
- dates: "Feb 19 - Mar 20",
178
- emoji: "🐠",
179
- symbol: "♓",
180
- imageFile: "PISCES.png",
181
- },
182
- ];
183
-
184
- const periods = [
185
- { id: "today", name: "Today" },
186
- { id: "week", name: "Week" },
187
- { id: "month", name: "Month" },
188
- { id: "year", name: "Year" },
189
- ];
190
-
191
- const categories = [
192
- {
193
- id: "overall",
194
- name: "Overall",
195
- icon: <i class="ri-dashboard-line"></i>,
196
- },
197
- {
198
- id: "fortune",
199
- name: "Fortune",
200
- icon: <i class="ri-seedling-line"></i>,
201
- },
202
- {
203
- id: "health",
204
- name: "Health",
205
- icon: <i className="ri-open-arm-line"></i>,
206
- },
207
- { id: "love", name: "Love", icon: <i className="ri-hearts-line"></i> },
208
- {
209
- id: "finance",
210
- name: "Finance",
211
- icon: <i className="ri-wallet-3-line"></i>,
212
- },
213
- {
214
- id: "relationship",
215
- name: "Relationship",
216
- icon: <i className="ri-team-line"></i>,
217
- },
218
- ];
219
-
220
- const horoscopeContent = {
221
- overall: {
222
- today:
223
- "Today brings a harmonious blend of opportunities and challenges across all aspects of your life. Your natural instincts will guide you well.",
224
- week: "This week promises balanced growth in multiple areas. Trust your intuition as you navigate new experiences.",
225
- month:
226
- "A month of significant personal development awaits. Embrace change and new perspectives with confidence.",
227
- year: "This year marks a transformative period of growth, bringing profound insights and meaningful achievements.",
228
- },
229
- love: {
230
- today:
231
- "Love is in the air today! Your romantic side is highlighted, making it a perfect day for expressing your feelings.",
232
- week: "This week brings romantic opportunities and deepening connections with your loved ones.",
233
- month:
234
- "A month of passionate encounters and meaningful relationships awaits you.",
235
- year: "This year will be transformative for your love life, bringing lasting happiness.",
236
- },
237
- health: {
238
- today:
239
- "Your energy levels are high today. Focus on maintaining balance between work and rest.",
240
- week: "Pay attention to your physical wellbeing this week. Small changes can make big differences.",
241
- month:
242
- "This month emphasizes the importance of mental and physical health harmony.",
243
- year: "A year of wellness and vitality lies ahead. Make health your priority.",
244
- },
245
- relationship: {
246
- today:
247
- "Relationships flourish under today's cosmic energy. Communication is key to success.",
248
- week: "Strengthen your bonds with family and friends through meaningful conversations.",
249
- month:
250
- "Social connections expand this month, bringing new friendships and opportunities.",
251
- year: "Your social circle will grow significantly, enriching your life in unexpected ways.",
252
- },
253
- finance: {
254
- today:
255
- "Financial opportunities present themselves today. Stay alert for new possibilities.",
256
- week: "This week favors careful financial planning and smart investments.",
257
- month:
258
- "Money matters require attention this month. Budget wisely for future security.",
259
- year: "Financial growth and stability are highlighted throughout this year.",
260
- },
261
- fortune: {
262
- today:
263
- "Lucky energies surround you today. Trust your instincts and take calculated risks for unexpected rewards.",
264
- week: "Fortune favors the bold this week. Positive surprises and serendipitous encounters await.",
265
- month:
266
- "A month of abundance and good fortune unfolds. Opportunities align perfectly with your goals.",
267
- year: "This year brings remarkable luck and prosperity. Your efforts will be rewarded beyond expectations.",
268
- },
269
- };
270
-
271
- const luckyData = {
272
- colors: [
273
- "Green",
274
- "Blue",
275
- "Purple",
276
- "Gold",
277
- "Silver",
278
- "Red",
279
- "Pink",
280
- "Orange",
281
- "Yellow",
282
- "Turquoise",
283
- "Violet",
284
- "Indigo",
285
- ],
286
- numbers: [1, 3, 7, 9, 11, 13, 21, 23, 27, 33, 42, 77],
287
- stones: [
288
- "Emerald",
289
- "Sapphire",
290
- "Ruby",
291
- "Diamond",
292
- "Amethyst",
293
- "Topaz",
294
- "Opal",
295
- "Pearl",
296
- "Garnet",
297
- "Aquamarine",
298
- "Citrine",
299
- "Turquoise",
300
- ],
301
- moods: [
302
- "Optimistic",
303
- "Energetic",
304
- "Peaceful",
305
- "Confident",
306
- "Creative",
307
- "Adventurous",
308
- "Romantic",
309
- "Focused",
310
- "Joyful",
311
- "Balanced",
312
- "Inspired",
313
- "Harmonious",
314
- ],
315
- };
316
-
317
- // Function to get image path based on gender
318
- const getImagePath = (sign) => {
319
- if (!sign.imageFile) return null;
320
- return `/img/${selectedGender}/${sign.imageFile}`;
321
- };
322
-
323
- const getCurrentSign = () => {
324
- return zodiacSigns.find((sign) => sign.id === selectedSign);
325
- };
326
-
327
- const getHoroscopeText = () => {
328
- if (selectedPeriod !== "today") {
329
- return "Upgrade to Premium to unlock weekly, monthly, and yearly horoscope insights. Get deeper cosmic guidance and detailed predictions for your future.";
330
- }
331
- return horoscopeContent[selectedCategory][selectedPeriod];
332
- };
333
-
334
- const getLuckyElement = (type) => {
335
- const data = luckyData[type];
336
- return data[Math.floor(Math.random() * data.length)];
337
- };
338
-
339
- const getPeriodTitle = () => {
340
- const titles = {
341
- today: "Horoscope for Today",
342
- week: "Horoscope for This Week",
343
- month: "Horoscope for This Month",
344
- year: "Horoscope for This Year",
345
- };
346
- return titles[selectedPeriod];
347
- };
348
-
349
- const toggleTheme = () => {
350
- setIsDarkMode(!isDarkMode);
351
- };
352
-
353
- // Scroll to selected zodiac sign on mount and selection change
354
- useEffect(() => {
355
- if (zodiacScrollRef.current) {
356
- const selectedElement = zodiacScrollRef.current.querySelector(
357
- `[data-sign="${selectedSign}"]`
358
- );
359
- if (selectedElement) {
360
- selectedElement.scrollIntoView({
361
- behavior: "smooth",
362
- block: "nearest",
363
- inline: "center",
364
- });
365
- }
366
- }
367
- }, [selectedSign]);
368
-
369
- // Handle zodiac sign selection with auto-center
370
- const handleSignSelect = (signId) => {
371
- setSelectedSign(signId);
372
- };
373
-
374
- return (
375
- <div
376
- className={`min-h-screen transition-all duration-500 ${
377
- isDarkMode ? "text-white" : "text-gray-800"
378
- }`}
379
- style={{
380
- background: isDarkMode
381
- ? "linear-gradient(135deg, #7E437B 0%, #0B194A 50%, #221C96 100%)"
382
- : "linear-gradient(135deg, #A19BFF 0%, #FCC6EA 50%, #BADCFF 100%)",
383
- }}
384
- >
385
- <div className="max-w-6xl mx-auto p-5">
386
- {/* Header Controls */}
387
- <div className="flex justify-between items-center mb-6 md:mb-8">
388
- {/* Theme Toggle - Icon only for all screen sizes */}
389
- <button
390
- onClick={toggleTheme}
391
- className={`flex items-center justify-center w-12 h-12 rounded-full font-medium transition-all duration-300 shadow-lg hover:scale-105 ${
392
- isDarkMode
393
- ? "bg-white bg-opacity-10 text-white hover:bg-opacity-20"
394
- : "bg-white bg-opacity-90 text-gray-700 hover:bg-opacity-100"
395
- }`}
396
- >
397
- <i
398
- className={`text-xl ${
399
- isDarkMode ? "ri-sun-line" : "ri-moon-line"
400
- }`}
401
- ></i>
402
- </button>
403
-
404
- {/* App Icon - Visible on mobile - Fixed centering */}
405
- <div className="sm:hidden flex-1 flex justify-center">
406
- <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">
407
- <svg
408
- xmlns="http://www.w3.org/2000/svg"
409
- width="24"
410
- height="24"
411
- viewBox="0 0 24 24"
412
- fill="none"
413
- stroke="currentColor"
414
- stroke-width="2"
415
- stroke-linecap="round"
416
- stroke-linejoin="round"
417
- class="icon icon-tabler icons-tabler-outline icon-tabler-crystal-ball"
418
- >
419
- <path stroke="none" d="M0 0h24v24H0z" fill="none" />
420
- <path d="M6.73 17.018a8 8 0 1 1 10.54 0" />
421
- <path d="M5 19a2 2 0 0 0 2 2h10a2 2 0 1 0 0 -4h-10a2 2 0 0 0 -2 2z" />
422
- <path d="M11 7a3 3 0 0 0 -3 3" />
423
- </svg>
424
- </div>
425
- </div>
426
-
427
- {/* Gender Selection - Toggle style on mobile, tabs on desktop */}
428
- <button
429
- onClick={() =>
430
- setSelectedGender(selectedGender === "male" ? "female" : "male")
431
- }
432
- 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 ${
433
- isDarkMode
434
- ? "bg-white bg-opacity-10 text-white hover:bg-opacity-20"
435
- : "bg-white bg-opacity-90 text-gray-700 hover:bg-opacity-100"
436
- }`}
437
- >
438
- <i
439
- className={
440
- selectedGender === "male"
441
- ? "ri-women-line text-xl"
442
- : "ri-men-line text-xl"
443
- }
444
- ></i>
445
- </button>
446
-
447
- {/* Gender Selection - Desktop tabs */}
448
- <div
449
- className={`hidden sm:flex rounded-full p-1 shadow-lg ${
450
- isDarkMode ? "bg-white bg-opacity-10" : "bg-white bg-opacity-90"
451
- }`}
452
- >
453
- <button
454
- onClick={() => setSelectedGender("female")}
455
- className={`px-3 md:px-6 py-2 rounded-full font-medium transition-all ${
456
- selectedGender === "female"
457
- ? "bg-pink-400 text-white shadow-md"
458
- : isDarkMode
459
- ? "text-white hover:text-pink-300"
460
- : "text-gray-600 hover:text-pink-400"
461
- }`}
462
- >
463
- <span>Female</span>
464
- </button>
465
- <button
466
- onClick={() => setSelectedGender("male")}
467
- className={`px-3 md:px-6 py-2 rounded-full font-medium transition-all ${
468
- selectedGender === "male"
469
- ? "bg-blue-400 text-white shadow-md"
470
- : isDarkMode
471
- ? "text-white hover:text-blue-300"
472
- : "text-gray-600 hover:text-blue-400"
473
- }`}
474
- >
475
- <span>Male</span>
476
- </button>
477
- </div>
478
- </div>
479
-
480
- {/* App Title - Hidden on mobile */}
481
- <div className="text-center mb-6 md:mb-10 hidden sm:block">
482
- <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">
483
- <svg
484
- xmlns="http://www.w3.org/2000/svg"
485
- width="24"
486
- height="24"
487
- viewBox="0 0 24 24"
488
- fill="none"
489
- stroke="currentColor"
490
- stroke-width="2"
491
- stroke-linecap="round"
492
- stroke-linejoin="round"
493
- class="icon icon-tabler icons-tabler-outline icon-tabler-crystal-ball"
494
- >
495
- <path stroke="none" d="M0 0h24v24H0z" fill="none" />
496
- <path d="M6.73 17.018a8 8 0 1 1 10.54 0" />
497
- <path d="M5 19a2 2 0 0 0 2 2h10a2 2 0 1 0 0 -4h-10a2 2 0 0 0 -2 2z" />
498
- <path d="M11 7a3 3 0 0 0 -3 3" />
499
- </svg>
500
- </div>
501
- <h1 className="text-4xl font-bold mb-2">Horoscope App</h1>
502
- </div>
503
-
504
- {/* Zodiac Signs Grid - Single Row */}
505
- <div
506
- className="w-full overflow-x-auto mb-6 md:mb-10"
507
- ref={zodiacScrollRef}
508
- >
509
- <div className="flex justify-start md:justify-center gap-2 md:gap-3 pb-4 px-4 min-w-max py-2">
510
- {zodiacSigns.map((sign) => (
511
- <div
512
- key={sign.id}
513
- data-sign={sign.id}
514
- onClick={() => handleSignSelect(sign.id)}
515
- className="flex flex-col items-center cursor-pointer transition-all duration-300 hover:scale-105 flex-shrink-0 py-1"
516
- >
517
- <div
518
- className={`rounded-full p-0.5 transition-all duration-300 ${
519
- selectedSign === sign.id
520
- ? "w-24 h-24 md:w-28 md:h-28 shadow-lg"
521
- : "w-20 h-20 md:w-24 md:h-24"
522
- }`}
523
- style={{
524
- background:
525
- selectedSign === sign.id
526
- ? "linear-gradient(135deg, #1D3249 0%, #96D1E2 100%)"
527
- : "linear-gradient(135deg, #07090A 0%, #979999 100%)",
528
- }}
529
- >
530
- {/* Background layer for separation */}
531
- <div
532
- className={`w-full h-full rounded-full p-0.5 ${
533
- isDarkMode ? "bg-gray-900" : "bg-gray-50"
534
- }`}
535
- >
536
- <div
537
- className={`w-full h-full rounded-full flex items-center justify-center text-xl overflow-hidden ${
538
- isDarkMode ? "bg-gray-800" : "bg-gray-100"
539
- }`}
540
- >
541
- {getImagePath(sign) ? (
542
- <img
543
- src={getImagePath(sign)}
544
- alt={sign.name}
545
- className="w-full h-full object-cover rounded-full"
546
- onError={(e) => {
547
- e.target.style.display = "none";
548
- e.target.nextSibling.style.display = "block";
549
- }}
550
- />
551
- ) : (
552
- <ZodiacIcon
553
- sign={sign.id}
554
- size={selectedSign === sign.id ? 30 : 28}
555
- className="text-gray-600"
556
- />
557
- )}
558
- <span
559
- style={{
560
- display: getImagePath(sign) ? "none" : "none",
561
- }}
562
- >
563
- {sign.emoji}
564
- </span>
565
- </div>
566
- </div>
567
- </div>
568
- <p
569
- className={`text-xs font-medium mt-3 text-center flex items-center justify-center gap-1 transition-all duration-300 ${
570
- selectedSign === sign.id ? "font-bold" : ""
571
- }`}
572
- >
573
- <ZodiacIcon
574
- sign={sign.id}
575
- size={selectedSign === sign.id ? 17 : 16}
576
- className="text-current"
577
- />
578
- {sign.name}
579
- </p>
580
- </div>
581
- ))}
582
- </div>
583
- </div>
584
-
585
- {/* Period Selection */}
586
- <div className="flex justify-center mb-6 md:mb-8">
587
- <div
588
- className={`flex rounded-full p-1 shadow-lg ${
589
- isDarkMode ? "bg-white bg-opacity-10" : "bg-white bg-opacity-90"
590
- }`}
591
- >
592
- {periods.map((period) => (
593
- <button
594
- key={period.id}
595
- onClick={() => setSelectedPeriod(period.id)}
596
- 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 ${
597
- selectedPeriod === period.id
598
- ? "bg-blue-400 text-white shadow-md"
599
- : isDarkMode
600
- ? "text-white hover:text-blue-300"
601
- : "text-gray-600 hover:text-blue-400"
602
- }`}
603
- >
604
- {period.name}
605
- {period.id !== "today" && (
606
- <i className="ri-lock-line text-xs opacity-70"></i>
607
- )}
608
- </button>
609
- ))}
610
- </div>
611
- </div>
612
-
613
- {/* Category Selection */}
614
- <div className="flex justify-center flex-wrap gap-2 md:gap-3 mb-6 md:mb-10">
615
- {categories.map((category) => (
616
- <button
617
- key={category.id}
618
- onClick={() => setSelectedCategory(category.id)}
619
- 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 ${
620
- selectedCategory === category.id
621
- ? "bg-purple-400 text-white shadow-lg"
622
- : isDarkMode
623
- ? "bg-white bg-opacity-10 text-white hover:bg-opacity-20"
624
- : "bg-white bg-opacity-90 text-gray-600 hover:bg-opacity-100"
625
- }`}
626
- >
627
- {category.icon}
628
- {category.name}
629
- </button>
630
- ))}
631
- </div>
632
-
633
- {/* Horoscope Content */}
634
- <div
635
- className={`rounded-3xl p-8 mb-4 shadow-2xl backdrop-blur-lg transition-all ${
636
- isDarkMode
637
- ? "bg-white bg-opacity-10 border border-white border-opacity-20"
638
- : "bg-white bg-opacity-95"
639
- }`}
640
- >
641
- <div className="flex items-center gap-6 mb-8">
642
- <div
643
- className="w-20 h-20 rounded-full p-0.5 shadow-lg"
644
- style={{
645
- background: "linear-gradient(135deg, #1D3249 0%, #96D1E2 100%)",
646
- }}
647
- >
648
- {/* Background layer for separation */}
649
- <div
650
- className={`w-full h-full rounded-full p-0.5 ${
651
- isDarkMode ? "bg-gray-900" : "bg-gray-50"
652
- }`}
653
- >
654
- <div
655
- className={`w-full h-full rounded-full flex items-center justify-center text-2xl overflow-hidden ${
656
- isDarkMode ? "bg-gray-800" : "bg-gray-100"
657
- }`}
658
- >
659
- {getImagePath(getCurrentSign()) ? (
660
- <img
661
- src={getImagePath(getCurrentSign())}
662
- alt={getCurrentSign()?.name}
663
- className="w-full h-full object-cover rounded-full"
664
- onError={(e) => {
665
- e.target.style.display = "none";
666
- e.target.nextSibling.style.display = "block";
667
- }}
668
- />
669
- ) : (
670
- <ZodiacIcon
671
- sign={getCurrentSign()?.id}
672
- size={32}
673
- className="text-gray-600"
674
- />
675
- )}
676
- <span
677
- style={{
678
- display: getImagePath(getCurrentSign()) ? "none" : "none",
679
- }}
680
- >
681
- {getCurrentSign()?.emoji}
682
- </span>
683
- </div>
684
- </div>
685
- </div>
686
- <div className="flex-1">
687
- <h2 className="text-3xl font-bold text-blue-400 flex items-center gap-2 mb-2">
688
- {getPeriodTitle()}
689
- <svg
690
- xmlns="http://www.w3.org/2000/svg"
691
- width="24"
692
- height="24"
693
- viewBox="0 0 24 24"
694
- fill="none"
695
- stroke="currentColor"
696
- stroke-width="2"
697
- stroke-linecap="round"
698
- stroke-linejoin="round"
699
- class="icon icon-tabler icons-tabler-outline icon-tabler-trending-up"
700
- >
701
- <path stroke="none" d="M0 0h24v24H0z" fill="none" />
702
- <path d="M3 17l6 -6l4 4l8 -8" />
703
- <path d="M14 7l7 0l0 7" />
704
- </svg>
705
- </h2>
706
- <div className="flex items-center gap-2">
707
- <ZodiacIcon
708
- sign={getCurrentSign()?.id}
709
- size={20}
710
- className="text-current"
711
- />
712
- <span className="font-semibold">
713
- {getCurrentSign()?.name} ({getCurrentSign()?.dates})
714
- </span>
715
- </div>
716
- </div>
717
- </div>
718
-
719
- <p className="text-lg leading-relaxed mb-8">{getHoroscopeText()}</p>
720
- </div>
721
-
722
- {/* Footer */}
723
- <div className="text-center text-sm opacity-70 mt-4 space-y-2">
724
- <p className="flex items-center justify-center gap-2">
725
- <i className="ri-sparkling-line"></i>
726
- Discover your daily horoscope and unlock the secrets of the stars
727
- <i className="ri-sparkling-line"></i>
728
- </p>
729
- <p className="text-xs opacity-60">
730
- © 2025 AstroLens Horoscope - All rights reserved.
731
- </p>
732
- </div>
733
- </div>
734
- </div>
735
- );
736
- };
737
-
738
- export default HoroscopeApp;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect, useRef } from "react";
2
+ import {
3
+ fetchHoroscopeData,
4
+ fallbackHoroscopeContent,
5
+ } from "./services/horoscopeService";
6
+
7
+ // Zodiac Icon Components using Tabler Icons SVGs
8
+ const ZodiacIcon = ({ sign, size = 24, className = "" }) => {
9
+ const iconPaths = {
10
+ taurus: [
11
+ "M6 3a6 6 0 0 0 12 0",
12
+ "M12 15m-6 0a6 6 0 1 0 12 0a6 6 0 1 0 -12 0",
13
+ ],
14
+ sagittarius: ["M4 20l16 -16", "M13 4h7v7", "M6.5 12.5l5 5"],
15
+ pisces: ["M5 3a21 21 0 0 1 0 18", "M19 3a21 21 0 0 0 0 18", "M5 12l14 0"],
16
+ libra: ["M5 20l14 0", "M5 17h5v-.3a7 7 0 1 1 4 0v.3h5"],
17
+ gemini: [
18
+ "M3 3a21 21 0 0 0 18 0",
19
+ "M3 21a21 21 0 0 1 18 0",
20
+ "M7 4.5l0 15",
21
+ "M17 4.5l0 15",
22
+ ],
23
+ leo: [
24
+ "M13 17a4 4 0 1 0 8 0",
25
+ "M6 16m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0",
26
+ "M11 7m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0",
27
+ "M7 7c0 3 2 5 2 9",
28
+ "M15 7c0 4 -2 6 -2 10",
29
+ ],
30
+ virgo: [
31
+ "M3 4a2 2 0 0 1 2 2v9",
32
+ "M5 6a2 2 0 0 1 4 0v9",
33
+ "M9 6a2 2 0 0 1 4 0v10a7 5 0 0 0 7 5",
34
+ "M12 21a7 5 0 0 0 7 -5v-2a3 3 0 0 0 -6 0",
35
+ ],
36
+ aquarius: [
37
+ "M3 10l3 -3l3 3l3 -3l3 3l3 -3l3 3",
38
+ "M3 17l3 -3l3 3l3 -3l3 3l3 -3l3 3",
39
+ ],
40
+ aries: ["M12 5a5 5 0 1 0 -4 8", "M16 13a5 5 0 1 0 -4 -8", "M12 21l0 -16"],
41
+ cancer: [
42
+ "M6 12m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0",
43
+ "M18 12m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0",
44
+ "M3 12a10 6.5 0 0 1 14 -6.5",
45
+ "M21 12a10 6.5 0 0 1 -14 6.5",
46
+ ],
47
+ scorpio: [
48
+ "M3 4a2 2 0 0 1 2 2v9",
49
+ "M5 6a2 2 0 0 1 4 0v9",
50
+ "M9 6a2 2 0 0 1 4 0v10a3 3 0 0 0 3 3h5l-3 -3m0 6l3 -3",
51
+ ],
52
+ capricorn: [
53
+ "M4 4a3 3 0 0 1 3 3v9",
54
+ "M7 7a3 3 0 0 1 6 0v11a3 3 0 0 1 -3 3",
55
+ "M16 17m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0",
56
+ ],
57
+ };
58
+
59
+ const paths = iconPaths[sign] || [];
60
+
61
+ return (
62
+ <svg
63
+ width={size}
64
+ height={size}
65
+ viewBox="0 0 24 24"
66
+ fill="none"
67
+ stroke="currentColor"
68
+ strokeWidth="2"
69
+ strokeLinecap="round"
70
+ strokeLinejoin="round"
71
+ className={className}
72
+ >
73
+ <path stroke="none" d="M0 0h24v24H0z" fill="none" />
74
+ {paths.map((path, index) => (
75
+ <path key={index} d={path} />
76
+ ))}
77
+ </svg>
78
+ );
79
+ };
80
+
81
+ const HoroscopeApp = () => {
82
+ const [selectedSign, setSelectedSign] = useState("virgo");
83
+ const [selectedGender, setSelectedGender] = useState("female");
84
+ const [selectedPeriod, setSelectedPeriod] = useState("today");
85
+ const [selectedCategory, setSelectedCategory] = useState("overall");
86
+ const [isDarkMode, setIsDarkMode] = useState(false);
87
+
88
+ const [horoscopeData, setHoroscopeData] = useState(null);
89
+ const [isLoading, setIsLoading] = useState(false);
90
+ const [error, setError] = useState(null);
91
+
92
+ const zodiacScrollRef = useRef(null);
93
+
94
+ const loadHoroscopeData = async (category, period) => {
95
+ setIsLoading(true);
96
+ setError(null);
97
+
98
+ try {
99
+ const data = await fetchHoroscopeData(category, period);
100
+
101
+ if (data && data.data) {
102
+ setHoroscopeData(data);
103
+ } else {
104
+ setHoroscopeData(null);
105
+ setError("No data available from GitHub, using fallback content");
106
+ }
107
+ } catch (err) {
108
+ setError("Failed to load horoscope data");
109
+ setHoroscopeData(null);
110
+ } finally {
111
+ setIsLoading(false);
112
+ }
113
+ };
114
+
115
+ const zodiacSigns = [
116
+ {
117
+ id: "aries",
118
+ name: "Aries",
119
+ dates: "Mar 21 - Apr 19",
120
+ emoji: "🐏",
121
+ symbol: "",
122
+ imageFile: "ARIES.png",
123
+ },
124
+ {
125
+ id: "taurus",
126
+ name: "Taurus",
127
+ dates: "April 20 - May 20",
128
+ emoji: "🐂",
129
+ symbol: "",
130
+ imageFile: "TAURUS.png",
131
+ },
132
+ {
133
+ id: "gemini",
134
+ name: "Gemini",
135
+ dates: "May 21 - Jun 20",
136
+ emoji: "👯",
137
+ symbol: "",
138
+ imageFile: "GEMINI.png",
139
+ },
140
+ {
141
+ id: "cancer",
142
+ name: "Cancer",
143
+ dates: "Jun 21 - Jul 22",
144
+ emoji: "🦀",
145
+ symbol: "",
146
+ imageFile: "CANCER.png",
147
+ },
148
+ {
149
+ id: "leo",
150
+ name: "Leo",
151
+ dates: "Jul 23 - Aug 22",
152
+ emoji: "🦁",
153
+ symbol: "",
154
+ imageFile: "LEO.png",
155
+ },
156
+ {
157
+ id: "virgo",
158
+ name: "Virgo",
159
+ dates: "Aug 23 - Sep 22",
160
+ emoji: "👸",
161
+ symbol: "",
162
+ imageFile: "VIRGO.png",
163
+ },
164
+ {
165
+ id: "libra",
166
+ name: "Libra",
167
+ dates: "Sep 23 - Oct 22",
168
+ emoji: "⚖️",
169
+ symbol: "",
170
+ imageFile: "LIBRA.png",
171
+ },
172
+ {
173
+ id: "scorpio",
174
+ name: "Scorpio",
175
+ dates: "Oct 23 - Nov 21",
176
+ emoji: "🦂",
177
+ symbol: "",
178
+ imageFile: "SCORPIO.png",
179
+ },
180
+ {
181
+ id: "sagittarius",
182
+ name: "Sagittarius",
183
+ dates: "Nov 22 - Dec 21",
184
+ emoji: "🏹",
185
+ symbol: "",
186
+ imageFile: "SAGITTARIUS.png",
187
+ },
188
+ {
189
+ id: "capricorn",
190
+ name: "Capricorn",
191
+ dates: "Dec 22 - Jan 19",
192
+ emoji: "🐐",
193
+ symbol: "",
194
+ imageFile: "CAPRICORN.png",
195
+ },
196
+ {
197
+ id: "aquarius",
198
+ name: "Aquarius",
199
+ dates: "Jan 20 - Feb 18",
200
+ emoji: "🏺",
201
+ symbol: "♒",
202
+ imageFile: "AQUARIUS.png",
203
+ },
204
+ {
205
+ id: "pisces",
206
+ name: "Pisces",
207
+ dates: "Feb 19 - Mar 20",
208
+ emoji: "🐠",
209
+ symbol: "",
210
+ imageFile: "PISCES.png",
211
+ },
212
+ ];
213
+
214
+ const periods = [
215
+ { id: "today", name: "Today" },
216
+ { id: "week", name: "Week" },
217
+ { id: "month", name: "Month" },
218
+ { id: "year", name: "Year" },
219
+ ];
220
+
221
+ const categories = [
222
+ {
223
+ id: "overall",
224
+ name: "Overall",
225
+ icon: <i className="ri-dashboard-line"></i>,
226
+ },
227
+ {
228
+ id: "fortune",
229
+ name: "Fortune",
230
+ icon: <i className="ri-seedling-line"></i>,
231
+ },
232
+ {
233
+ id: "health",
234
+ name: "Health",
235
+ icon: <i className="ri-open-arm-line"></i>,
236
+ },
237
+ { id: "love", name: "Love", icon: <i className="ri-hearts-line"></i> },
238
+ {
239
+ id: "finance",
240
+ name: "Finance",
241
+ icon: <i className="ri-wallet-3-line"></i>,
242
+ },
243
+ {
244
+ id: "relationship",
245
+ name: "Relationship",
246
+ icon: <i className="ri-team-line"></i>,
247
+ },
248
+ ];
249
+
250
+ // const horoscopeContent = {
251
+ // overall: {
252
+ // today:
253
+ // "Today brings a harmonious blend of opportunities and challenges across all aspects of your life. Your natural instincts will guide you well.",
254
+ // week: "This week promises balanced growth in multiple areas. Trust your intuition as you navigate new experiences.",
255
+ // month:
256
+ // "A month of significant personal development awaits. Embrace change and new perspectives with confidence.",
257
+ // year: "This year marks a transformative period of growth, bringing profound insights and meaningful achievements.",
258
+ // },
259
+ // love: {
260
+ // today:
261
+ // "Love is in the air today! Your romantic side is highlighted, making it a perfect day for expressing your feelings.",
262
+ // week: "This week brings romantic opportunities and deepening connections with your loved ones.",
263
+ // month:
264
+ // "A month of passionate encounters and meaningful relationships awaits you.",
265
+ // year: "This year will be transformative for your love life, bringing lasting happiness.",
266
+ // },
267
+ // health: {
268
+ // today:
269
+ // "Your energy levels are high today. Focus on maintaining balance between work and rest.",
270
+ // week: "Pay attention to your physical wellbeing this week. Small changes can make big differences.",
271
+ // month:
272
+ // "This month emphasizes the importance of mental and physical health harmony.",
273
+ // year: "A year of wellness and vitality lies ahead. Make health your priority.",
274
+ // },
275
+ // relationship: {
276
+ // today:
277
+ // "Relationships flourish under today's cosmic energy. Communication is key to success.",
278
+ // week: "Strengthen your bonds with family and friends through meaningful conversations.",
279
+ // month:
280
+ // "Social connections expand this month, bringing new friendships and opportunities.",
281
+ // year: "Your social circle will grow significantly, enriching your life in unexpected ways.",
282
+ // },
283
+ // finance: {
284
+ // today:
285
+ // "Financial opportunities present themselves today. Stay alert for new possibilities.",
286
+ // week: "This week favors careful financial planning and smart investments.",
287
+ // month:
288
+ // "Money matters require attention this month. Budget wisely for future security.",
289
+ // year: "Financial growth and stability are highlighted throughout this year.",
290
+ // },
291
+ // fortune: {
292
+ // today:
293
+ // "Lucky energies surround you today. Trust your instincts and take calculated risks for unexpected rewards.",
294
+ // week: "Fortune favors the bold this week. Positive surprises and serendipitous encounters await.",
295
+ // month:
296
+ // "A month of abundance and good fortune unfolds. Opportunities align perfectly with your goals.",
297
+ // year: "This year brings remarkable luck and prosperity. Your efforts will be rewarded beyond expectations.",
298
+ // },
299
+ // };
300
+
301
+ // Function to get image path based on gender
302
+ const getImagePath = (sign) => {
303
+ if (!sign.imageFile) return null;
304
+ return `/img/${selectedGender}/${sign.imageFile}`;
305
+ };
306
+
307
+ const getCurrentSign = () => {
308
+ return zodiacSigns.find((sign) => sign.id === selectedSign);
309
+ };
310
+
311
+ const getHoroscopeText = () => {
312
+ // Hiển thị loading state
313
+ if (isLoading) {
314
+ return "Loading your horoscope...";
315
+ }
316
+
317
+ if (!horoscopeData || error) {
318
+ if (selectedPeriod !== "today") {
319
+ return "Upgrade to Premium to unlock weekly, monthly, and yearly horoscope insights. Get deeper cosmic guidance and detailed predictions for your future.";
320
+ }
321
+ return fallbackHoroscopeContent[selectedCategory][selectedPeriod];
322
+ }
323
+
324
+ const currentSign = getCurrentSign();
325
+ if (currentSign && horoscopeData.data[currentSign.id]) {
326
+ const signData = horoscopeData.data[currentSign.id];
327
+ return (
328
+ signData[selectedGender] ||
329
+ signData.male ||
330
+ signData.female ||
331
+ "No horoscope available for today."
332
+ );
333
+ }
334
+
335
+ return "No horoscope available for this sign.";
336
+ };
337
+
338
+ const getPeriodTitle = () => {
339
+ const titles = {
340
+ today: "Horoscope for Today",
341
+ week: "Horoscope for This Week",
342
+ month: "Horoscope for This Month",
343
+ year: "Horoscope for This Year",
344
+ };
345
+ return titles[selectedPeriod];
346
+ };
347
+
348
+ const toggleTheme = () => {
349
+ setIsDarkMode(!isDarkMode);
350
+ };
351
+
352
+ // Scroll to selected zodiac sign on mount and selection change
353
+ useEffect(() => {
354
+ if (zodiacScrollRef.current) {
355
+ const selectedElement = zodiacScrollRef.current.querySelector(
356
+ `[data-sign="${selectedSign}"]`
357
+ );
358
+ if (selectedElement) {
359
+ selectedElement.scrollIntoView({
360
+ behavior: "smooth",
361
+ block: "nearest",
362
+ inline: "center",
363
+ });
364
+ }
365
+ }
366
+ }, [selectedSign]);
367
+
368
+ // Load horoscope data when category or period changes
369
+ useEffect(() => {
370
+ loadHoroscopeData(selectedCategory, selectedPeriod);
371
+ }, [selectedCategory, selectedPeriod]);
372
+
373
+ // Handle zodiac sign selection with auto-center
374
+ const handleSignSelect = (signId) => {
375
+ setSelectedSign(signId);
376
+ };
377
+
378
+ return (
379
+ <div
380
+ className={`min-h-screen transition-all duration-500 ${
381
+ isDarkMode ? "text-white" : "text-gray-800"
382
+ }`}
383
+ style={{
384
+ background: isDarkMode
385
+ ? "linear-gradient(135deg, #7E437B 0%, #0B194A 50%, #221C96 100%)"
386
+ : "linear-gradient(135deg, #A19BFF 0%, #FCC6EA 50%, #BADCFF 100%)",
387
+ }}
388
+ >
389
+ <div className="max-w-6xl mx-auto p-5">
390
+ {/* Header Controls */}
391
+ <div className="flex justify-between items-center mb-6 md:mb-8">
392
+ {/* Theme Toggle - Icon only for all screen sizes */}
393
+ <button
394
+ onClick={toggleTheme}
395
+ className={`flex items-center justify-center w-12 h-12 rounded-full font-medium transition-all duration-300 shadow-lg hover:scale-105 ${
396
+ isDarkMode
397
+ ? "bg-white bg-opacity-10 text-white hover:bg-opacity-20"
398
+ : "bg-white bg-opacity-90 text-gray-700 hover:bg-opacity-100"
399
+ }`}
400
+ >
401
+ <i
402
+ className={`text-xl ${
403
+ isDarkMode ? "ri-sun-line" : "ri-moon-line"
404
+ }`}
405
+ ></i>
406
+ </button>
407
+
408
+ {/* App Icon - Visible on mobile - Fixed centering */}
409
+ <div className="sm:hidden flex-1 flex justify-center">
410
+ <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">
411
+ <svg
412
+ xmlns="http://www.w3.org/2000/svg"
413
+ width="24"
414
+ height="24"
415
+ viewBox="0 0 24 24"
416
+ fill="none"
417
+ stroke="currentColor"
418
+ strokeWidth="2"
419
+ strokeLinecap="round"
420
+ strokeLinejoin="round"
421
+ className="icon icon-tabler icons-tabler-outline icon-tabler-crystal-ball"
422
+ >
423
+ <path stroke="none" d="M0 0h24v24H0z" fill="none" />
424
+ <path d="M6.73 17.018a8 8 0 1 1 10.54 0" />
425
+ <path d="M5 19a2 2 0 0 0 2 2h10a2 2 0 1 0 0 -4h-10a2 2 0 0 0 -2 2z" />
426
+ <path d="M11 7a3 3 0 0 0 -3 3" />
427
+ </svg>
428
+ </div>
429
+ </div>
430
+
431
+ {/* Gender Selection - Toggle style on mobile, tabs on desktop */}
432
+ <button
433
+ onClick={() =>
434
+ setSelectedGender(selectedGender === "male" ? "female" : "male")
435
+ }
436
+ 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 ${
437
+ isDarkMode
438
+ ? "bg-white bg-opacity-10 text-white hover:bg-opacity-20"
439
+ : "bg-white bg-opacity-90 text-gray-700 hover:bg-opacity-100"
440
+ }`}
441
+ >
442
+ <i
443
+ className={
444
+ selectedGender === "male"
445
+ ? "ri-women-line text-xl"
446
+ : "ri-men-line text-xl"
447
+ }
448
+ ></i>
449
+ </button>
450
+
451
+ {/* Gender Selection - Desktop tabs */}
452
+ <div
453
+ className={`hidden sm:flex rounded-full p-1 shadow-lg ${
454
+ isDarkMode ? "bg-white bg-opacity-10" : "bg-white bg-opacity-90"
455
+ }`}
456
+ >
457
+ <button
458
+ onClick={() => setSelectedGender("female")}
459
+ className={`px-3 md:px-6 py-2 rounded-full font-medium transition-all ${
460
+ selectedGender === "female"
461
+ ? "bg-pink-400 text-white shadow-md"
462
+ : isDarkMode
463
+ ? "text-white hover:text-pink-300"
464
+ : "text-gray-600 hover:text-pink-400"
465
+ }`}
466
+ >
467
+ <span>Female</span>
468
+ </button>
469
+ <button
470
+ onClick={() => setSelectedGender("male")}
471
+ className={`px-3 md:px-6 py-2 rounded-full font-medium transition-all ${
472
+ selectedGender === "male"
473
+ ? "bg-blue-400 text-white shadow-md"
474
+ : isDarkMode
475
+ ? "text-white hover:text-blue-300"
476
+ : "text-gray-600 hover:text-blue-400"
477
+ }`}
478
+ >
479
+ <span>Male</span>
480
+ </button>
481
+ </div>
482
+ </div>
483
+
484
+ {/* App Title - Hidden on mobile */}
485
+ <div className="text-center mb-6 md:mb-10 hidden sm:block">
486
+ <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">
487
+ <svg
488
+ xmlns="http://www.w3.org/2000/svg"
489
+ width="24"
490
+ height="24"
491
+ viewBox="0 0 24 24"
492
+ fill="none"
493
+ stroke="currentColor"
494
+ strokeWidth="2"
495
+ strokeLinecap="round"
496
+ strokeLinejoin="round"
497
+ className="icon icon-tabler icons-tabler-outline icon-tabler-crystal-ball"
498
+ >
499
+ <path stroke="none" d="M0 0h24v24H0z" fill="none" />
500
+ <path d="M6.73 17.018a8 8 0 1 1 10.54 0" />
501
+ <path d="M5 19a2 2 0 0 0 2 2h10a2 2 0 1 0 0 -4h-10a2 2 0 0 0 -2 2z" />
502
+ <path d="M11 7a3 3 0 0 0 -3 3" />
503
+ </svg>
504
+ </div>
505
+ <h1 className="text-4xl font-bold mb-2">Horoscope App</h1>
506
+ </div>
507
+
508
+ {/* Zodiac Signs Grid - Single Row */}
509
+ <div
510
+ className="w-full overflow-x-auto mb-6 md:mb-10"
511
+ ref={zodiacScrollRef}
512
+ >
513
+ <div className="flex justify-start md:justify-center gap-2 md:gap-3 pb-4 px-4 min-w-max py-2">
514
+ {zodiacSigns.map((sign) => (
515
+ <div
516
+ key={sign.id}
517
+ data-sign={sign.id}
518
+ onClick={() => handleSignSelect(sign.id)}
519
+ className="flex flex-col items-center cursor-pointer transition-all duration-300 hover:scale-105 flex-shrink-0 py-1"
520
+ >
521
+ <div
522
+ className={`rounded-full p-0.5 transition-all duration-300 ${
523
+ selectedSign === sign.id
524
+ ? "w-24 h-24 md:w-28 md:h-28 shadow-lg"
525
+ : "w-20 h-20 md:w-24 md:h-24"
526
+ }`}
527
+ style={{
528
+ background:
529
+ selectedSign === sign.id
530
+ ? "linear-gradient(135deg, #1D3249 0%, #96D1E2 100%)"
531
+ : "linear-gradient(135deg, #07090A 0%, #979999 100%)",
532
+ }}
533
+ >
534
+ {/* Background layer for separation */}
535
+ <div
536
+ className={`w-full h-full rounded-full p-0.5 ${
537
+ isDarkMode ? "bg-gray-900" : "bg-gray-50"
538
+ }`}
539
+ >
540
+ <div
541
+ className={`w-full h-full rounded-full flex items-center justify-center text-xl overflow-hidden ${
542
+ isDarkMode ? "bg-gray-800" : "bg-gray-100"
543
+ }`}
544
+ >
545
+ {getImagePath(sign) ? (
546
+ <img
547
+ src={getImagePath(sign)}
548
+ alt={sign.name}
549
+ className="w-full h-full object-cover rounded-full"
550
+ onError={(e) => {
551
+ e.target.style.display = "none";
552
+ e.target.nextSibling.style.display = "block";
553
+ }}
554
+ />
555
+ ) : (
556
+ <ZodiacIcon
557
+ sign={sign.id}
558
+ size={selectedSign === sign.id ? 30 : 28}
559
+ className="text-gray-600"
560
+ />
561
+ )}
562
+ <span
563
+ style={{
564
+ display: getImagePath(sign) ? "none" : "none",
565
+ }}
566
+ >
567
+ {sign.emoji}
568
+ </span>
569
+ </div>
570
+ </div>
571
+ </div>
572
+ <p
573
+ className={`text-xs font-medium mt-3 text-center flex items-center justify-center gap-1 transition-all duration-300 ${
574
+ selectedSign === sign.id ? "font-bold" : ""
575
+ }`}
576
+ >
577
+ <ZodiacIcon
578
+ sign={sign.id}
579
+ size={selectedSign === sign.id ? 17 : 16}
580
+ className="text-current"
581
+ />
582
+ {sign.name}
583
+ </p>
584
+ </div>
585
+ ))}
586
+ </div>
587
+ </div>
588
+
589
+ {/* Period Selection */}
590
+ <div className="flex justify-center mb-6 md:mb-8">
591
+ <div
592
+ className={`flex rounded-full p-1 shadow-lg ${
593
+ isDarkMode ? "bg-white bg-opacity-10" : "bg-white bg-opacity-90"
594
+ }`}
595
+ >
596
+ {periods.map((period) => (
597
+ <button
598
+ key={period.id}
599
+ onClick={() => setSelectedPeriod(period.id)}
600
+ 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 ${
601
+ selectedPeriod === period.id
602
+ ? "bg-blue-400 text-white shadow-md"
603
+ : isDarkMode
604
+ ? "text-white hover:text-blue-300"
605
+ : "text-gray-600 hover:text-blue-400"
606
+ }`}
607
+ >
608
+ {period.name}
609
+ {period.id !== "today" && (
610
+ <i className="ri-lock-line text-xs opacity-70"></i>
611
+ )}
612
+ </button>
613
+ ))}
614
+ </div>
615
+ </div>
616
+
617
+ {/* Category Selection */}
618
+ <div className="flex justify-center flex-wrap gap-2 md:gap-3 mb-6 md:mb-10">
619
+ {categories.map((category) => (
620
+ <button
621
+ key={category.id}
622
+ onClick={() => setSelectedCategory(category.id)}
623
+ 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 ${
624
+ selectedCategory === category.id
625
+ ? "bg-purple-400 text-white shadow-lg"
626
+ : isDarkMode
627
+ ? "bg-white bg-opacity-10 text-white hover:bg-opacity-20"
628
+ : "bg-white bg-opacity-90 text-gray-600 hover:bg-opacity-100"
629
+ }`}
630
+ >
631
+ {category.icon}
632
+ {category.name}
633
+ </button>
634
+ ))}
635
+ </div>
636
+
637
+ {/* Horoscope Content */}
638
+ <div
639
+ className={`rounded-3xl p-8 mb-4 shadow-2xl backdrop-blur-lg transition-all ${
640
+ isDarkMode
641
+ ? "bg-white bg-opacity-10 border border-white border-opacity-20"
642
+ : "bg-white bg-opacity-95"
643
+ }`}
644
+ >
645
+ <div className="flex items-center gap-6 mb-8">
646
+ <div
647
+ className="w-20 h-20 rounded-full p-0.5 shadow-lg"
648
+ style={{
649
+ background: "linear-gradient(135deg, #1D3249 0%, #96D1E2 100%)",
650
+ }}
651
+ >
652
+ {/* Background layer for separation */}
653
+ <div
654
+ className={`w-full h-full rounded-full p-0.5 ${
655
+ isDarkMode ? "bg-gray-900" : "bg-gray-50"
656
+ }`}
657
+ >
658
+ <div
659
+ className={`w-full h-full rounded-full flex items-center justify-center text-2xl overflow-hidden ${
660
+ isDarkMode ? "bg-gray-800" : "bg-gray-100"
661
+ }`}
662
+ >
663
+ {getImagePath(getCurrentSign()) ? (
664
+ <img
665
+ src={getImagePath(getCurrentSign())}
666
+ alt={getCurrentSign()?.name}
667
+ className="w-full h-full object-cover rounded-full"
668
+ onError={(e) => {
669
+ e.target.style.display = "none";
670
+ e.target.nextSibling.style.display = "block";
671
+ }}
672
+ />
673
+ ) : (
674
+ <ZodiacIcon
675
+ sign={getCurrentSign()?.id}
676
+ size={32}
677
+ className="text-gray-600"
678
+ />
679
+ )}
680
+ <span
681
+ style={{
682
+ display: getImagePath(getCurrentSign()) ? "none" : "none",
683
+ }}
684
+ >
685
+ {getCurrentSign()?.emoji}
686
+ </span>
687
+ </div>
688
+ </div>
689
+ </div>
690
+ <div className="flex-1">
691
+ <h2 className="text-3xl font-bold text-blue-400 flex items-center gap-2 mb-2">
692
+ {getPeriodTitle()}
693
+ <svg
694
+ xmlns="http://www.w3.org/2000/svg"
695
+ width="24"
696
+ height="24"
697
+ viewBox="0 0 24 24"
698
+ fill="none"
699
+ stroke="currentColor"
700
+ strokeWidth="2"
701
+ strokeLinecap="round"
702
+ strokeLinejoin="round"
703
+ className="icon icon-tabler icons-tabler-outline icon-tabler-trending-up"
704
+ >
705
+ <path stroke="none" d="M0 0h24v24H0z" fill="none" />
706
+ <path d="M3 17l6 -6l4 4l8 -8" />
707
+ <path d="M14 7l7 0l0 7" />
708
+ </svg>
709
+ </h2>
710
+ <div className="flex items-center gap-2">
711
+ <ZodiacIcon
712
+ sign={getCurrentSign()?.id}
713
+ size={20}
714
+ className="text-current"
715
+ />
716
+ <span className="font-semibold">
717
+ {getCurrentSign()?.name} ({getCurrentSign()?.dates})
718
+ </span>
719
+ </div>
720
+ </div>
721
+ </div>
722
+ <div className="text-lg leading-relaxed mb-8">
723
+ {isLoading ? (
724
+ <div className="flex items-center justify-center py-4">
725
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-400"></div>
726
+ <span className="ml-2">Loading your horoscope...</span>
727
+ </div>
728
+ ) : (
729
+ <p>{getHoroscopeText()}</p>
730
+ )}
731
+
732
+ {error && (
733
+ <div className="text-sm opacity-70 mt-2">
734
+ <i className="ri-information-line mr-1"></i>
735
+ Using cached content
736
+ </div>
737
+ )}
738
+ </div>{" "}
739
+ </div>
740
+
741
+ {/* Footer */}
742
+ <div className="text-center text-sm opacity-70 mt-4 space-y-2">
743
+ <p className="flex items-center justify-center gap-2">
744
+ <i className="ri-sparkling-line"></i>
745
+ Discover your daily horoscope and unlock the secrets of the stars
746
+ <i className="ri-sparkling-line"></i>
747
+ </p>
748
+ <p className="text-xs opacity-60">
749
+ © 2025 AstroLens Horoscope - All rights reserved.
750
+ </p>
751
+ </div>
752
+ </div>
753
+ </div>
754
+ );
755
+ };
756
+
757
+ export default HoroscopeApp;
src/services/horoscopeService.js ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const GITHUB_BASE_URL =
2
+ "https://raw.githubusercontent.com/ntphuc149/AstroLens-Horoscope-Data-Storage/main";
3
+
4
+ const getCurrentDate = () => {
5
+ const today = new Date();
6
+ return today.toISOString().split("T")[0];
7
+ };
8
+
9
+ // Function để decode base64 UTF-8 đúng cách
10
+ const decodeBase64UTF8 = (base64String) => {
11
+ try {
12
+ // Decode base64 thành binary string
13
+ const binaryString = atob(base64String);
14
+
15
+ // Convert binary string thành Uint8Array
16
+ const bytes = new Uint8Array(binaryString.length);
17
+ for (let i = 0; i < binaryString.length; i++) {
18
+ bytes[i] = binaryString.charCodeAt(i);
19
+ }
20
+
21
+ // Decode UTF-8 đúng cách
22
+ const decoder = new TextDecoder("utf-8");
23
+ return decoder.decode(bytes);
24
+ } catch (error) {
25
+ console.error("Error decoding base64 UTF-8:", error);
26
+ return null;
27
+ }
28
+ };
29
+
30
+ // Fetch data horoscope từ GitHub với authentication
31
+ export const fetchHoroscopeData = async (category, period) => {
32
+ try {
33
+ const currentDate = getCurrentDate();
34
+ const fileName = `${category}_${period}.json`;
35
+ const url = `https://api.github.com/repos/ntphuc149/AstroLens-Horoscope-Data-Storage/contents/data/${currentDate}/${fileName}`;
36
+
37
+ // Chuẩn bị headers với token
38
+ const headers = {
39
+ Accept: "application/vnd.github.v3+json",
40
+ };
41
+
42
+ // Thêm Authorization header nếu có token
43
+ if (process.env.REACT_APP_GITHUB_TOKEN) {
44
+ headers["Authorization"] = `token ${process.env.REACT_APP_GITHUB_TOKEN}`;
45
+ }
46
+
47
+ console.log("Fetching from URL:", url);
48
+ console.log(
49
+ "Using token:",
50
+ process.env.REACT_APP_GITHUB_TOKEN ? "Yes" : "No"
51
+ );
52
+
53
+ const response = await fetch(url, { headers });
54
+
55
+ console.log("Response status:", response.status);
56
+
57
+ if (!response.ok) {
58
+ throw new Error(
59
+ `GitHub API error: ${response.status} - ${response.statusText}`
60
+ );
61
+ }
62
+
63
+ const githubResponse = await response.json();
64
+
65
+ // GitHub API trả về content dạng base64, cần decode UTF-8 đúng cách
66
+ if (githubResponse.content) {
67
+ // Loại bỏ line breaks trong base64 content
68
+ const cleanBase64 = githubResponse.content.replace(/\n/g, "");
69
+
70
+ // Decode base64 với UTF-8 support
71
+ const decodedContent = decodeBase64UTF8(cleanBase64);
72
+
73
+ if (!decodedContent) {
74
+ throw new Error("Failed to decode base64 content");
75
+ }
76
+
77
+ const data = JSON.parse(decodedContent);
78
+ console.log("Successfully loaded data:", data);
79
+ return data;
80
+ } else {
81
+ throw new Error("No content in GitHub response");
82
+ }
83
+ } catch (error) {
84
+ console.error("Error fetching horoscope data:", error);
85
+ return null;
86
+ }
87
+ };
88
+
89
+ export const fallbackHoroscopeContent = {
90
+ overall: {
91
+ today:
92
+ "Today brings a harmonious blend of opportunities and challenges across all aspects of your life. Your natural instincts will guide you well.",
93
+ week: "This week promises balanced growth in multiple areas. Trust your intuition as you navigate new experiences.",
94
+ month:
95
+ "A month of significant personal development awaits. Embrace change and new perspectives with confidence.",
96
+ year: "This year marks a transformative period of growth, bringing profound insights and meaningful achievements.",
97
+ },
98
+ love: {
99
+ today:
100
+ "Love is in the air today! Your romantic side is highlighted, making it a perfect day for expressing your feelings.",
101
+ week: "This week brings romantic opportunities and deepening connections with your loved ones.",
102
+ month:
103
+ "A month of passionate encounters and meaningful relationships awaits you.",
104
+ year: "This year will be transformative for your love life, bringing lasting happiness.",
105
+ },
106
+ health: {
107
+ today:
108
+ "Your energy levels are high today. Focus on maintaining balance between work and rest.",
109
+ week: "Pay attention to your physical wellbeing this week. Small changes can make big differences.",
110
+ month:
111
+ "This month emphasizes the importance of mental and physical health harmony.",
112
+ year: "A year of wellness and vitality lies ahead. Make health your priority.",
113
+ },
114
+ relationship: {
115
+ today:
116
+ "Relationships flourish under today's cosmic energy. Communication is key to success.",
117
+ week: "Strengthen your bonds with family and friends through meaningful conversations.",
118
+ month:
119
+ "Social connections expand this month, bringing new friendships and opportunities.",
120
+ year: "Your social circle will grow significantly, enriching your life in unexpected ways.",
121
+ },
122
+ finance: {
123
+ today:
124
+ "Financial opportunities present themselves today. Stay alert for new possibilities.",
125
+ week: "This week favors careful financial planning and smart investments.",
126
+ month:
127
+ "Money matters require attention this month. Budget wisely for future security.",
128
+ year: "Financial growth and stability are highlighted throughout this year.",
129
+ },
130
+ fortune: {
131
+ today:
132
+ "Lucky energies surround you today. Trust your instincts and take calculated risks for unexpected rewards.",
133
+ week: "Fortune favors the bold this week. Positive surprises and serendipitous encounters await.",
134
+ month:
135
+ "A month of abundance and good fortune unfolds. Opportunities align perfectly with your goals.",
136
+ year: "This year brings remarkable luck and prosperity. Your efforts will be rewarded beyond expectations.",
137
+ },
138
+ };