cc09ddbd2c
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 36s
CI / Playwright E2E tests (push) Successful in 1m18s
CI / Build and push Docker image (push) Successful in 41s
CI / Notify (push) Successful in 2s
188 lines
6.5 KiB
TypeScript
188 lines
6.5 KiB
TypeScript
import { useCallback, useEffect, useState } from "react";
|
|
import { Button, OverlayTrigger, Tooltip } from "react-bootstrap";
|
|
import { ToastContainer } from "react-toastify";
|
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
import { faThumbsUp, faThumbsDown, faTrash, faPlus, faGear } from "@fortawesome/free-solid-svg-icons";
|
|
import Header from "../components/Header";
|
|
import Footer from "../components/Footer";
|
|
import Loader from "../components/Loader";
|
|
import { useAuth } from "../context/auth";
|
|
import Login from "../Login";
|
|
import AddSuggestionModal from "../components/modals/AddSuggestionModal";
|
|
import SuggestionDetailModal from "../components/modals/SuggestionDetailModal";
|
|
import {
|
|
Suggestion,
|
|
VoteDirection,
|
|
listSuggestions,
|
|
addSuggestion,
|
|
voteSuggestion,
|
|
deleteSuggestion,
|
|
} from "../../../types";
|
|
import "./SuggestionsPage.scss";
|
|
|
|
export default function SuggestionsPage() {
|
|
const auth = useAuth();
|
|
const [suggestions, setSuggestions] = useState<Suggestion[]>();
|
|
const [addModalOpen, setAddModalOpen] = useState(false);
|
|
const [detail, setDetail] = useState<Suggestion>();
|
|
|
|
const reload = useCallback(async () => {
|
|
if (!auth?.login) return;
|
|
const response = await listSuggestions();
|
|
setSuggestions(response.data ?? []);
|
|
}, [auth?.login]);
|
|
|
|
useEffect(() => {
|
|
reload();
|
|
}, [reload]);
|
|
|
|
const handleAdd = async (title: string, description: string) => {
|
|
const response = await addSuggestion({ body: { title, description } });
|
|
if (response.data) {
|
|
setSuggestions(response.data);
|
|
}
|
|
};
|
|
|
|
const handleVote = async (id: string, direction: VoteDirection) => {
|
|
const response = await voteSuggestion({ body: { id, direction } });
|
|
if (response.data) {
|
|
setSuggestions(response.data);
|
|
}
|
|
};
|
|
|
|
const handleDelete = async (suggestion: Suggestion) => {
|
|
if (!window.confirm(`Opravdu chcete smazat návrh „${suggestion.title}“? Smažou se i všechny jeho hlasy.`)) {
|
|
return;
|
|
}
|
|
const response = await deleteSuggestion({ body: { id: suggestion.id } });
|
|
if (response.data) {
|
|
setSuggestions(response.data);
|
|
}
|
|
};
|
|
|
|
// Vykreslí jeden řádek tabulky. Vyřešené návrhy jsou read-only (bez hlasování),
|
|
// ale autor je stále může smazat.
|
|
const renderRow = (suggestion: Suggestion) => (
|
|
<OverlayTrigger
|
|
key={suggestion.id}
|
|
placement="top"
|
|
overlay={<Tooltip id={`tooltip-${suggestion.id}`}>{suggestion.description}</Tooltip>}
|
|
>
|
|
<tr onClick={() => setDetail(suggestion)}>
|
|
<td>{suggestion.author}</td>
|
|
<td>{suggestion.title}</td>
|
|
<td className="col-score">{suggestion.voteScore}</td>
|
|
<td className="col-actions" onClick={e => e.stopPropagation()}>
|
|
{!suggestion.resolved && (
|
|
<>
|
|
<button
|
|
type="button"
|
|
className={`vote-btn vote-up ${suggestion.myVote === VoteDirection.UP ? "active" : ""}`}
|
|
title="Hlasovat pro"
|
|
onClick={() => handleVote(suggestion.id, VoteDirection.UP)}
|
|
>
|
|
<FontAwesomeIcon icon={faThumbsUp} />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className={`vote-btn vote-down ${suggestion.myVote === VoteDirection.DOWN ? "active" : ""}`}
|
|
title="Hlasovat proti"
|
|
onClick={() => handleVote(suggestion.id, VoteDirection.DOWN)}
|
|
>
|
|
<FontAwesomeIcon icon={faThumbsDown} />
|
|
</button>
|
|
</>
|
|
)}
|
|
{suggestion.isMine && (
|
|
<button
|
|
type="button"
|
|
className="vote-btn delete-btn"
|
|
title="Smazat návrh"
|
|
onClick={() => handleDelete(suggestion)}
|
|
>
|
|
<FontAwesomeIcon icon={faTrash} />
|
|
</button>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
</OverlayTrigger>
|
|
);
|
|
|
|
if (!auth?.login) {
|
|
return <Login />;
|
|
}
|
|
|
|
if (!suggestions) {
|
|
return <Loader icon={faGear} description={"Načítám návrhy..."} animation={"fa-bounce"} />;
|
|
}
|
|
|
|
const activeSuggestions = suggestions.filter(s => !s.resolved);
|
|
const resolvedSuggestions = suggestions.filter(s => s.resolved);
|
|
|
|
return (
|
|
<>
|
|
<Header />
|
|
<div className="suggestions-page">
|
|
<div className="suggestions-header">
|
|
<h1>Návrhy na vylepšení</h1>
|
|
<Button onClick={() => setAddModalOpen(true)}>
|
|
<FontAwesomeIcon icon={faPlus} /> Přidat návrh
|
|
</Button>
|
|
</div>
|
|
<p className="suggestions-info">
|
|
Zde můžete navrhovat vylepšení aplikace a hlasovat o návrzích ostatních. U každého návrhu je
|
|
zobrazeno jméno navrhovatele. Jména hlasujících jsou dostupná pouze administrátorům.
|
|
</p>
|
|
|
|
{suggestions.length === 0 ? (
|
|
<p className="suggestions-empty">Zatím nebyly přidány žádné návrhy. Buďte první!</p>
|
|
) : (
|
|
<>
|
|
{activeSuggestions.length > 0 && (
|
|
<table className="suggestions-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Navrhovatel</th>
|
|
<th>Název</th>
|
|
<th className="col-score">Hlasy</th>
|
|
<th className="col-actions">Akce</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{activeSuggestions.map(renderRow)}
|
|
</tbody>
|
|
</table>
|
|
)}
|
|
|
|
{resolvedSuggestions.length > 0 && (
|
|
<div className="resolved-section">
|
|
<h2>Vyřešené návrhy</h2>
|
|
<p className="suggestions-info">
|
|
Tyto návrhy již byly zapracovány. Nelze pro ně hlasovat, autor je však může odstranit.
|
|
</p>
|
|
<table className="suggestions-table resolved">
|
|
<thead>
|
|
<tr>
|
|
<th>Navrhovatel</th>
|
|
<th>Název</th>
|
|
<th className="col-score">Hlasy</th>
|
|
<th className="col-actions">Akce</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{resolvedSuggestions.map(renderRow)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
<Footer />
|
|
<AddSuggestionModal isOpen={addModalOpen} onClose={() => setAddModalOpen(false)} onSubmit={handleAdd} />
|
|
<SuggestionDetailModal suggestion={detail} onClose={() => setDetail(undefined)} />
|
|
<ToastContainer />
|
|
</>
|
|
);
|
|
}
|