You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
196 lines
5.0 KiB
196 lines
5.0 KiB
import { dbGlobal } from "drizzle-pkg/lib/db";
|
|
import { favorites, cards, cardImages, cardTags, tags } from "drizzle-pkg/lib/schema/content";
|
|
import { eq, and, inArray, desc, sql, asc } from "drizzle-orm";
|
|
import type { CardWithRelations } from "../card";
|
|
|
|
// ============ Types ============
|
|
|
|
export interface ListFavoritesOptions {
|
|
page?: number
|
|
pageSize?: number
|
|
}
|
|
|
|
// ============ Toggle ============
|
|
|
|
/**
|
|
* Toggle favorite status for a card.
|
|
* Returns the new favorited state.
|
|
*/
|
|
export async function toggleFavorite(
|
|
userId: number,
|
|
cardId: number,
|
|
): Promise<{ favorited: boolean }> {
|
|
const [existing] = await dbGlobal
|
|
.select()
|
|
.from(favorites)
|
|
.where(
|
|
and(
|
|
eq(favorites.userId, userId),
|
|
eq(favorites.cardId, cardId),
|
|
),
|
|
)
|
|
.limit(1);
|
|
|
|
if (existing) {
|
|
await dbGlobal
|
|
.delete(favorites)
|
|
.where(
|
|
and(
|
|
eq(favorites.userId, userId),
|
|
eq(favorites.cardId, cardId),
|
|
),
|
|
);
|
|
return { favorited: false };
|
|
}
|
|
|
|
await dbGlobal.insert(favorites).values({
|
|
userId,
|
|
cardId,
|
|
});
|
|
return { favorited: true };
|
|
}
|
|
|
|
// ============ Batch check ============
|
|
|
|
/**
|
|
* Given a list of card IDs, return which ones the user has favorited.
|
|
*/
|
|
export async function batchIsFavorited(
|
|
userId: number,
|
|
cardIds: number[],
|
|
): Promise<Set<number>> {
|
|
if (cardIds.length === 0) return new Set();
|
|
|
|
const rows = await dbGlobal
|
|
.select({ cardId: favorites.cardId })
|
|
.from(favorites)
|
|
.where(
|
|
and(
|
|
eq(favorites.userId, userId),
|
|
inArray(favorites.cardId, cardIds),
|
|
),
|
|
);
|
|
|
|
return new Set(rows.map((r) => r.cardId));
|
|
}
|
|
|
|
// ============ List ============
|
|
|
|
/**
|
|
* List favorited card IDs for a user.
|
|
*/
|
|
export async function listFavoriteCardIds(
|
|
userId: number,
|
|
opts: ListFavoritesOptions = {},
|
|
): Promise<{ cardIds: number[]; total: number }> {
|
|
const page = opts.page ?? 1;
|
|
const pageSize = opts.pageSize ?? 12;
|
|
|
|
const [rows, countResult] = await Promise.all([
|
|
dbGlobal
|
|
.select({ cardId: favorites.cardId })
|
|
.from(favorites)
|
|
.where(eq(favorites.userId, userId))
|
|
.orderBy(desc(favorites.createdAt))
|
|
.limit(pageSize)
|
|
.offset((page - 1) * pageSize),
|
|
dbGlobal
|
|
.select({ count: sql<number>`count(*)` })
|
|
.from(favorites)
|
|
.where(eq(favorites.userId, userId)),
|
|
]);
|
|
|
|
return {
|
|
cardIds: rows.map((r) => r.cardId),
|
|
total: countResult[0]?.count ?? 0,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* List favorited cards with full card data.
|
|
*/
|
|
export async function listFavoriteCards(
|
|
userId: number,
|
|
opts: ListFavoritesOptions = {},
|
|
): Promise<{ items: CardWithRelations[]; total: number; page: number; pageSize: number; hasMore: boolean }> {
|
|
const page = opts.page ?? 1;
|
|
const pageSize = opts.pageSize ?? 12;
|
|
|
|
const { cardIds, total } = await listFavoriteCardIds(userId, { page, pageSize });
|
|
|
|
if (cardIds.length === 0) {
|
|
return { items: [], total, page, pageSize, hasMore: false };
|
|
}
|
|
|
|
// Fetch full card data via existing listCards with id filter
|
|
// Build a simple query with the IDs
|
|
const rows = await dbGlobal
|
|
.select()
|
|
.from(cards)
|
|
.where(inArray(cards.id, cardIds))
|
|
.orderBy(desc(cards.createdAt));
|
|
|
|
// Load relations
|
|
const allImages = await dbGlobal
|
|
.select()
|
|
.from(cardImages)
|
|
.where(inArray(cardImages.cardId, cardIds));
|
|
|
|
const allCardTags = await dbGlobal
|
|
.select()
|
|
.from(cardTags)
|
|
.where(inArray(cardTags.cardId, cardIds));
|
|
|
|
const tagIds = [...new Set(allCardTags.map((ct) => ct.tagId))];
|
|
const tagRows =
|
|
tagIds.length > 0
|
|
? await dbGlobal.select().from(tags).where(inArray(tags.id, tagIds))
|
|
: [];
|
|
|
|
const tagMap = new Map(tagRows.map((t) => [t.id, t]));
|
|
const imagesByCard = new Map<number, { id: number; url: string; sortOrder: number }[]>();
|
|
for (const img of allImages) {
|
|
if (!imagesByCard.has(img.cardId)) imagesByCard.set(img.cardId, []);
|
|
imagesByCard.get(img.cardId)!.push({
|
|
id: img.id,
|
|
url: img.url,
|
|
sortOrder: img.sortOrder,
|
|
});
|
|
}
|
|
const tagsByCard = new Map<number, { id: number; name: string; slug: string }[]>();
|
|
for (const ct of allCardTags) {
|
|
const tag = tagMap.get(ct.tagId);
|
|
if (!tag) continue;
|
|
if (!tagsByCard.has(ct.cardId)) tagsByCard.set(ct.cardId, []);
|
|
tagsByCard.get(ct.cardId)!.push({
|
|
id: tag.id,
|
|
name: tag.name,
|
|
slug: tag.slug,
|
|
});
|
|
}
|
|
|
|
const items: CardWithRelations[] = rows
|
|
.filter((row) => cardIds.includes(row.id))
|
|
.sort((a, b) => cardIds.indexOf(a.id) - cardIds.indexOf(b.id))
|
|
.map((row) => ({
|
|
id: row.id,
|
|
type: row.type as CardWithRelations["type"],
|
|
title: row.title,
|
|
description: row.description,
|
|
aspectRatio: row.aspectRatio,
|
|
categoryId: row.categoryId,
|
|
createdAt: row.createdAt,
|
|
updatedAt: row.updatedAt,
|
|
images: imagesByCard.get(row.id) ?? [],
|
|
tags: tagsByCard.get(row.id) ?? [],
|
|
articles: [],
|
|
}));
|
|
|
|
return {
|
|
items,
|
|
total,
|
|
page,
|
|
pageSize,
|
|
hasMore: page * pageSize < total,
|
|
};
|
|
}
|
|
|