11import * as React from 'react'
2- import { useQuery } from '@tanstack/react-query'
2+ import { useMutation , useQuery , useQueryClient } from '@tanstack/react-query'
33import { Link , useNavigate , useSearch } from '@tanstack/react-router'
4- import { getApprovedShowcasesQueryOptions } from '~/queries/showcases'
4+ import {
5+ getApprovedShowcasesQueryOptions ,
6+ getMyShowcaseVotesQueryOptions ,
7+ } from '~/queries/showcases'
8+ import { voteShowcase } from '~/utils/showcase.functions'
59import { ShowcaseCard , ShowcaseCardSkeleton } from './ShowcaseCard'
610import { SubmitShowcasePlaceholder } from './ShowcaseSection'
711import { PaginationControls } from './PaginationControls'
@@ -10,10 +14,13 @@ import { SHOWCASE_USE_CASES, type ShowcaseUseCase } from '~/db/types'
1014import { Plus } from 'lucide-react'
1115import { USE_CASE_LABELS } from '~/utils/showcase.client'
1216import { Button } from './Button'
17+ import { useCurrentUser } from '~/hooks/useCurrentUser'
1318
1419export function ShowcaseGallery ( ) {
1520 const navigate = useNavigate ( { from : '/showcase/' } )
1621 const search = useSearch ( { from : '/showcase/' } )
22+ const queryClient = useQueryClient ( )
23+ const currentUser = useCurrentUser ( )
1724
1825 const { data, isLoading } = useQuery (
1926 getApprovedShowcasesQueryOptions ( {
@@ -28,6 +35,150 @@ export function ShowcaseGallery() {
2835 } ) ,
2936 )
3037
38+ const showcaseIds = React . useMemo (
39+ ( ) => data ?. showcases . map ( ( s ) => s . showcase . id ) ?? [ ] ,
40+ [ data ?. showcases ] ,
41+ )
42+
43+ const { data : votesData } = useQuery ( {
44+ ...getMyShowcaseVotesQueryOptions ( showcaseIds ) ,
45+ enabled : ! ! currentUser && showcaseIds . length > 0 ,
46+ } )
47+
48+ const votesMap = React . useMemo ( ( ) => {
49+ const map = new Map < string , 1 | - 1 > ( )
50+ votesData ?. votes . forEach ( ( v ) => {
51+ if ( v . value === 1 || v . value === - 1 ) {
52+ map . set ( v . showcaseId , v . value )
53+ }
54+ } )
55+ return map
56+ } , [ votesData ] )
57+
58+ const voteMutation = useMutation ( {
59+ mutationFn : ( params : { showcaseId : string ; value : 1 | - 1 } ) =>
60+ voteShowcase ( { data : params } ) ,
61+ onMutate : async ( { showcaseId, value } ) => {
62+ // Cancel outgoing refetches
63+ await queryClient . cancelQueries ( { queryKey : [ 'showcases' ] } )
64+
65+ // Snapshot previous values
66+ const previousShowcases = queryClient . getQueryData (
67+ getApprovedShowcasesQueryOptions ( {
68+ pagination : { page : search . page , pageSize : 24 } ,
69+ filters : {
70+ libraryId : search . libraryId ,
71+ useCases : search . useCases as ShowcaseUseCase [ ] ,
72+ } ,
73+ } ) . queryKey ,
74+ )
75+ const previousVotes = queryClient . getQueryData (
76+ getMyShowcaseVotesQueryOptions ( showcaseIds ) . queryKey ,
77+ )
78+
79+ // Optimistically update votes
80+ queryClient . setQueryData (
81+ getMyShowcaseVotesQueryOptions ( showcaseIds ) . queryKey ,
82+ ( old : typeof votesData ) => {
83+ if ( ! old ) return { votes : [ { showcaseId, value } ] }
84+ const existingVote = old . votes . find (
85+ ( v ) => v . showcaseId === showcaseId ,
86+ )
87+ if ( existingVote ) {
88+ if ( existingVote . value === value ) {
89+ // Toggle off
90+ return {
91+ votes : old . votes . filter ( ( v ) => v . showcaseId !== showcaseId ) ,
92+ }
93+ } else {
94+ // Change vote
95+ return {
96+ votes : old . votes . map ( ( v ) =>
97+ v . showcaseId === showcaseId ? { ...v , value } : v ,
98+ ) ,
99+ }
100+ }
101+ } else {
102+ // New vote
103+ return { votes : [ ...old . votes , { showcaseId, value } ] }
104+ }
105+ } ,
106+ )
107+
108+ // Optimistically update showcase score
109+ queryClient . setQueryData (
110+ getApprovedShowcasesQueryOptions ( {
111+ pagination : { page : search . page , pageSize : 24 } ,
112+ filters : {
113+ libraryId : search . libraryId ,
114+ useCases : search . useCases as ShowcaseUseCase [ ] ,
115+ } ,
116+ } ) . queryKey ,
117+ ( old : typeof data ) => {
118+ if ( ! old ) return old
119+ const currentVote = votesMap . get ( showcaseId )
120+ return {
121+ ...old ,
122+ showcases : old . showcases . map ( ( s ) => {
123+ if ( s . showcase . id !== showcaseId ) return s
124+ let scoreDelta : number = value
125+ if ( currentVote === value ) {
126+ // Toggling off
127+ scoreDelta = - value
128+ } else if ( currentVote ) {
129+ // Changing vote
130+ scoreDelta = value * 2
131+ }
132+ return {
133+ ...s ,
134+ showcase : {
135+ ...s . showcase ,
136+ voteScore : s . showcase . voteScore + scoreDelta ,
137+ } ,
138+ }
139+ } ) ,
140+ }
141+ } ,
142+ )
143+
144+ return { previousShowcases, previousVotes }
145+ } ,
146+ onError : ( _err , _vars , context ) => {
147+ // Rollback on error
148+ if ( context ?. previousShowcases ) {
149+ queryClient . setQueryData (
150+ getApprovedShowcasesQueryOptions ( {
151+ pagination : { page : search . page , pageSize : 24 } ,
152+ filters : {
153+ libraryId : search . libraryId ,
154+ useCases : search . useCases as ShowcaseUseCase [ ] ,
155+ } ,
156+ } ) . queryKey ,
157+ context . previousShowcases ,
158+ )
159+ }
160+ if ( context ?. previousVotes ) {
161+ queryClient . setQueryData (
162+ getMyShowcaseVotesQueryOptions ( showcaseIds ) . queryKey ,
163+ context . previousVotes ,
164+ )
165+ }
166+ } ,
167+ onSettled : ( ) => {
168+ // Refetch to sync with server
169+ queryClient . invalidateQueries ( { queryKey : [ 'showcases' ] } )
170+ } ,
171+ } )
172+
173+ const handleVote = ( showcaseId : string , value : 1 | - 1 ) => {
174+ if ( ! currentUser ) {
175+ // Redirect to login
176+ navigate ( { to : '/auth/login' , search : { redirect : '/showcase' } } )
177+ return
178+ }
179+ voteMutation . mutate ( { showcaseId, value } )
180+ }
181+
31182 const handleLibraryFilter = ( libraryId : string | undefined ) => {
32183 navigate ( {
33184 search : ( prev : typeof search ) => ( {
@@ -182,6 +333,12 @@ export function ShowcaseGallery() {
182333 key = { showcase . id }
183334 showcase = { showcase }
184335 user = { user }
336+ currentUserVote = { votesMap . get ( showcase . id ) }
337+ onVote = { ( value ) => handleVote ( showcase . id , value ) }
338+ isVoting = {
339+ voteMutation . isPending &&
340+ voteMutation . variables ?. showcaseId === showcase . id
341+ }
185342 />
186343 ) ) }
187344 </ div >
0 commit comments