From 75ca53ebafbbe175dbcf561569a95ff50c8b4672 Mon Sep 17 00:00:00 2001 From: Chip Wasson Date: Fri, 8 Aug 2025 11:38:29 -0600 Subject: [PATCH] Add baby name cards --- src/baby-names/baby-names.controller.ts | 105 +++++++++++++++++++++--- src/baby-names/baby-names.service.ts | 93 +++++++++++++++++++-- 2 files changed, 182 insertions(+), 16 deletions(-) diff --git a/src/baby-names/baby-names.controller.ts b/src/baby-names/baby-names.controller.ts index 1af5d3a..a1cb8ee 100644 --- a/src/baby-names/baby-names.controller.ts +++ b/src/baby-names/baby-names.controller.ts @@ -2,14 +2,17 @@ import { Body, Controller, Get, + Param, Post, + Put, + Query, Redirect, Render, Req, Res, } from '@nestjs/common'; import { ApiConsumes, ApiTags } from '@nestjs/swagger'; -import { BabyNamesService } from './baby-names.service'; +import { BabyNamesService, Cards } from './baby-names.service'; import { Request, Response } from 'express'; @Controller('baby-names') @@ -31,20 +34,44 @@ export class BabyNamesController { if (!key) { 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 { - key: key, - name, - index: currentIndex, + ...(await this.babyNamesService.getUserCurrentName(key as string)), 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() @ApiConsumes('multipart/form-data') @Redirect('/baby-names', 302) @@ -89,4 +116,62 @@ export class BabyNamesController { 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); + } } diff --git a/src/baby-names/baby-names.service.ts b/src/baby-names/baby-names.service.ts index c2aa689..c8a9722 100644 --- a/src/baby-names/baby-names.service.ts +++ b/src/baby-names/baby-names.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; import axios from 'axios'; +import { uniq } from 'ramda'; import { KvService } from 'src/kv/kv.service'; import { MinioService } from 'src/minio/minio.service'; @@ -10,6 +11,16 @@ export interface Name { count: number; } +export interface Cards { + green: string[]; + red: string[]; + wild: string[]; +} + +export interface CardsWithHistory extends Cards { + removed?: Cards; +} + export interface NameMap { [key: string]: Name[]; } @@ -68,7 +79,7 @@ export class BabyNamesService { const currentKey = await this.minioService .getBuffer( this.minioService.defaultBucketName, - `baby-names/${userKey}-current`, + `baby-names/${userKey}-current.json`, ) .then((buffer) => buffer.toString()) .catch(() => null); @@ -77,22 +88,39 @@ export class BabyNamesService { } const currentNumber = parseInt(currentKey || '0', 10); - return currentNumber + 1; + return currentNumber; } public async writeUserNumber(userKey: string, number: number): Promise { await this.minioService.uploadBuffer( this.minioService.defaultBucketName, - `baby-names/${userKey}-current`, + `baby-names/${userKey}-current.json`, 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 { const scoresKey = await this.minioService .getBuffer( this.minioService.defaultBucketName, - `baby-names/${userKey}-scores`, + `baby-names/${userKey}-scores.json`, ) .then((buffer) => buffer.toString()) .catch(() => null); @@ -102,14 +130,44 @@ export class BabyNamesService { return JSON.parse(scoresKey); } + public async getUserCardsOrDefault( + userKey: string, + ): Promise { + 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 { + 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( userKey: string, scores: UserScoreMap, ): Promise { await this.minioService.uploadBuffer( this.minioService.defaultBucketName, - `baby-names/${userKey}-scores`, - Buffer.from(JSON.stringify(scores)), + `baby-names/${userKey}-scores.json`, + Buffer.from(JSON.stringify(scores, null, 2)), ); } @@ -131,3 +189,26 @@ export class BabyNamesService { 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)), + ]), + }, + }; +};