feat: nová stránka pro návrhy na vylepšení
CI / Generate TypeScript types (push) Successful in 10s
CI / Server unit tests (push) Successful in 21s
CI / Build server (push) Successful in 24s
CI / Build client (push) Successful in 38s
CI / Playwright E2E tests (push) Successful in 1m18s
CI / Build and push Docker image (push) Successful in 40s
CI / Notify (push) Successful in 2s
CI / Generate TypeScript types (push) Successful in 10s
CI / Server unit tests (push) Successful in 21s
CI / Build server (push) Successful in 24s
CI / Build client (push) Successful in 38s
CI / Playwright E2E tests (push) Successful in 1m18s
CI / Build and push Docker image (push) Successful in 40s
CI / Notify (push) Successful in 2s
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
import crypto from "crypto";
|
||||
import { Suggestion, VoteDirection } from "../../types/gen/types.gen";
|
||||
import getStorage from "./storage";
|
||||
|
||||
/** Interní reprezentace návrhu uložená ve storage (včetně seznamů hlasujících). */
|
||||
interface StoredSuggestion {
|
||||
id: string;
|
||||
author: string;
|
||||
title: string;
|
||||
description: string;
|
||||
createdAt: string;
|
||||
/** Loginy uživatelů hlasujících PRO návrh */
|
||||
upvoters: string[];
|
||||
/** Loginy uživatelů hlasujících PROTI návrhu */
|
||||
downvoters: string[];
|
||||
}
|
||||
|
||||
const storage = getStorage();
|
||||
const STORAGE_KEY = 'suggestions';
|
||||
|
||||
/** Načte interní seznam návrhů ze storage. */
|
||||
async function loadSuggestions(): Promise<StoredSuggestion[]> {
|
||||
return (await storage.getData<StoredSuggestion[]>(STORAGE_KEY)) ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Převede interní návrh na DTO pro daného uživatele - skryje seznamy hlasujících
|
||||
* a doplní hlas přihlášeného uživatele a příznak vlastnictví.
|
||||
*/
|
||||
function toDto(suggestion: StoredSuggestion, login: string): Suggestion {
|
||||
let myVote: VoteDirection | undefined;
|
||||
if (suggestion.upvoters.includes(login)) {
|
||||
myVote = 'up';
|
||||
} else if (suggestion.downvoters.includes(login)) {
|
||||
myVote = 'down';
|
||||
}
|
||||
return {
|
||||
id: suggestion.id,
|
||||
author: suggestion.author,
|
||||
title: suggestion.title,
|
||||
description: suggestion.description,
|
||||
voteScore: suggestion.upvoters.length - suggestion.downvoters.length,
|
||||
myVote,
|
||||
isMine: suggestion.author === login,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Vrátí seznam návrhů jako DTO pro daného uživatele, seřazený sestupně dle skóre
|
||||
* (při shodě skóre stabilně dle data vytvoření vzestupně).
|
||||
*
|
||||
* @param login login přihlášeného uživatele
|
||||
*/
|
||||
export async function listSuggestions(login: string): Promise<Suggestion[]> {
|
||||
const suggestions = await loadSuggestions();
|
||||
return suggestions
|
||||
.map(s => toDto(s, login))
|
||||
.sort((a, b) => b.voteScore - a.voteScore);
|
||||
}
|
||||
|
||||
/**
|
||||
* Přidá nový návrh. Autorovi se automaticky nastaví hlas pro.
|
||||
*
|
||||
* @param login login autora
|
||||
* @param title název návrhu
|
||||
* @param description detailní popis návrhu
|
||||
* @returns aktualizovaný seznam návrhů jako DTO
|
||||
*/
|
||||
export async function addSuggestion(login: string, title: string, description: string): Promise<Suggestion[]> {
|
||||
const trimmedTitle = title?.trim();
|
||||
const trimmedDescription = description?.trim();
|
||||
if (!trimmedTitle) {
|
||||
throw new Error('Název návrhu nesmí být prázdný');
|
||||
}
|
||||
if (!trimmedDescription) {
|
||||
throw new Error('Popis návrhu nesmí být prázdný');
|
||||
}
|
||||
const suggestions = await loadSuggestions();
|
||||
suggestions.push({
|
||||
id: crypto.randomUUID(),
|
||||
author: login,
|
||||
title: trimmedTitle,
|
||||
description: trimmedDescription,
|
||||
createdAt: new Date().toISOString(),
|
||||
// Autor automaticky hlasuje pro svůj návrh
|
||||
upvoters: [login],
|
||||
downvoters: [],
|
||||
});
|
||||
await storage.setData(STORAGE_KEY, suggestions);
|
||||
return listSuggestions(login);
|
||||
}
|
||||
|
||||
/**
|
||||
* Přepne hlas uživatele u návrhu. Klik na již aktivní směr hlas zruší,
|
||||
* opačný směr stávající hlas přepíše.
|
||||
*
|
||||
* @param login login hlasujícího uživatele
|
||||
* @param id identifikátor návrhu
|
||||
* @param direction směr hlasu, na který uživatel klikl
|
||||
* @returns aktualizovaný seznam návrhů jako DTO
|
||||
*/
|
||||
export async function voteSuggestion(login: string, id: string, direction: VoteDirection): Promise<Suggestion[]> {
|
||||
const suggestions = await loadSuggestions();
|
||||
const suggestion = suggestions.find(s => s.id === id);
|
||||
if (!suggestion) {
|
||||
throw new Error('Návrh nebyl nalezen');
|
||||
}
|
||||
const hadUp = suggestion.upvoters.includes(login);
|
||||
const hadDown = suggestion.downvoters.includes(login);
|
||||
// Nejprve odebereme případný stávající hlas uživatele
|
||||
suggestion.upvoters = suggestion.upvoters.filter(l => l !== login);
|
||||
suggestion.downvoters = suggestion.downvoters.filter(l => l !== login);
|
||||
// Hlas přidáme pouze pokud uživatel neklikl na již aktivní směr (jinak ho jen zrušíme)
|
||||
if (direction === 'up' && !hadUp) {
|
||||
suggestion.upvoters.push(login);
|
||||
} else if (direction === 'down' && !hadDown) {
|
||||
suggestion.downvoters.push(login);
|
||||
}
|
||||
await storage.setData(STORAGE_KEY, suggestions);
|
||||
return listSuggestions(login);
|
||||
}
|
||||
|
||||
/**
|
||||
* Smaže návrh včetně všech jeho hlasů. Smazat lze pouze vlastní návrh.
|
||||
*
|
||||
* @param login login uživatele požadujícího smazání
|
||||
* @param id identifikátor návrhu ke smazání
|
||||
* @returns aktualizovaný seznam návrhů jako DTO
|
||||
*/
|
||||
export async function deleteSuggestion(login: string, id: string): Promise<Suggestion[]> {
|
||||
const suggestions = await loadSuggestions();
|
||||
const suggestion = suggestions.find(s => s.id === id);
|
||||
if (!suggestion) {
|
||||
throw new Error('Návrh nebyl nalezen');
|
||||
}
|
||||
if (suggestion.author !== login) {
|
||||
throw new Error('Smazat lze pouze vlastní návrh');
|
||||
}
|
||||
const filtered = suggestions.filter(s => s.id !== id);
|
||||
await storage.setData(STORAGE_KEY, filtered);
|
||||
return listSuggestions(login);
|
||||
}
|
||||
Reference in New Issue
Block a user