1- import React , { useEffect , useMemo , useCallback } from "react" ;
1+ import React , { useMemo , useCallback } from "react" ;
22import { View , Text , FlatList , StyleSheet } from "react-native" ;
33import Animated , { FadeInRight , FadeInLeft } from "react-native-reanimated" ;
44import { useIsFocused , useFocusEffect } from "@react-navigation/native" ;
55import { useCachedWindowDimensions } from "../hooks/useCachedDimensions" ;
66import {
77 useAchievement ,
88 useAchievementsLastViewed ,
9+ useIsAchievementUnseen ,
910} from "../stores/useAchievementsStore" ;
1011import { useImages } from "../hooks/useImages" ;
1112import achievementJson from "../configs/achievements.json" ;
@@ -35,7 +36,6 @@ const ICON_CONTAINER_BOTTOM = 38;
3536const ICON_CONTAINER_LEFT = 33 ;
3637const ICON_SIZE = 50 ;
3738const TILE_HORIZONTAL_MARGIN = 4 ;
38- const HEADER_TO_ROW_SPACING = 8 ;
3939const GRADIENT_FRACTION = 0.14 ; // of screen width, clamped below
4040
4141const styles = StyleSheet . create ( {
@@ -144,6 +144,133 @@ const styles = StyleSheet.create({
144144 } ,
145145} ) ;
146146
147+ const AchievementItem : React . FC < {
148+ achievement : any ;
149+ categoryId : string ;
150+ achievementIndex : number ;
151+ progress : number ;
152+ getImage : ( imageName : string ) => any ;
153+ renderAchievementName : ( name : string ) => React . ReactNode ;
154+ } > = ( {
155+ achievement,
156+ categoryId,
157+ achievementIndex,
158+ progress,
159+ getImage,
160+ renderAchievementName,
161+ } ) => {
162+ const isUnseen = useIsAchievementUnseen ( achievement . id ) ;
163+
164+ return (
165+ < Animated . View
166+ style = { styles . achievementItem }
167+ key = { achievementIndex }
168+ entering = { FadeInRight }
169+ >
170+ < Canvas
171+ key = { `achievement-bg-${ categoryId } -${ achievement . id } ` }
172+ style = { styles . fillCanvas }
173+ >
174+ < Image
175+ image = { getImage ( "achievements.tile.locked" ) }
176+ fit = "fill"
177+ x = { 0 }
178+ y = { 0 }
179+ width = { ACHIEVEMENT_TILE_WIDTH }
180+ height = { ACHIEVEMENT_TILE_HEIGHT }
181+ />
182+ </ Canvas >
183+ { progress > 0 && (
184+ < View
185+ style = { [
186+ styles . progressOverlayBase ,
187+ {
188+ backgroundColor : "#6dcd64" ,
189+ width : `${ progress } %` ,
190+ } ,
191+ ] }
192+ />
193+ ) }
194+ { progress > 0 && (
195+ < View style = { styles . progressOverlayImageWrapper } >
196+ < Canvas
197+ key = { `achievement-overlay-${ categoryId } -${ achievement . id } -${ progress } ` }
198+ style = { styles . fillCanvas }
199+ >
200+ < Image
201+ image = { getImage (
202+ progress === 100
203+ ? "achievements.tile.achieved"
204+ : "achievements.tile.overlay" ,
205+ ) }
206+ fit = "fill"
207+ x = { 0 }
208+ y = { 0 }
209+ width = { ACHIEVEMENT_TILE_WIDTH }
210+ height = { ACHIEVEMENT_TILE_HEIGHT }
211+ />
212+ </ Canvas >
213+ </ View >
214+ ) }
215+ { progress === 100 && isUnseen && (
216+ < View
217+ style = { {
218+ position : "absolute" ,
219+ bottom : 8 ,
220+ left : 0 ,
221+ right : 0 ,
222+ justifyContent : "center" ,
223+ alignItems : "center" ,
224+ zIndex : 10 ,
225+ } }
226+ >
227+ < View
228+ style = { {
229+ backgroundColor : "#7c3aed" ,
230+ paddingHorizontal : 12 ,
231+ paddingVertical : 3 ,
232+ borderRadius : 6 ,
233+ } }
234+ >
235+ < Text
236+ style = { {
237+ color : "#fff7ff" ,
238+ fontSize : 15 ,
239+ fontFamily : PIXEL_FONT ,
240+ fontWeight : "bold" ,
241+ } }
242+ >
243+ NEW!
244+ </ Text >
245+ </ View >
246+ </ View >
247+ ) }
248+ < View style = { styles . achievementNameContainer } >
249+ { renderAchievementName ( achievement . name ) }
250+ </ View >
251+ < View style = { styles . achievementIconContainer } >
252+ < Canvas
253+ key = { `achievement-icon-${ categoryId } -${ achievement . id } ` }
254+ style = { styles . fillCanvas }
255+ >
256+ < Image
257+ image = { getImage ( achievement . image ) }
258+ fit = "contain"
259+ x = { 0 }
260+ y = { 0 }
261+ width = { ICON_SIZE }
262+ height = { ICON_SIZE }
263+ sampling = { {
264+ filter : FilterMode . Nearest ,
265+ mipmap : MipmapMode . Nearest ,
266+ } }
267+ />
268+ </ Canvas >
269+ </ View >
270+ </ Animated . View >
271+ ) ;
272+ } ;
273+
147274export const AchievementsPage : React . FC = ( ) => {
148275 const isFocused = useIsFocused ( ) ;
149276 const { achievementsProgress } = useAchievement ( ) ;
@@ -153,7 +280,10 @@ export const AchievementsPage: React.FC = () => {
153280
154281 useFocusEffect (
155282 useCallback ( ( ) => {
156- setAchievementsLastViewedNow ( ) ;
283+ // Mark achievements as viewed when the user leaves the page
284+ return ( ) => {
285+ setAchievementsLastViewedNow ( ) ;
286+ } ;
157287 } , [ setAchievementsLastViewedNow ] ) ,
158288 ) ;
159289
@@ -276,79 +406,14 @@ export const AchievementsPage: React.FC = () => {
276406 maxToRenderPerBatch = { 6 }
277407 windowSize = { 10 }
278408 renderItem = { ( { item : achievement , index : achievementIndex } ) => (
279- < Animated . View
280- style = { styles . achievementItem }
281- key = { achievementIndex }
282- entering = { FadeInRight }
283- >
284- < Canvas
285- key = { `achievement-bg-${ item . id } -${ achievement . id } ` }
286- style = { styles . fillCanvas }
287- >
288- < Image
289- image = { getImage ( "achievements.tile.locked" ) }
290- fit = "fill"
291- x = { 0 }
292- y = { 0 }
293- width = { ACHIEVEMENT_TILE_WIDTH }
294- height = { ACHIEVEMENT_TILE_HEIGHT }
295- />
296- </ Canvas >
297- { achievementsProgress [ achievement . id ] > 0 && (
298- < View
299- style = { [
300- styles . progressOverlayBase ,
301- {
302- backgroundColor : "#6dcd64" ,
303- width : `${ achievementsProgress [ achievement . id ] } %` ,
304- } ,
305- ] }
306- />
307- ) }
308- { achievementsProgress [ achievement . id ] > 0 && (
309- < View style = { styles . progressOverlayImageWrapper } >
310- < Canvas
311- key = { `achievement-overlay-${ item . id } -${ achievement . id } -${ achievementsProgress [ achievement . id ] } ` }
312- style = { styles . fillCanvas }
313- >
314- < Image
315- image = { getImage (
316- achievementsProgress [ achievement . id ] === 100
317- ? "achievements.tile.achieved"
318- : "achievements.tile.overlay" ,
319- ) }
320- fit = "fill"
321- x = { 0 }
322- y = { 0 }
323- width = { ACHIEVEMENT_TILE_WIDTH }
324- height = { ACHIEVEMENT_TILE_HEIGHT }
325- />
326- </ Canvas >
327- </ View >
328- ) }
329- < View style = { styles . achievementNameContainer } >
330- { renderAchievementName ( achievement . name ) }
331- </ View >
332- < View style = { styles . achievementIconContainer } >
333- < Canvas
334- key = { `achievement-icon-${ item . id } -${ achievement . id } ` }
335- style = { styles . fillCanvas }
336- >
337- < Image
338- image = { getImage ( achievement . image ) }
339- fit = "contain"
340- x = { 0 }
341- y = { 0 }
342- width = { ICON_SIZE }
343- height = { ICON_SIZE }
344- sampling = { {
345- filter : FilterMode . Nearest ,
346- mipmap : MipmapMode . Nearest ,
347- } }
348- />
349- </ Canvas >
350- </ View >
351- </ Animated . View >
409+ < AchievementItem
410+ achievement = { achievement }
411+ categoryId = { item . id }
412+ achievementIndex = { achievementIndex }
413+ progress = { achievementsProgress [ achievement . id ] || 0 }
414+ getImage = { getImage }
415+ renderAchievementName = { renderAchievementName }
416+ />
352417 ) }
353418 />
354419 </ View >
0 commit comments