151 lines
5.9 KiB
TypeScript
151 lines
5.9 KiB
TypeScript
// Die Community-Logos lesen: Tabelle `logos` direkt aus Appwrite (Gäste dürfen sichtbare Zeilen lesen), dazu die eigenen Herzen
|
|
// (`logo_likes`, jeder liest nur seine) und die Meldungen (`logo_reports`, nur Admins). Schreiben (Hochladen, Herz, Melden,
|
|
// Löschen, Ausblenden) läuft über den API-Dienst, siehe logo-api.ts. Aufbau wie community/gallery.ts.
|
|
import { AppwriteException, Query, type Models } from 'appwrite';
|
|
import { ID_PATTERN, fetchFile } from '../account/cards';
|
|
import { IDS, tables } from '../account/appwrite';
|
|
import { isRowMissing } from '../account/errors';
|
|
|
|
export interface CommunityLogo {
|
|
id: string;
|
|
name: string;
|
|
displayName: string;
|
|
fileId: string;
|
|
ownerId: string;
|
|
/** Einfarbig: nur die Deckkraft zählt, das Logo nimmt die Farbe der Karte an (hell/dunkel). */
|
|
mono: boolean;
|
|
likeCount: number;
|
|
hidden: boolean;
|
|
reportCount: number;
|
|
createdAt: string;
|
|
}
|
|
|
|
export interface LogoReport {
|
|
id: string;
|
|
logoId: string;
|
|
reason: string;
|
|
createdAt: string;
|
|
}
|
|
|
|
/** Beliebt: meiste Herzen zuerst, bei Gleichstand die neuesten; Neu: neueste zuerst. */
|
|
export type LogoSort = 'popular' | 'new';
|
|
|
|
/** Ansicht der Logos: öffentlich `all`; `reported`/`hidden` zeigen nur Admins etwas (die Rechte prüft der Server). */
|
|
export type LogoFilter = 'all' | 'reported' | 'hidden';
|
|
|
|
export interface LogoListOptions {
|
|
sort: LogoSort;
|
|
filter?: LogoFilter;
|
|
cursor?: string;
|
|
}
|
|
|
|
export const LOGO_PAGE_SIZE = 24;
|
|
/** Obergrenze für die Meldungen eines Logos und die Herzen einer Seite (eine Seite hat 24 Logos). */
|
|
const LIST_MAX = 100;
|
|
|
|
type Row = Record<string, unknown>;
|
|
/** Eine Zeile, wie Appwrite sie liefert: Systemfelder (`$id`, …) plus die Spalten der Tabelle. */
|
|
type TableRow = Models.Row & Row;
|
|
|
|
function text(value: unknown): string {
|
|
return typeof value === 'string' ? value : '';
|
|
}
|
|
|
|
function count(value: unknown): number {
|
|
return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : 0;
|
|
}
|
|
|
|
/** Eine Zeile der Tabelle `logos` als Logo; fehlende oder falsch typisierte Felder werden leer. */
|
|
export function toLogo(row: Row): CommunityLogo {
|
|
return {
|
|
id: text(row.$id),
|
|
name: text(row.name),
|
|
displayName: text(row.displayName),
|
|
fileId: text(row.fileId),
|
|
ownerId: text(row.ownerId),
|
|
mono: row.mono === true,
|
|
likeCount: count(row.likeCount),
|
|
hidden: row.hidden === true,
|
|
reportCount: count(row.reportCount),
|
|
createdAt: text(row.$createdAt),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Abfragen für eine Seite der Logos. Öffentlich immer nur sichtbare; Admin-Filter: „Gemeldet“ = reportCount > 0 (auch schon
|
|
* ausgeblendete), „Ausgeblendet“ = hidden. Beliebt sortiert nach Herzen, dann nach Alter (beides absteigend), Neu nur nach Alter.
|
|
* Optional: Cursor (die ID des letzten Logos der Seite davor).
|
|
*/
|
|
export function logoQueries(opts: LogoListOptions): string[] {
|
|
const base =
|
|
opts.filter === 'reported'
|
|
? Query.greaterThan('reportCount', 0)
|
|
: opts.filter === 'hidden'
|
|
? Query.equal('hidden', true)
|
|
: Query.equal('hidden', false);
|
|
const order = opts.sort === 'popular' ? [Query.orderDesc('likeCount'), Query.orderDesc('$createdAt')] : [Query.orderDesc('$createdAt')];
|
|
const queries = [base, ...order, Query.limit(LOGO_PAGE_SIZE)];
|
|
if (opts.cursor) queries.push(Query.cursorAfter(opts.cursor));
|
|
return queries;
|
|
}
|
|
|
|
/** Eine Seite der Logos. `nextCursor` ist die ID des letzten Logos, solange die Seite voll war, sonst null. */
|
|
export async function listLogos(opts: LogoListOptions): Promise<{ logos: CommunityLogo[]; nextCursor: string | null }> {
|
|
const list = await tables.listRows<TableRow>({ databaseId: IDS.database, tableId: IDS.logos, queries: logoQueries(opts) });
|
|
const logos = list.rows.map((row) => toLogo(row));
|
|
return { logos, nextCursor: logos.length === LOGO_PAGE_SIZE ? logos[logos.length - 1].id : null };
|
|
}
|
|
|
|
/**
|
|
* Ein Logo per ID. Gibt es das Logo nicht oder ist es für mich ausgeblendet oder gelöscht (Appwrite antwortet beides mit 404),
|
|
* ist das Ergebnis null. Eine ID, die keine Appwrite-ID sein kann, ergibt null ohne Anfrage.
|
|
*/
|
|
export async function getLogo(id: string): Promise<CommunityLogo | null> {
|
|
if (!ID_PATTERN.test(id)) return null;
|
|
try {
|
|
return toLogo(await tables.getRow<TableRow>({ databaseId: IDS.database, tableId: IDS.logos, rowId: id }));
|
|
} catch (err) {
|
|
if (isRowMissing(err)) return null;
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
/** Von diesen Logos (einer Seite, höchstens 100) die, die ich mit einem Herz markiert habe; Gäste und Nutzer ohne Herzen: leer. */
|
|
export async function myHearts(userId: string, logoIds: readonly string[]): Promise<Set<string>> {
|
|
const hearts = new Set<string>();
|
|
if (!userId || logoIds.length === 0) return hearts;
|
|
const list = await tables.listRows<TableRow>({
|
|
databaseId: IDS.database,
|
|
tableId: IDS.logoLikes,
|
|
queries: [Query.equal('userId', userId), Query.equal('logoId', [...logoIds]), Query.limit(LIST_MAX)],
|
|
});
|
|
for (const row of list.rows) {
|
|
const logoId = text(row.logoId);
|
|
if (logoId) hearts.add(logoId);
|
|
}
|
|
return hearts;
|
|
}
|
|
|
|
/** Admin: die Meldungen eines Logos, neueste zuerst. Der Melder-Hash bleibt außen vor. */
|
|
export async function listLogoReports(logoId: string): Promise<LogoReport[]> {
|
|
if (!ID_PATTERN.test(logoId)) return [];
|
|
const list = await tables.listRows<TableRow>({
|
|
databaseId: IDS.database,
|
|
tableId: IDS.logoReports,
|
|
queries: [Query.equal('logoId', logoId), Query.orderDesc('$createdAt'), Query.limit(LIST_MAX)],
|
|
});
|
|
return list.rows.map((row) => ({
|
|
id: text(row.$id),
|
|
logoId: text(row.logoId),
|
|
reason: text(row.reason),
|
|
createdAt: text(row.$createdAt),
|
|
}));
|
|
}
|
|
|
|
/** Lädt die PNG-Datei eines Logos aus dem Logo-Bucket. */
|
|
export async function loadLogoBlob(logo: CommunityLogo): Promise<Blob> {
|
|
// Die ID landet im Pfad der Anfrage; was keine Appwrite-ID sein kann, gibt es nicht.
|
|
if (!ID_PATTERN.test(logo.fileId)) throw new AppwriteException('File not found', 404, 'storage_file_not_found', '');
|
|
return fetchFile(IDS.logoBucket, logo.fileId);
|
|
}
|