|
import {Topic, topicsCtx} from "@/contexts/topics"; |
|
import {Layout} from "../layout"; |
|
import {Spinner} from "../spinner"; |
|
import style from "./style.module.scss"; |
|
import {Button} from "../button"; |
|
import {Input} from "../input"; |
|
import {Clipboard, Trash2} from "preact-feather"; |
|
import {useContext, useEffect, useMemo, useRef, useState} from "preact/hooks"; |
|
import {frenchToIso8601, iso8601ToFrench} from "@/utils/dates"; |
|
import {FormGroup} from "../form"; |
|
import {Slider} from "../slider"; |
|
import cn from "classnames"; |
|
import {routeCtx, routes} from "@/contexts/route"; |
|
import {settingsCtx} from "@/contexts/settings"; |
|
import {Wysiwyg} from "@/components/wysiwyg"; |
|
import {Alert} from "@/components/alert"; |
|
import {logCtx} from "@/contexts/log"; |
|
import {Pagination} from "@/components/pagination"; |
|
|
|
const topicsPerPage = 10; |
|
|
|
export function Topics(props: { |
|
page: number, |
|
setPage: (newPage: number) => void, |
|
}) { |
|
const [importContent, setImportContent] = useState(""); |
|
const [topicsContext, setTopicsContext, topicsActions] = useContext(topicsCtx); |
|
const [, , logActions] = useContext(logCtx); |
|
const [settings, setSettings] = useContext(settingsCtx); |
|
|
|
useEffect(() => { |
|
if((topicsPerPage * props.page) >= topicsContext.topics.length) { |
|
props.setPage(Math.ceil(topicsContext.topics.length / topicsPerPage)); |
|
} |
|
}, [topicsContext.topics.length]) |
|
|
|
const sortedTopics = useMemo(() => { |
|
return [...topicsContext.topics].sort((topicA, topicB) => { |
|
if (topicA.posts.length < 1 || topicB.posts.length < 1) { |
|
return 0; |
|
} |
|
return topicB.posts[topicB.posts.length - 1].date.localeCompare(topicA.posts[topicA.posts.length - 1].date); |
|
}); |
|
}, [topicsContext.topics]); |
|
|
|
const sortedTopicsSlice = useMemo( |
|
() => sortedTopics.slice(props.page * topicsPerPage, (props.page + 1) * topicsPerPage), |
|
[props.page, sortedTopics] |
|
); |
|
|
|
const pendingGeneration = topicsContext.generation === "pending"; |
|
|
|
return ( |
|
<div> |
|
<div className={style.listWrapper}> |
|
<Pagination |
|
pageCount={Math.ceil(sortedTopics.length / topicsPerPage)} |
|
page={props.page} |
|
setPage={props.setPage} |
|
/> |
|
<List topics={sortedTopicsSlice}/> |
|
<Pagination |
|
pageCount={Math.ceil(sortedTopics.length / topicsPerPage)} |
|
page={props.page} |
|
setPage={props.setPage} |
|
/> |
|
</div> |
|
<div> |
|
<h2>Nouveau sujet</h2> |
|
<Alert |
|
lines={ |
|
topicsContext.generation === "error" ? [ |
|
"Erreur lors de la génération du topic", |
|
"Veuillez regarder le log pour plus d'informations" |
|
] : [] |
|
} |
|
/> |
|
<div className={style.generationSettings}> |
|
<FormGroup> |
|
<label for="postCount">Nombre de posts</label> |
|
<Slider |
|
name="postCount" |
|
value={settings.postCount} |
|
onChange={(v) => setSettings({...settings, postCount: v})} |
|
min={1} |
|
max={10} |
|
step={1} |
|
/> |
|
</FormGroup> |
|
</div> |
|
<Button |
|
onClick={() => { |
|
topicsActions.generateTopic(settings, logActions.log) |
|
}} |
|
secondary={true} |
|
loading={pendingGeneration} |
|
> |
|
{pendingGeneration ? "Génération en cours…" : "Générer"} |
|
</Button> |
|
</div> |
|
<hr/> |
|
<div> |
|
<Wysiwyg |
|
showTitle={true} |
|
onSubmit={(user, title, text) => { |
|
// setLastAddedTopicId(topicsActions.addTopic(user, title, text)); |
|
topicsActions.addTopic(user, title, text) |
|
props.setPage(0); |
|
window.scrollTo({top: 0, behavior: 'smooth'}); |
|
}} |
|
disabled={pendingGeneration} |
|
/> |
|
</div> |
|
<hr/> |
|
<div> |
|
<Alert |
|
lines={ |
|
topicsContext.import === "error" ? [ |
|
"Erreur lors de l'importation du sujet", |
|
] : [] |
|
} |
|
/> |
|
<div className={style.import}> |
|
<textarea |
|
placeholder="Importer un sujet…" |
|
name="import" |
|
id="import" |
|
cols="30" |
|
rows="10" |
|
value={importContent} |
|
onInput={(e) => setImportContent((e.target as HTMLTextAreaElement).value)} |
|
/> |
|
</div> |
|
<Button |
|
onClick={() => { |
|
topicsActions.importTopic(importContent); |
|
setImportContent(""); |
|
}} |
|
secondary={true} |
|
> |
|
Importer |
|
</Button> |
|
</div> |
|
</div> |
|
) |
|
} |
|
|
|
function List(props: { |
|
topics: Topic[], |
|
// latestGeneratedTopicId: string | null, |
|
}) { |
|
return ( |
|
<ul className={style.list}> |
|
<li className={style.head}> |
|
<span>Sujet</span> |
|
<span>Auteur</span> |
|
<span>NB</span> |
|
<span>Dernier msg</span> |
|
<span></span> |
|
</li> |
|
{props.topics.length < 1 && <li><span>Aucun sujet</span><span/><span/><span/><span/></li>} |
|
{props.topics.map(topic => <TopicElement topic={topic}/>)} |
|
</ul> |
|
) |
|
} |
|
|
|
function TopicElement(props: { |
|
topic: Topic; |
|
}) { |
|
const topic = props.topic; |
|
const [, setRoute] = useContext(routeCtx); |
|
const [, , topicsActions] = useContext(topicsCtx); |
|
|
|
let isRecent: boolean = false; |
|
if (topic.posts.length > 0) { |
|
const firstPost = topic.posts[0]; |
|
const dateDelta = new Date() - new Date(firstPost.generationDate || firstPost.date); |
|
|
|
|
|
if (dateDelta < 3000) { |
|
isRecent = true; |
|
} |
|
} |
|
|
|
return ( |
|
<li className={cn({[style.highlight]: isRecent})}> |
|
<span> |
|
<a href={routes.topic(topic.id, 1).location} onClick={(e) => { |
|
e.preventDefault(); |
|
setRoute(routes.topic(topic.id, 1)); |
|
}}> |
|
{topic.title} |
|
</a> |
|
</span> |
|
<span>{topic.posts[0].user}</span> |
|
<span>{topic.posts.length}</span> |
|
<span>{iso8601ToFrench(topic.posts[topic.posts.length - 1].date)}</span> |
|
<span> |
|
<span title="Copier le sujet"> |
|
<Clipboard |
|
size={16} |
|
className={style.clipboard} |
|
onClick={() => { |
|
const json = JSON.stringify(topic); |
|
navigator.clipboard.writeText(json); |
|
}} |
|
/> |
|
</span> |
|
<span title="Supprimer le sujet"> |
|
<Trash2 |
|
size={16} |
|
className={style.trash} |
|
onClick={() => topicsActions.deleteTopic(topic.id)} |
|
/> |
|
</span> |
|
</span> |
|
</li> |
|
) |
|
} |