feat: add reset deck progress feature for deck detail page
This commit is contained in:
@@ -162,3 +162,17 @@ export type ActionOutputGetCardById = {
|
||||
message: string;
|
||||
data?: ActionOutputCardWithNote;
|
||||
};
|
||||
|
||||
export const schemaActionInputResetDeckCards = z.object({
|
||||
deckId: z.number().int().positive(),
|
||||
});
|
||||
export type ActionInputResetDeckCards = z.infer<typeof schemaActionInputResetDeckCards>;
|
||||
export const validateActionInputResetDeckCards = generateValidator(schemaActionInputResetDeckCards);
|
||||
|
||||
export type ActionOutputResetDeckCards = {
|
||||
success: boolean;
|
||||
message: string;
|
||||
data?: {
|
||||
count: number;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
ActionInputGetCardStats,
|
||||
ActionInputDeleteCard,
|
||||
ActionInputGetCardById,
|
||||
ActionInputResetDeckCards,
|
||||
ActionOutputCreateCard,
|
||||
ActionOutputAnswerCard,
|
||||
ActionOutputGetCards,
|
||||
@@ -23,6 +24,7 @@ import {
|
||||
ActionOutputCard,
|
||||
ActionOutputCardWithNote,
|
||||
ActionOutputScheduledCard,
|
||||
ActionOutputResetDeckCards,
|
||||
validateActionInputCreateCard,
|
||||
validateActionInputAnswerCard,
|
||||
validateActionInputGetCardsForReview,
|
||||
@@ -31,6 +33,7 @@ import {
|
||||
validateActionInputGetCardStats,
|
||||
validateActionInputDeleteCard,
|
||||
validateActionInputGetCardById,
|
||||
validateActionInputResetDeckCards,
|
||||
} from "./card-action-dto";
|
||||
import {
|
||||
serviceCreateCard,
|
||||
@@ -43,6 +46,7 @@ import {
|
||||
serviceDeleteCard,
|
||||
serviceGetCardByIdWithNote,
|
||||
serviceCheckCardOwnership,
|
||||
serviceResetDeckCards,
|
||||
} from "./card-service";
|
||||
import { CardQueue } from "../../../generated/prisma/enums";
|
||||
|
||||
@@ -425,3 +429,36 @@ export async function actionGetCardById(
|
||||
return { success: false, message: "An error occurred while fetching the card" };
|
||||
}
|
||||
}
|
||||
|
||||
export async function actionResetDeckCards(
|
||||
input: unknown,
|
||||
): Promise<ActionOutputResetDeckCards> {
|
||||
try {
|
||||
const userId = await getCurrentUserId();
|
||||
if (!userId) {
|
||||
return { success: false, message: "Unauthorized" };
|
||||
}
|
||||
|
||||
const validated = validateActionInputResetDeckCards(input);
|
||||
const result = await serviceResetDeckCards({
|
||||
deckId: validated.deckId,
|
||||
userId,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, message: result.message };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: result.message,
|
||||
data: { count: result.count },
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof ValidateError) {
|
||||
return { success: false, message: e.message };
|
||||
}
|
||||
log.error("Failed to reset deck cards", { error: e });
|
||||
return { success: false, message: "An error occurred while resetting deck cards" };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,3 +102,11 @@ export type RepoOutputCardStats = {
|
||||
review: number;
|
||||
due: number;
|
||||
};
|
||||
|
||||
export interface RepoInputResetDeckCards {
|
||||
deckId: number;
|
||||
}
|
||||
|
||||
export type RepoOutputResetDeckCards = {
|
||||
count: number;
|
||||
};
|
||||
|
||||
@@ -7,9 +7,11 @@ import {
|
||||
RepoInputGetCardsForReview,
|
||||
RepoInputGetNewCards,
|
||||
RepoInputBulkUpdateCards,
|
||||
RepoInputResetDeckCards,
|
||||
RepoOutputCard,
|
||||
RepoOutputCardWithNote,
|
||||
RepoOutputCardStats,
|
||||
RepoOutputResetDeckCards,
|
||||
} from "./card-repository-dto";
|
||||
import { CardType, CardQueue } from "../../../generated/prisma/enums";
|
||||
|
||||
@@ -307,3 +309,29 @@ export async function repoGetCardsByNoteId(noteId: bigint): Promise<RepoOutputCa
|
||||
});
|
||||
return cards;
|
||||
}
|
||||
|
||||
export async function repoResetDeckCards(
|
||||
input: RepoInputResetDeckCards,
|
||||
): Promise<RepoOutputResetDeckCards> {
|
||||
log.debug("Resetting deck cards", { deckId: input.deckId });
|
||||
|
||||
const result = await prisma.card.updateMany({
|
||||
where: { deckId: input.deckId },
|
||||
data: {
|
||||
type: CardType.NEW,
|
||||
queue: CardQueue.NEW,
|
||||
due: 0,
|
||||
ivl: 0,
|
||||
factor: 2500,
|
||||
reps: 0,
|
||||
lapses: 0,
|
||||
left: 0,
|
||||
odue: 0,
|
||||
odid: 0,
|
||||
mod: Math.floor(Date.now() / 1000),
|
||||
},
|
||||
});
|
||||
|
||||
log.info("Deck cards reset", { deckId: input.deckId, count: result.count });
|
||||
return { count: result.count };
|
||||
}
|
||||
|
||||
@@ -99,6 +99,24 @@ export type ServiceOutputReviewResult = {
|
||||
scheduled: ServiceOutputScheduledCard;
|
||||
};
|
||||
|
||||
export interface ServiceInputResetDeckCards {
|
||||
deckId: number;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export interface ServiceInputCheckDeckOwnership {
|
||||
deckId: number;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export type ServiceOutputCheckDeckOwnership = boolean;
|
||||
|
||||
export type ServiceOutputResetDeckCards = {
|
||||
success: boolean;
|
||||
count: number;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export const SM2_CONFIG = {
|
||||
LEARNING_STEPS: [1, 10],
|
||||
RELEARNING_STEPS: [10],
|
||||
|
||||
@@ -12,7 +12,9 @@ import {
|
||||
repoDeleteCard,
|
||||
repoGetCardsByNoteId,
|
||||
repoGetCardDeckOwnerId,
|
||||
repoResetDeckCards,
|
||||
} from "./card-repository";
|
||||
import { repoGetUserIdByDeckId } from "@/modules/deck/deck-repository";
|
||||
import {
|
||||
RepoInputUpdateCard,
|
||||
RepoOutputCard,
|
||||
@@ -25,12 +27,15 @@ import {
|
||||
ServiceInputGetCardsByDeckId,
|
||||
ServiceInputGetCardStats,
|
||||
ServiceInputCheckCardOwnership,
|
||||
ServiceInputResetDeckCards,
|
||||
ServiceInputCheckDeckOwnership,
|
||||
ServiceOutputCard,
|
||||
ServiceOutputCardWithNote,
|
||||
ServiceOutputCardStats,
|
||||
ServiceOutputScheduledCard,
|
||||
ServiceOutputReviewResult,
|
||||
ServiceOutputCheckCardOwnership,
|
||||
ServiceOutputResetDeckCards,
|
||||
ReviewEase,
|
||||
SM2_CONFIG,
|
||||
} from "./card-service-dto";
|
||||
@@ -495,3 +500,27 @@ export async function serviceCheckCardOwnership(
|
||||
const ownerId = await repoGetCardDeckOwnerId(input.cardId);
|
||||
return ownerId === input.userId;
|
||||
}
|
||||
|
||||
export async function serviceCheckDeckOwnership(
|
||||
input: ServiceInputCheckDeckOwnership,
|
||||
): Promise<ServiceOutputCheckCardOwnership> {
|
||||
log.debug("Checking deck ownership", { deckId: input.deckId });
|
||||
const ownerId = await repoGetUserIdByDeckId(input.deckId);
|
||||
return ownerId === input.userId;
|
||||
}
|
||||
|
||||
export async function serviceResetDeckCards(
|
||||
input: ServiceInputResetDeckCards,
|
||||
): Promise<ServiceOutputResetDeckCards> {
|
||||
log.info("Resetting deck cards", { deckId: input.deckId, userId: input.userId });
|
||||
|
||||
const isOwner = await serviceCheckDeckOwnership({ deckId: input.deckId, userId: input.userId });
|
||||
if (!isOwner) {
|
||||
return { success: false, count: 0, message: "You do not have permission to reset this deck" };
|
||||
}
|
||||
|
||||
const result = await repoResetDeckCards({ deckId: input.deckId });
|
||||
|
||||
log.info("Deck cards reset successfully", { deckId: input.deckId, count: result.count });
|
||||
return { success: true, count: result.count, message: "Deck cards reset successfully" };
|
||||
}
|
||||
|
||||
@@ -32,7 +32,8 @@ export function repoGenerateGuid(): string {
|
||||
|
||||
export function repoCalculateCsum(text: string): number {
|
||||
const hash = createHash("sha1").update(text.normalize("NFC")).digest("hex");
|
||||
return parseInt(hash.substring(0, 8), 16);
|
||||
// Use 7 hex chars to stay within INTEGER range (max 268,435,455 < 2,147,483,647)
|
||||
return parseInt(hash.substring(0, 7), 16);
|
||||
}
|
||||
|
||||
export function repoJoinFields(fields: string[]): string {
|
||||
|
||||
Reference in New Issue
Block a user