@ -3,7 +3,7 @@ import { supabase } from '../supabase.js';
export async function fetchMyDecks ( userId ) {
export async function fetchMyDecks ( userId ) {
const { data , error } = await supabase
const { data , error } = await supabase
. from ( 'decks' )
. from ( 'decks' )
. select ( 'id, title, description, config, published, created_at, updated_at, copied_from_deck_id ')
. select ( 'id, title, description, config, published, created_at, updated_at, copied_from_deck_id , version, copied_from_version ')
. eq ( 'owner_id' , userId )
. eq ( 'owner_id' , userId )
. order ( 'updated_at' , { ascending : false } ) ;
. order ( 'updated_at' , { ascending : false } ) ;
if ( error ) throw error ;
if ( error ) throw error ;
@ -29,7 +29,7 @@ export async function fetchMyDecks(userId) {
? supabase . from ( 'deck_ratings' ) . select ( 'deck_id, rating' ) . in ( 'deck_id' , ratingDeckIds )
? supabase . from ( 'deck_ratings' ) . select ( 'deck_id, rating' ) . in ( 'deck_id' , ratingDeckIds )
: Promise . resolve ( { data : [ ] } ) ,
: Promise . resolve ( { data : [ ] } ) ,
sourceDeckIds . length > 0
sourceDeckIds . length > 0
? supabase . from ( 'decks' ) . select ( 'id, title ') . in ( 'id' , sourceDeckIds )
? supabase . from ( 'decks' ) . select ( 'id, title , version, updated_at ') . in ( 'id' , sourceDeckIds )
: Promise . resolve ( { data : [ ] } ) ,
: Promise . resolve ( { data : [ ] } ) ,
] ) ;
] ) ;
@ -45,13 +45,23 @@ export async function fetchMyDecks(userId) {
return { average _rating : Math . round ( ( sum / arr . length ) * 100 ) / 100 , rating _count : arr . length } ;
return { average _rating : Math . round ( ( sum / arr . length ) * 100 ) / 100 , rating _count : arr . length } ;
} ;
} ;
const source Title ById = new Map ( ( sourceTitlesRows . data ? ? [ ] ) . map ( ( d ) => [ d . id , d . title ] ) ) ;
const source ById = new Map ( ( sourceTitlesRows . data ? ? [ ] ) . map ( ( d ) => [ d . id , d ] ) ) ;
return decks . map ( ( d , i ) => {
return decks . map ( ( d , i ) => {
const showRatingDeckId = d . copied _from _deck _id || d . id ;
const showRatingDeckId = d . copied _from _deck _id || d . id ;
const rating = getRating ( showRatingDeckId ) ;
const rating = getRating ( showRatingDeckId ) ;
const canRate = ! ! d . copied _from _deck _id ;
const canRate = ! ! d . copied _from _deck _id ;
const source _deck _title = d . copied _from _deck _id ? sourceTitleById . get ( d . copied _from _deck _id ) ? ? 'Deck' : null ;
const sourceDeck = d . copied _from _deck _id ? sourceById . get ( d . copied _from _deck _id ) : null ;
const source _deck _title = sourceDeck ? . title ? ? 'Deck' ;
const source _version = sourceDeck ? . version ? ? 1 ;
const my _version = d . copied _from _version ? ? 0 ;
const versionOutdated = source _version > my _version ;
const sourceNewerByTime =
sourceDeck ? . updated _at &&
d . updated _at &&
new Date ( sourceDeck . updated _at ) . getTime ( ) > new Date ( d . updated _at ) . getTime ( ) ;
const needs _update =
! ! d . copied _from _deck _id && ( versionOutdated || sourceNewerByTime ) ;
return {
return {
... d ,
... d ,
question _count : questionCounts [ i ] ,
question _count : questionCounts [ i ] ,
@ -59,6 +69,8 @@ export async function fetchMyDecks(userId) {
can _rate : canRate ,
can _rate : canRate ,
rateable _deck _id : d . copied _from _deck _id || null ,
rateable _deck _id : d . copied _from _deck _id || null ,
source _deck _title ,
source _deck _title ,
source _version ,
needs _update ,
} ;
} ;
} ) ;
} ) ;
}
}
@ -269,10 +281,11 @@ export async function copyDeckToUser(deckId, userId) {
config : source . config ? ? { } ,
config : source . config ? ? { } ,
questions ,
questions ,
copiedFromDeckId : deckId ,
copiedFromDeckId : deckId ,
copiedFromVersion : source . version ? ? 1 ,
} ) ;
} ) ;
}
}
export async function createDeck ( ownerId , { title , description , config , questions , copiedFromDeckId } ) {
export async function createDeck ( ownerId , { title , description , config , questions , copiedFromDeckId , copiedFromVersion } ) {
const row = {
const row = {
owner _id : ownerId ,
owner _id : ownerId ,
title : title . trim ( ) ,
title : title . trim ( ) ,
@ -280,6 +293,7 @@ export async function createDeck(ownerId, { title, description, config, question
config : config ? ? { } ,
config : config ? ? { } ,
} ;
} ;
if ( copiedFromDeckId != null ) row . copied _from _deck _id = copiedFromDeckId ;
if ( copiedFromDeckId != null ) row . copied _from _deck _id = copiedFromDeckId ;
if ( copiedFromVersion != null ) row . copied _from _version = copiedFromVersion ;
const { data : deck , error : deckError } = await supabase
const { data : deck , error : deckError } = await supabase
. from ( 'decks' )
. from ( 'decks' )
. insert ( row )
. insert ( row )
@ -302,13 +316,23 @@ export async function createDeck(ownerId, { title, description, config, question
}
}
export async function updateDeck ( deckId , { title , description , config , questions } ) {
export async function updateDeck ( deckId , { title , description , config , questions } ) {
const { data : current , error : fetchErr } = await supabase
. from ( 'decks' )
. select ( 'version, published' )
. eq ( 'id' , deckId )
. single ( ) ;
if ( fetchErr ) throw fetchErr ;
const bumpVersion = current ? . published === true ;
const nextVersion = bumpVersion ? ( current ? . version ? ? 1 ) + 1 : ( current ? . version ? ? 1 ) ;
const updatePayload = {
title : title . trim ( ) ,
description : ( description ? ? '' ) . trim ( ) ,
config : config ? ? { } ,
version : nextVersion ,
} ;
const { error : deckError } = await supabase
const { error : deckError } = await supabase
. from ( 'decks' )
. from ( 'decks' )
. update ( {
. update ( updatePayload )
title : title . trim ( ) ,
description : ( description ? ? '' ) . trim ( ) ,
config : config ? ? { } ,
} )
. eq ( 'id' , deckId ) ;
. eq ( 'id' , deckId ) ;
if ( deckError ) throw deckError ;
if ( deckError ) throw deckError ;
const { error : delError } = await supabase . from ( 'questions' ) . delete ( ) . eq ( 'deck_id' , deckId ) ;
const { error : delError } = await supabase . from ( 'questions' ) . delete ( ) . eq ( 'deck_id' , deckId ) ;
@ -337,6 +361,94 @@ export async function deleteDeck(deckId) {
if ( error ) throw error ;
if ( error ) throw error ;
}
}
/ * *
* Get source deck and copy deck with questions for the "Update from community" preview .
* Copy must be owned by userId and have copied _from _deck _id .
* @ returns { { source : object , copy : object , changes : string [ ] } }
* /
export async function getSourceUpdatePreview ( copyDeckId , userId ) {
const { data : copy , error : copyErr } = await supabase
. from ( 'decks' )
. select ( '*' )
. eq ( 'id' , copyDeckId )
. eq ( 'owner_id' , userId )
. single ( ) ;
if ( copyErr || ! copy ? . copied _from _deck _id ) throw new Error ( 'Deck not found or not a copy' ) ;
const sourceId = copy . copied _from _deck _id ;
const [ source , copyQuestionsRows ] = await Promise . all ( [
fetchDeckWithQuestions ( sourceId ) ,
supabase . from ( 'questions' ) . select ( '*' ) . eq ( 'deck_id' , copyDeckId ) . order ( 'sort_order' , { ascending : true } ) ,
] ) ;
if ( ! source . published ) throw new Error ( 'Source deck is no longer available' ) ;
const copyQuestions = copyQuestionsRows . data ? ? [ ] ;
const copyWithQuestions = { ... copy , questions : copyQuestions } ;
const changes = [ ] ;
if ( ( source . title ? ? '' ) . trim ( ) !== ( copy . title ? ? '' ) . trim ( ) ) {
changes . push ( ` Title: " ${ ( copy . title ? ? '' ) . trim ( ) } " → " ${ ( source . title ? ? '' ) . trim ( ) } " ` ) ;
}
if ( ( source . description ? ? '' ) . trim ( ) !== ( copy . description ? ? '' ) . trim ( ) ) {
changes . push ( 'Description updated' ) ;
}
const srcLen = ( source . questions ? ? [ ] ) . length ;
const copyLen = copyQuestions . length ;
if ( srcLen !== copyLen ) {
changes . push ( ` Questions: ${ copyLen } → ${ srcLen } ` ) ;
} else {
const anyDifferent = ( source . questions ? ? [ ] ) . some ( ( sq , i ) => {
const cq = copyQuestions [ i ] ;
if ( ! cq ) return true ;
return ( sq . prompt ? ? '' ) !== ( cq . prompt ? ? '' ) || ( sq . explanation ? ? '' ) !== ( cq . explanation ? ? '' ) ;
} ) ;
if ( anyDifferent ) changes . push ( 'Some question content updated' ) ;
}
if ( changes . length === 0 ) changes . push ( 'Content is in sync (version metadata will update)' ) ;
return { source , copy : copyWithQuestions , changes } ;
}
/ * *
* Update a community copy to match the current source deck ( title , description , config , questions , copied _from _version ) .
* /
export async function applySourceUpdate ( copyDeckId , userId ) {
const { data : copy , error : copyErr } = await supabase
. from ( 'decks' )
. select ( 'id, copied_from_deck_id' )
. eq ( 'id' , copyDeckId )
. eq ( 'owner_id' , userId )
. single ( ) ;
if ( copyErr || ! copy ? . copied _from _deck _id ) throw new Error ( 'Deck not found or not a copy' ) ;
const source = await fetchDeckWithQuestions ( copy . copied _from _deck _id ) ;
if ( ! source . published ) throw new Error ( 'Source deck is no longer available' ) ;
const { error : deckError } = await supabase
. from ( 'decks' )
. update ( {
title : source . title ? ? '' ,
description : source . description ? ? '' ,
config : source . config ? ? { } ,
copied _from _version : source . version ? ? 1 ,
} )
. eq ( 'id' , copyDeckId ) ;
if ( deckError ) throw deckError ;
const { error : delError } = await supabase . from ( 'questions' ) . delete ( ) . eq ( 'deck_id' , copyDeckId ) ;
if ( delError ) throw delError ;
const questions = source . questions ? ? [ ] ;
if ( questions . length > 0 ) {
const rows = questions . map ( ( q , i ) => ( {
deck _id : copyDeckId ,
sort _order : i ,
prompt : ( q . prompt ? ? '' ) . trim ( ) ,
explanation : ( q . explanation ? ? '' ) . trim ( ) || null ,
answers : Array . isArray ( q . answers ) ? q . answers : [ ] ,
correct _answer _indices : Array . isArray ( q . correct _answer _indices ) ? q . correct _answer _indices : [ ] ,
} ) ) ;
const { error : qError } = await supabase . from ( 'questions' ) . insert ( rows ) ;
if ( qError ) throw qError ;
}
}
/** True if the user already has this deck (owns it or has added a copy). */
/** True if the user already has this deck (owns it or has added a copy). */
export async function userHasDeck ( deckId , userId ) {
export async function userHasDeck ( deckId , userId ) {
if ( ! userId ) return false ;
if ( ! userId ) return false ;