Skip to content

Commit 2b6458d

Browse files
committed
feat(terracotta): add TerracottaAchievementsPage
1 parent 23cb5b5 commit 2b6458d

1 file changed

Lines changed: 253 additions & 0 deletions

File tree

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
import React, { useState } from 'react';
2+
import { Link } from 'react-router-dom';
3+
import {
4+
ArrowLeftIcon,
5+
TrophyIcon,
6+
LockIcon,
7+
InfoIcon,
8+
BellSlashIcon,
9+
FunnelIcon,
10+
XCircleIcon,
11+
CalendarBlankIcon,
12+
} from '@phosphor-icons/react';
13+
import Seo from '../../components/Seo';
14+
import { useAchievements } from '../../context/AchievementContext';
15+
import { ACHIEVEMENTS } from '../../config/achievements';
16+
17+
const TerracottaAchievementsPage = () => {
18+
const { unlockedAchievements, showAchievementToast } = useAchievements();
19+
const [selectedCategories, setSelectedCategories] = useState([]);
20+
21+
const uniqueCategories = ['All', ...new Set(ACHIEVEMENTS.map((ach) => ach.category))].sort();
22+
23+
const unlockedCount = Object.keys(unlockedAchievements).filter(
24+
(key) => unlockedAchievements[key].unlocked,
25+
).length;
26+
const totalCount = ACHIEVEMENTS.length;
27+
const progressPercentage = Math.round((unlockedCount / totalCount) * 100);
28+
29+
const toggleCategory = (category) => {
30+
if (category === 'All') {
31+
setSelectedCategories([]);
32+
} else {
33+
setSelectedCategories((prev) =>
34+
prev.includes(category) ? prev.filter((c) => c !== category) : [...prev, category],
35+
);
36+
}
37+
};
38+
39+
const clearFilters = () => setSelectedCategories([]);
40+
41+
const filteredAchievements = ACHIEVEMENTS.filter((achievement) => {
42+
const matchesCategory =
43+
selectedCategories.length === 0 || selectedCategories.includes(achievement.category);
44+
return matchesCategory;
45+
});
46+
47+
return (
48+
<div className="py-16 sm:py-24 bg-[#F3ECE0] min-h-screen">
49+
<Seo title="Achievements | Fezcodex" description="Track your progress and unlocked secrets." />
50+
51+
<div className="mx-auto max-w-7xl px-6 lg:px-8">
52+
<Link
53+
to="/"
54+
className="group text-[#9E4A2F] hover:text-[#C96442] flex items-center justify-center gap-2 text-lg mb-4 transition-all font-playfairDisplay italic"
55+
>
56+
<ArrowLeftIcon className="text-xl transition-transform group-hover:-translate-x-1" /> Back to Home
57+
</Link>
58+
59+
<div className="mx-auto max-w-2xl text-center relative z-10">
60+
<h1 className="text-4xl font-playfairDisplay italic tracking-tight text-[#1A1613] sm:text-6xl flex flex-col sm:flex-row items-center justify-center gap-4">
61+
<div className="relative">
62+
<div className="absolute inset-0 bg-[#C96442] blur-2xl opacity-20 rounded-full"></div>
63+
<TrophyIcon
64+
size={56}
65+
weight="duotone"
66+
className="text-[#C96442] relative z-10 drop-shadow-[0_0_15px_rgba(201,100,66,0.4)]"
67+
/>
68+
</div>
69+
Achievements
70+
<TrophyIcon
71+
size={56}
72+
weight="duotone"
73+
className="text-[#C96442] relative z-10 drop-shadow-[0_0_15px_rgba(201,100,66,0.4)]"
74+
/>
75+
</h1>
76+
<p className="mt-6 text-lg leading-8 text-[#2E2620]">
77+
Discover the secrets hidden within Fezcodex.
78+
</p>
79+
80+
<div className="flex flex-wrap items-center justify-center gap-2 mt-8 max-w-2xl mx-auto font-mono">
81+
<div className="flex items-center gap-2 mr-2 text-[#2E2620]/50 text-sm">
82+
<FunnelIcon size={16} />
83+
<span>Filter:</span>
84+
</div>
85+
{uniqueCategories.map((category) => {
86+
const isSelected =
87+
selectedCategories.includes(category) ||
88+
(category === 'All' && selectedCategories.length === 0);
89+
const colorClass = isSelected
90+
? 'bg-[#C96442]/15 text-[#9E4A2F] border-[#C96442]/50 shadow-[0_0_10px_rgba(201,100,66,0.15)]'
91+
: 'bg-[#E8DECE]/60 text-[#2E2620]/60 border-[#1A161320] hover:border-[#1A1613] hover:text-[#1A1613]';
92+
return (
93+
<button
94+
key={category}
95+
onClick={() => toggleCategory(category)}
96+
className={`px-3 py-1 rounded-full text-sm font-medium border transition-colors duration-200 ${colorClass}`}
97+
>
98+
{category}
99+
</button>
100+
);
101+
})}
102+
103+
{selectedCategories.length > 0 && (
104+
<button
105+
onClick={clearFilters}
106+
className="ml-2 text-sm text-[#9E4A2F] hover:text-[#C96442] flex items-center gap-1 transition-colors"
107+
>
108+
<XCircleIcon size={20} /> Clear
109+
</button>
110+
)}
111+
</div>
112+
113+
<div className="mt-8 max-w-md mx-auto">
114+
<div className="flex justify-between text-sm text-[#2E2620] mb-2">
115+
<span>Progress</span>
116+
<span>
117+
{unlockedCount} / {totalCount}
118+
</span>
119+
</div>
120+
<div className="w-full bg-[#E8DECE]/80 rounded-full h-4 overflow-hidden border border-[#1A161320] shadow-inner">
121+
<div
122+
className="bg-gradient-to-r from-[#9E4A2F] via-[#C96442] to-[#B88532] h-4 rounded-full transition-all duration-1000 ease-out shadow-[0_0_10px_rgba(201,100,66,0.4)]"
123+
style={{ width: `${progressPercentage}%` }}
124+
></div>
125+
</div>
126+
127+
<div
128+
className={`mt-8 flex items-center gap-4 p-4 rounded-sm border backdrop-blur-sm transition-all duration-300 ${
129+
showAchievementToast
130+
? 'bg-[#C96442]/10 border-[#C96442]/40 text-[#9E4A2F]'
131+
: 'bg-[#E8DECE]/40 border-[#1A161320] text-[#2E2620]'
132+
}`}
133+
>
134+
<div
135+
className={`p-2.5 rounded-full shrink-0 ${
136+
showAchievementToast ? 'bg-[#C96442]/20 text-[#C96442]' : 'bg-[#1A161320] text-[#2E2620]/60'
137+
}`}
138+
>
139+
{showAchievementToast ? <InfoIcon size={24} weight="duotone" /> : <BellSlashIcon size={24} weight="duotone" />}
140+
</div>
141+
<div className="flex-1 text-left">
142+
<p className="font-medium text-sm tracking-wide">
143+
NOTIFICATIONS:{' '}
144+
<span className="font-bold">{showAchievementToast ? 'ACTIVE' : 'MUTED'}</span>
145+
</p>
146+
<p className={`text-xs mt-1 ${showAchievementToast ? 'text-[#9E4A2F]/70' : 'text-[#2E2620]/60'}`}>
147+
Manage in{' '}
148+
<Link to="/settings" className="underline underline-offset-2 hover:text-[#1A1613] transition-colors">
149+
Settings
150+
</Link>
151+
.
152+
</p>
153+
</div>
154+
</div>
155+
</div>
156+
</div>
157+
158+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 mt-16">
159+
{filteredAchievements.map((achievement) => {
160+
const isUnlocked = unlockedAchievements[achievement.id]?.unlocked;
161+
const unlockedDate = isUnlocked ? new Date(unlockedAchievements[achievement.id].unlockedAt) : null;
162+
163+
return (
164+
<div
165+
key={achievement.id}
166+
className={`relative group flex flex-col h-full overflow-hidden rounded-sm border transition-all duration-500 ease-out ${
167+
isUnlocked
168+
? 'border-[#C96442]/40 hover:-translate-y-2 hover:shadow-[0_15px_40px_-10px_rgba(201,100,66,0.35)]'
169+
: 'border-[#1A161320] bg-[#E8DECE]/30 grayscale opacity-70 hover:opacity-100 hover:border-[#1A1613]'
170+
}`}
171+
>
172+
<div
173+
className={`absolute inset-0 z-0 transition-all duration-500 ${
174+
isUnlocked
175+
? 'bg-gradient-to-br from-[#C96442]/15 via-[#F3ECE0] to-[#F3ECE0] opacity-100'
176+
: 'bg-[#F3ECE0]'
177+
}`}
178+
/>
179+
180+
<div className="relative z-10 flex flex-col items-center text-center p-6 flex-grow">
181+
<span
182+
className={`mb-6 px-3 py-1 text-[10px] font-bold tracking-widest uppercase rounded-full border ${
183+
isUnlocked
184+
? 'bg-[#C96442]/10 text-[#9E4A2F] border-[#C96442]/40'
185+
: 'bg-[#E8DECE] text-[#2E2620]/50 border-[#1A161320]'
186+
}`}
187+
>
188+
{achievement.category}
189+
</span>
190+
191+
<div className="relative mb-6 group-hover:scale-105 transition-transform duration-300">
192+
{isUnlocked && (
193+
<div className="absolute inset-0 bg-[#C96442] blur-2xl opacity-20 rounded-full animate-pulse-slow"></div>
194+
)}
195+
196+
<div
197+
className={`relative h-24 w-24 rounded-full flex items-center justify-center border-[3px] shadow-2xl ${
198+
isUnlocked
199+
? 'bg-gradient-to-b from-[#F3ECE0] to-[#E8DECE] border-[#C96442]/60 text-[#9E4A2F] ring-4 ring-[#C96442]/10'
200+
: 'bg-[#E8DECE]/60 border-[#1A161320] text-[#2E2620]/40'
201+
}`}
202+
>
203+
<div className="scale-[1.4] drop-shadow-lg">
204+
{isUnlocked ? achievement.icon : <LockIcon weight="fill" />}
205+
</div>
206+
</div>
207+
</div>
208+
209+
<h3
210+
className={`text-2xl font-playfairDisplay italic tracking-tight mb-3 ${
211+
isUnlocked ? 'text-[#1A1613]' : 'text-[#2E2620]/40'
212+
}`}
213+
>
214+
{achievement.title}
215+
</h3>
216+
<p className={`text-sm leading-relaxed ${isUnlocked ? 'text-[#2E2620]' : 'text-[#2E2620]/40'}`}>
217+
{achievement.description}
218+
</p>
219+
</div>
220+
221+
<div
222+
className={`relative z-10 mt-auto p-4 w-full border-t ${
223+
isUnlocked ? 'border-[#C96442]/20 bg-[#C96442]/5' : 'border-[#1A161320] bg-[#E8DECE]/20'
224+
}`}
225+
>
226+
{isUnlocked ? (
227+
<div className="flex items-center justify-center gap-2 text-xs text-[#9E4A2F]/80 font-medium font-mono uppercase tracking-widest">
228+
<CalendarBlankIcon weight="duotone" size={16} />
229+
<span>
230+
Unlocked:{' '}
231+
{unlockedDate.toLocaleDateString(undefined, {
232+
year: 'numeric',
233+
month: 'short',
234+
day: 'numeric',
235+
})}
236+
</span>
237+
</div>
238+
) : (
239+
<div className="text-center text-xs text-[#2E2620]/40 font-mono uppercase tracking-widest flex items-center justify-center gap-2">
240+
<LockIcon size={14} /> Locked
241+
</div>
242+
)}
243+
</div>
244+
</div>
245+
);
246+
})}
247+
</div>
248+
</div>
249+
</div>
250+
);
251+
};
252+
253+
export default TerracottaAchievementsPage;

0 commit comments

Comments
 (0)