1
+ 'use server' ;
2
+
3
+ import { ObjectId } from 'mongodb' ;
4
+ import { revalidatePath } from 'next/cache' ;
5
+ import type { Card , CardDocument , ReviewEventDocument } from '@/types' ;
6
+ import { ReviewResult } from '@/types' ; // Import enum value
7
+ // import jwt from 'jsonwebtoken'; // Removed
8
+ import { connectToDatabase } from '@/lib/db' ;
9
+ import { verifyAuthToken } from '@/lib/auth' ; // Import shared auth function
10
+ import { mapMongoId } from '@/lib/utils' ; // Import shared helper
11
+
12
+ // --- Remove Auth Helper ---
13
+ // function verifyAuthToken(token: string | undefined): JwtPayload | null { ... } // Removed
14
+
15
+ // --- Remove Generic Helper ---
16
+ // function mapMongoId<T extends { _id?: ObjectId }>(...) { ... } // Removed
17
+
18
+ // --- Keep Specific Helper ---
19
+ function mapCardDocument ( doc : CardDocument | null | undefined ) : Card | null {
20
+ const mapped = mapMongoId ( doc ) ; // Use imported helper
21
+ if ( mapped && mapped . deck_id instanceof ObjectId ) {
22
+ return {
23
+ ...mapped ,
24
+ deck_id : mapped . deck_id . toString ( ) ,
25
+ } as Card ;
26
+ }
27
+ // Handle cases where mapping fails or deck_id isn't ObjectId (shouldn't happen with proper types)
28
+ return null ;
29
+ }
30
+
31
+ // --- Card Actions ---
32
+
33
+ interface CreateCardResult {
34
+ success : boolean ;
35
+ card ?: Card ; // Use shared Card type
36
+ message ?: string ;
37
+ }
38
+
39
+ interface CreateCardInput {
40
+ deckId : string ;
41
+ frontText : string ;
42
+ backText : string ;
43
+ token : string | undefined ;
44
+ }
45
+
46
+ export async function createCardAction ( input : CreateCardInput ) : Promise < CreateCardResult > {
47
+ const { deckId, frontText, backText, token } = input ;
48
+
49
+ const user = verifyAuthToken ( token ) ;
50
+ if ( ! user ) return { success : false , message : 'Unauthorized' } ;
51
+
52
+ if ( ! deckId || ! ObjectId . isValid ( deckId ) ) {
53
+ return { success : false , message : 'Valid Deck ID is required' } ;
54
+ }
55
+ if ( ! frontText || ! frontText . trim ( ) ) {
56
+ return { success : false , message : 'Front text cannot be empty' } ;
57
+ }
58
+ if ( ! backText || ! backText . trim ( ) ) {
59
+ return { success : false , message : 'Back text cannot be empty' } ;
60
+ }
61
+
62
+ // No client variable needed
63
+ try {
64
+ const { db } = await connectToDatabase ( ) ; // Use imported function
65
+ const cardsCollection = db . collection < CardDocument > ( 'cards' ) ;
66
+
67
+ const newCardData : Omit < CardDocument , '_id' > = {
68
+ deck_id : new ObjectId ( deckId ) ,
69
+ front_text : frontText . trim ( ) ,
70
+ back_text : backText . trim ( ) ,
71
+ createdAt : new Date ( ) ,
72
+ updatedAt : new Date ( ) ,
73
+ } ;
74
+
75
+ const result = await cardsCollection . insertOne ( newCardData as CardDocument ) ;
76
+
77
+ if ( ! result . insertedId ) {
78
+ throw new Error ( 'Card creation failed in DB.' ) ;
79
+ }
80
+
81
+ const createdCardDoc = await cardsCollection . findOne ( { _id : result . insertedId } ) ;
82
+ // Use specific card mapper
83
+ const mappedCreatedCard = mapCardDocument ( createdCardDoc ) ;
84
+
85
+ if ( ! mappedCreatedCard ) {
86
+ throw new Error ( 'Failed to map created card.' ) ;
87
+ }
88
+
89
+ // No client.close() needed
90
+ revalidatePath ( `/deck/${ deckId } /edit` ) ;
91
+ return { success : true , card : mappedCreatedCard } ; // Already type Card
92
+
93
+ } catch ( error ) {
94
+ console . error ( '[Create Card Action Error]' , error ) ;
95
+ // No client?.close() needed
96
+ const message = error instanceof Error ? error . message : 'Failed to create card' ;
97
+ return { success : false , message } ;
98
+ }
99
+ }
100
+
101
+ // --- Fetch Cards for a Deck ---
102
+ interface FetchCardsResult {
103
+ success : boolean ;
104
+ cards ?: Card [ ] ; // Use shared Card type
105
+ message ?: string ;
106
+ }
107
+
108
+ export async function fetchDeckCardsAction ( deckId : string ) : Promise < FetchCardsResult > {
109
+ if ( ! deckId || ! ObjectId . isValid ( deckId ) ) {
110
+ return { success : false , message : 'Valid Deck ID is required' } ;
111
+ }
112
+
113
+ try {
114
+ const { db } = await connectToDatabase ( ) ;
115
+ const cardsCollection = db . collection < CardDocument > ( 'cards' ) ;
116
+
117
+ const cardDocs = await cardsCollection . find ( { deck_id : new ObjectId ( deckId ) } ) . sort ( { createdAt : 1 } ) . toArray ( ) ;
118
+ const mappedCards = cardDocs . map ( mapCardDocument ) . filter ( ( c ) : c is Card => c !== null ) ;
119
+
120
+ return { success : true , cards : mappedCards } ;
121
+
122
+ } catch ( error ) {
123
+ console . error ( '[Fetch Deck Cards Action Error]' , error ) ;
124
+ const message = error instanceof Error ? error . message : 'Failed to fetch cards for deck' ;
125
+ return { success : false , message } ;
126
+ }
127
+ }
128
+
129
+ // --- Update Card ---
130
+ interface UpdateCardResult {
131
+ success : boolean ;
132
+ card ?: Card ;
133
+ message ?: string ;
134
+ }
135
+
136
+ interface UpdateCardInput {
137
+ cardId : string ;
138
+ deckId : string ; // Needed for revalidation
139
+ frontText ?: string ;
140
+ backText ?: string ;
141
+ token : string | undefined ;
142
+ }
143
+
144
+ export async function updateCardAction ( input : UpdateCardInput ) : Promise < UpdateCardResult > {
145
+ const { cardId, deckId, frontText, backText, token } = input ;
146
+
147
+ const user = verifyAuthToken ( token ) ;
148
+ if ( ! user ) return { success : false , message : 'Unauthorized' } ;
149
+
150
+ if ( ! cardId || ! ObjectId . isValid ( cardId ) || ! deckId || ! ObjectId . isValid ( deckId ) ) {
151
+ return { success : false , message : 'Valid Card ID and Deck ID are required' } ;
152
+ }
153
+ if ( ( ! frontText || ! frontText . trim ( ) ) && ( ! backText || ! backText . trim ( ) ) ) {
154
+ return { success : false , message : 'At least one field (front or back text) must be provided for update' } ;
155
+ }
156
+
157
+ const updates : Partial < Pick < CardDocument , 'front_text' | 'back_text' | 'updatedAt' > > = {
158
+ updatedAt : new Date ( ) ,
159
+ } ;
160
+ if ( frontText && frontText . trim ( ) ) updates . front_text = frontText . trim ( ) ;
161
+ if ( backText && backText . trim ( ) ) updates . back_text = backText . trim ( ) ;
162
+
163
+ try {
164
+ const { db } = await connectToDatabase ( ) ;
165
+ const cardsCollection = db . collection < CardDocument > ( 'cards' ) ;
166
+
167
+ const result = await cardsCollection . findOneAndUpdate (
168
+ { _id : new ObjectId ( cardId ) , deck_id : new ObjectId ( deckId ) } , // Ensure card belongs to the deck
169
+ { $set : updates } ,
170
+ { returnDocument : 'after' }
171
+ ) ;
172
+
173
+ const mappedUpdatedCard = mapCardDocument ( result ) ;
174
+
175
+ if ( ! mappedUpdatedCard ) {
176
+ return { success : false , message : 'Card not found for update or update failed' } ;
177
+ }
178
+
179
+ revalidatePath ( `/deck/${ deckId } /edit` ) ;
180
+ return { success : true , card : mappedUpdatedCard } ;
181
+
182
+ } catch ( error ) {
183
+ console . error ( '[Update Card Action Error]' , error ) ;
184
+ const message = error instanceof Error ? error . message : 'Failed to update card' ;
185
+ return { success : false , message } ;
186
+ }
187
+ }
188
+
189
+ // --- Delete Card ---
190
+ interface DeleteCardResult {
191
+ success : boolean ;
192
+ message ?: string ;
193
+ }
194
+
195
+ interface DeleteCardInput {
196
+ cardId : string ;
197
+ deckId : string ; // Needed for revalidation and verification
198
+ token : string | undefined ;
199
+ }
200
+
201
+ export async function deleteCardAction ( input : DeleteCardInput ) : Promise < DeleteCardResult > {
202
+ const { cardId, deckId, token } = input ;
203
+
204
+ const user = verifyAuthToken ( token ) ;
205
+ if ( ! user ) return { success : false , message : 'Unauthorized' } ;
206
+
207
+ if ( ! cardId || ! ObjectId . isValid ( cardId ) || ! deckId || ! ObjectId . isValid ( deckId ) ) {
208
+ return { success : false , message : 'Valid Card ID and Deck ID are required' } ;
209
+ }
210
+
211
+ try {
212
+ const { db } = await connectToDatabase ( ) ;
213
+ const cardsCollection = db . collection < CardDocument > ( 'cards' ) ;
214
+
215
+ // TODO: Delete associated Review Events if necessary
216
+ const result = await cardsCollection . deleteOne ( { _id : new ObjectId ( cardId ) , deck_id : new ObjectId ( deckId ) } ) ;
217
+
218
+ if ( result . deletedCount === 0 ) {
219
+ return { success : false , message : 'Card not found or does not belong to the specified deck' } ;
220
+ }
221
+
222
+ revalidatePath ( `/deck/${ deckId } /edit` ) ;
223
+ return { success : true } ;
224
+
225
+ } catch ( error ) {
226
+ console . error ( '[Delete Card Action Error]' , error ) ;
227
+ const message = error instanceof Error ? error . message : 'Failed to delete card' ;
228
+ return { success : false , message } ;
229
+ }
230
+ }
231
+
232
+ // --- Create Review Event ---
233
+ interface CreateReviewEventResult {
234
+ success : boolean ;
235
+ reviewEventId ?: string ; // Return the ID of the created event
236
+ message ?: string ;
237
+ }
238
+
239
+ interface CreateReviewEventInput {
240
+ cardId : string ;
241
+ deckId : string ; // Might be useful for context/validation
242
+ result : ReviewResult ;
243
+ // No token needed if we decide reviews are public/unauthenticated
244
+ // Add token if reviews should be tied to a logged-in user
245
+ }
246
+
247
+ export async function createReviewEventAction ( input : CreateReviewEventInput ) : Promise < CreateReviewEventResult > {
248
+ const { cardId, deckId, result } = input ;
249
+
250
+ // Validate input
251
+ if ( ! cardId || ! ObjectId . isValid ( cardId ) ) {
252
+ return { success : false , message : 'Valid Card ID is required' } ;
253
+ }
254
+ if ( ! deckId || ! ObjectId . isValid ( deckId ) ) {
255
+ // Optional: Could fetch card to verify deckId association
256
+ return { success : false , message : 'Valid Deck ID is required' } ;
257
+ }
258
+ // Check if result is a valid enum value
259
+ if ( ! Object . values ( ReviewResult ) . includes ( result ) ) {
260
+ return { success : false , message : 'Invalid review result value' } ;
261
+ }
262
+
263
+ // TODO: Add auth check here if reviews require login
264
+ // const user = verifyAuthToken(token);
265
+ // if (!user) return { success: false, message: 'Unauthorized' };
266
+
267
+ try {
268
+ const { db } = await connectToDatabase ( ) ;
269
+ const reviewsCollection = db . collection < ReviewEventDocument > ( 'review_events' ) ; // Use a separate collection
270
+
271
+ const newReviewEventData : Omit < ReviewEventDocument , '_id' > = {
272
+ card_id : new ObjectId ( cardId ) ,
273
+ // deck_id: new ObjectId(deckId), // Optionally store deck_id too
274
+ result : result ,
275
+ timestamp : new Date ( ) , // Use server timestamp
276
+ } ;
277
+
278
+ const dbResult = await reviewsCollection . insertOne ( newReviewEventData as ReviewEventDocument ) ;
279
+
280
+ if ( ! dbResult . insertedId ) {
281
+ throw new Error ( 'Review event creation failed in DB.' ) ;
282
+ }
283
+
284
+ // No revalidation needed typically for review events unless displaying history
285
+ // revalidatePath(...)
286
+
287
+ return { success : true , reviewEventId : dbResult . insertedId . toString ( ) } ;
288
+
289
+ } catch ( error ) {
290
+ console . error ( '[Create Review Event Action Error]' , error ) ;
291
+ const message = error instanceof Error ? error . message : 'Failed to record review event' ;
292
+ return { success : false , message } ;
293
+ }
294
+ }
295
+
296
+ // Remove duplicated example
297
+ /*
298
+ export async function fetchDecksAction(): Promise<{ success: boolean; decks?: Deck[]; message?: string }> {
299
+ // ... implementation ...
300
+ }
301
+ */
0 commit comments