Add baby name cards

This commit is contained in:
2025-08-08 11:38:29 -06:00
parent aba378ab44
commit 75ca53ebaf
2 changed files with 182 additions and 16 deletions

View File

@@ -2,14 +2,17 @@ import {
Body, Body,
Controller, Controller,
Get, Get,
Param,
Post, Post,
Put,
Query,
Redirect, Redirect,
Render, Render,
Req, Req,
Res, Res,
} from '@nestjs/common'; } from '@nestjs/common';
import { ApiConsumes, ApiTags } from '@nestjs/swagger'; import { ApiConsumes, ApiTags } from '@nestjs/swagger';
import { BabyNamesService } from './baby-names.service'; import { BabyNamesService, Cards } from './baby-names.service';
import { Request, Response } from 'express'; import { Request, Response } from 'express';
@Controller('baby-names') @Controller('baby-names')
@@ -31,20 +34,44 @@ export class BabyNamesController {
if (!key) { if (!key) {
throw new Error("Missing 'key' field"); throw new Error("Missing 'key' field");
} }
const currentIndex = await this.babyNamesService.getCurrentNumber(
key as string,
);
const name = this.babyNamesService.nameList[currentIndex];
const synonyms = this.babyNamesService.getSynonyms(name);
return { return {
key: key, ...(await this.babyNamesService.getUserCurrentName(key as string)),
name,
index: currentIndex,
message: request.query.message || null, message: request.query.message || null,
synonyms: synonyms.join(', '),
}; };
} }
@Post('.json')
@ApiConsumes('application/json')
async submitJson(
@Body()
body: {
key: string;
name: string;
nameindex: number;
opinion: number;
pronunciation: number;
spelling: number;
comment: string;
},
@Res() res: Response,
) {
const { key, name, opinion, nameindex, pronunciation, spelling, comment } =
body;
if (!key) {
res.status(400).json({ error: "Missing 'key' field" });
return;
}
await this.babyNamesService.addUserScore(key, name, {
opinion,
pronunciation,
spelling,
comment: comment || '',
});
await this.babyNamesService.writeUserNumber(key, nameindex + 1);
res.cookie('baby-names-key', key, { maxAge: 30 * 24 * 60 * 60 * 1000 }); // 30 days
res.json({ status: 'ok', nextIndex: nameindex + 1 });
}
@Post() @Post()
@ApiConsumes('multipart/form-data') @ApiConsumes('multipart/form-data')
@Redirect('/baby-names', 302) @Redirect('/baby-names', 302)
@@ -89,4 +116,62 @@ export class BabyNamesController {
nameCount: this.babyNamesService.nameCountMap, nameCount: this.babyNamesService.nameCountMap,
}; };
} }
@Get('current-index.json')
async getCurrentIndex(@Query('key') key: string, @Res() res: Response) {
if (!key) {
res.status(400).json({ error: "Missing 'key' field" });
return;
}
const currentIndex = await this.babyNamesService.getCurrentNumber(key);
res.json({ currentIndex });
}
@Get('data/:index.json')
async getNameByIndex(@Req() request: Request, @Res() res: Response) {
const indexStr = request.params.index;
const index = parseInt(indexStr, 10);
if (
isNaN(index) ||
index < 0 ||
index >= this.babyNamesService.nameList.length
) {
res.status(400).json({ error: 'Invalid index' });
return;
}
const name = this.babyNamesService.nameList[index];
const synonyms = this.babyNamesService.getSynonyms(name);
res.json({
index,
name,
synonyms,
count: this.babyNamesService.nameCountMap[name] || 0,
});
}
@Get('cards/:key.json')
async getUserCards(@Req() request: Request, @Res() res: Response) {
const key = request.params.key;
if (!key) {
res.status(400).json({ error: "Missing 'key' field" });
return;
}
const cards = await this.babyNamesService.getUserCardsOrDefault(key);
res.json(cards);
}
@Post('cards/:key.json')
async updateUserCards(
@Req() request: Request,
@Res() res: Response,
@Body() body: Cards,
) {
const key = request.params.key;
if (!key) {
res.status(400).json({ error: "Missing 'key' field" });
return;
}
const cards = await this.babyNamesService.saveUserCards(key, body);
res.json(cards);
}
} }

View File

@@ -1,5 +1,6 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import axios from 'axios'; import axios from 'axios';
import { uniq } from 'ramda';
import { KvService } from 'src/kv/kv.service'; import { KvService } from 'src/kv/kv.service';
import { MinioService } from 'src/minio/minio.service'; import { MinioService } from 'src/minio/minio.service';
@@ -10,6 +11,16 @@ export interface Name {
count: number; count: number;
} }
export interface Cards {
green: string[];
red: string[];
wild: string[];
}
export interface CardsWithHistory extends Cards {
removed?: Cards;
}
export interface NameMap { export interface NameMap {
[key: string]: Name[]; [key: string]: Name[];
} }
@@ -68,7 +79,7 @@ export class BabyNamesService {
const currentKey = await this.minioService const currentKey = await this.minioService
.getBuffer( .getBuffer(
this.minioService.defaultBucketName, this.minioService.defaultBucketName,
`baby-names/${userKey}-current`, `baby-names/${userKey}-current.json`,
) )
.then((buffer) => buffer.toString()) .then((buffer) => buffer.toString())
.catch(() => null); .catch(() => null);
@@ -77,22 +88,39 @@ export class BabyNamesService {
} }
const currentNumber = parseInt(currentKey || '0', 10); const currentNumber = parseInt(currentKey || '0', 10);
return currentNumber + 1; return currentNumber;
} }
public async writeUserNumber(userKey: string, number: number): Promise<void> { public async writeUserNumber(userKey: string, number: number): Promise<void> {
await this.minioService.uploadBuffer( await this.minioService.uploadBuffer(
this.minioService.defaultBucketName, this.minioService.defaultBucketName,
`baby-names/${userKey}-current`, `baby-names/${userKey}-current.json`,
Buffer.from(number.toString()), Buffer.from(number.toString()),
); );
} }
public async getUserCurrentName(userKey: string): Promise<{
key: string;
name: string;
index: number;
synonyms: string[];
}> {
const currentIndex = await this.getCurrentNumber(userKey);
const name = this.nameList[currentIndex];
const synonyms = this.getSynonyms(name);
return {
key: userKey,
name,
index: currentIndex,
synonyms: synonyms,
};
}
public async getUserScores(userKey: string): Promise<UserScoreMap> { public async getUserScores(userKey: string): Promise<UserScoreMap> {
const scoresKey = await this.minioService const scoresKey = await this.minioService
.getBuffer( .getBuffer(
this.minioService.defaultBucketName, this.minioService.defaultBucketName,
`baby-names/${userKey}-scores`, `baby-names/${userKey}-scores.json`,
) )
.then((buffer) => buffer.toString()) .then((buffer) => buffer.toString())
.catch(() => null); .catch(() => null);
@@ -102,14 +130,44 @@ export class BabyNamesService {
return JSON.parse(scoresKey); return JSON.parse(scoresKey);
} }
public async getUserCardsOrDefault(
userKey: string,
): Promise<CardsWithHistory> {
const cardsKey = await this.minioService
.getBuffer(
this.minioService.defaultBucketName,
`baby-names/${userKey}-cards.json`,
)
.then((buffer) => buffer.toString())
.catch(() => null);
if (cardsKey === null) {
return { green: [], red: [], wild: [] };
}
return JSON.parse(cardsKey);
}
public async saveUserCards(
userKey: string,
cards: Cards,
): Promise<CardsWithHistory> {
const existingCards = await this.getUserCardsOrDefault(userKey);
const updatedCards = resolveRemovedCards(existingCards, cards);
await this.minioService.uploadBuffer(
this.minioService.defaultBucketName,
`baby-names/${userKey}-cards.json`,
Buffer.from(JSON.stringify(updatedCards, null, 2)),
);
return updatedCards;
}
public async saveUserScores( public async saveUserScores(
userKey: string, userKey: string,
scores: UserScoreMap, scores: UserScoreMap,
): Promise<void> { ): Promise<void> {
await this.minioService.uploadBuffer( await this.minioService.uploadBuffer(
this.minioService.defaultBucketName, this.minioService.defaultBucketName,
`baby-names/${userKey}-scores`, `baby-names/${userKey}-scores.json`,
Buffer.from(JSON.stringify(scores)), Buffer.from(JSON.stringify(scores, null, 2)),
); );
} }
@@ -131,3 +189,26 @@ export class BabyNamesService {
return []; return [];
} }
} }
const resolveRemovedCards = (
existingCards: CardsWithHistory,
newCards: Cards,
): CardsWithHistory => {
return {
...newCards,
removed: {
green: uniq([
...(existingCards.removed?.green || []),
...existingCards.green.filter((name) => !newCards.green.includes(name)),
]),
red: uniq([
...(existingCards.removed?.red || []),
...existingCards.red.filter((name) => !newCards.red.includes(name)),
]),
wild: uniq([
...(existingCards.removed?.wild || []),
...existingCards.wild.filter((name) => !newCards.wild.includes(name)),
]),
},
};
};