added history view

This commit is contained in:
Shoofle 2024-10-06 16:01:13 -04:00
parent 13fb4db1d1
commit 090ded9e85
9 changed files with 203 additions and 173 deletions

View File

@ -1,11 +1,12 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Landing from '/src/landing/Landing.jsx';
import PageView from '/src/page/PageView.jsx';
import PageEdit from '/src/page/PageEdit.jsx';
import Page from '/src/page/Page.jsx';
import HistoryView from '/src/page/HistoryView.jsx';
import LogIn from '/src/login/LogIn.jsx';
import Register from '/src/login/Register.jsx';
import Profile from '/src/login/Profile.jsx';
import AuthProvider from '/src/AuthProvider.jsx';
import './App.css';
@ -21,8 +22,10 @@ function App() {
<Route path="/" element={<Landing/>}/>
<Route path="/login" element={<LogIn/>}/>
<Route path="/register" element={<Register/>}/>
<Route path="/:pagenumber" element={<PageView/>}/>
<Route path="/:pagenumber/edit" element={<PageEdit/>}/>
<Route path="/:pagenumber" element={<Page/>}/>
<Route path="/:pagenumber/edit" element={<Page editing="true"/>}/>
<Route path="/:pagenumber/history" element={<HistoryView/>}/>
<Route path="/:pagenumber/:editid" element={<Page/>}/>
<Route path="/profile" element={<Profile/>}/>
</Routes>
</BrowserRouter>

View File

@ -27,47 +27,43 @@ const defaults = {
};
export async function postNewPage() {
return shoofetch(`${apiUrl}/page/new`, {
method: 'POST',
})
return shoofetch(`${apiUrl}/page/new`, {method: 'POST'})
}
export async function fetchPageList() {
return shoofetch(`${apiUrl}/pages`, {method: 'GET'});
}
export async function fetchPage(id) {
return fetch(`${apiUrl}/page/${id}`, {
method: 'GET',
...defaults
}).then((res) => res.json())
export async function fetchPage(number) {
return shoofetch(`${apiUrl}/page/${number}`, {method: 'GET'});
}
export async function fetchPageHistory(number) {
return shoofetch(`${apiUrl}/page/${number}/history`, {method: 'GET'});
}
export async function fetchPageAtEdit(number, id) {
return shoofetch(`${apiUrl}/page/${number}/${id}`, {method: 'GET'});
}
export async function postPage({id, title, description}) {
return shoofetch(`${apiUrl}/page/${id}`, {
method: 'POST',
body: JSON.stringify({id: id, title: title, description: description}),
...defaults
})
}
export async function deletePage(id) {
return fetch(`${apiUrl}/page/${id}`, {
method: 'DELETE',
...defaults
})
return shoofetch(`${apiUrl}/page/${id}`, {method: 'DELETE'});
}
export async function fetchGraph() {
return fetch(`${apiUrl}/graph`, {
method: 'GET',
...defaults
}).then((res) => res.json())
return shoofetch(`${apiUrl}/graph`, {method: 'GET'})
.then((serialized) => {
const graph = new Graph();
graph.import(serialized);
return graph;
})
});
}
export async function createAccount({name, password, nonce}) {

View File

@ -30,6 +30,7 @@ function Profile() {
<label>
Id:
<input {...register("id")} value={id} disabled/>
<br/>
</label>
<label>
User name:

View File

@ -0,0 +1,49 @@
import { useQuery } from '@tanstack/react-query';
import { useParams, useNavigate } from 'react-router-dom';
import { fetchPageHistory } from '../apiTools.jsx';
import { useFixLinks } from '../clientStuff.jsx';
import './Pages.css';
function HistoryView() {
const noLoad = useFixLinks();
const { pagenumber, editid } = useParams();
const navigate = useNavigate();
const { isPending, error, data } = useQuery({ // fetch the currrent values
queryKey: ['page history', pagenumber],
queryFn: () => fetchPageHistory(pagenumber)
})
const edits = data;
const ready = !(error || isPending);
const pageTitle = edits ? edits[0]?.title : "[no title]";
return (
<div className="main-column">
<div>
<header>
<h1><a href="/" {...noLoad}>🌳</a>{pagenumber}. {ready ? pageTitle : "..."}</h1>
<hr/>
</header>
<section className="page-contents">
{ ready ?
<ol>
{ edits.map(({id, number, title, time, author}) =>
<li key={id}>
<a href={`/${number}/${id}`} {...noLoad}>{id} : "{title}", {time} by user #{author}</a>
</li>
) }
</ol>
:
(isPending ? "Loading..." : JSON.stringify(error))
}
</section>
<button onClick={() => navigate(`/${pagenumber}`)}>Return to page</button>
</div>
</div>
);
}
export default HistoryView;

116
client/src/page/Page.jsx Normal file
View File

@ -0,0 +1,116 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useParams, useNavigate } from 'react-router-dom';
import { apiUrl, fetchPage, fetchPageAtEdit, postPage, deletePage } from '../apiTools.jsx';
import { useFixLinks } from '../clientStuff.jsx';
import { useLoggedIn } from '../AuthProvider.jsx';
import './Pages.css';
function Page({ editing }) {
const queryClient = useQueryClient();
const navigate = useNavigate();
const { pagenumber, editid } = useParams();
const loggedIn = useLoggedIn();
const noLoad = useFixLinks();
const fetchQuery = useQuery({ // fetch the currrent values
queryKey: ['page', pagenumber, editid],
queryFn: () => editid ? fetchPageAtEdit(pagenumber, editid) : fetchPage(pagenumber)
})
const postMutation = useMutation({ // for changing the value when we're done with it
mutationFn: ({id, title, description}) => postPage({id, title, description}),
onSettled: async (data, error, variables) => {
// Invalidate and refetch
await queryClient.invalidateQueries({ queryKey: ['page', variables.id, undefined] })
},
});
const deleteMutation = useMutation({ // for changing the value when we're done with it
mutationFn: (id) => deletePage(id),
onSettled: async (data, error, variables) => {
// Invalidate and refetch
await queryClient.invalidateQueries({ queryKey: ['pages'] })
},
});
const readyToShow = !(fetchQuery.error || fetchQuery.isPending);
let {id, title, description, html, time, author} = fetchQuery.data || {};
if (!title) title = "[no title]";
if (!html) html = "[body missing]";
if (!description) description = "[body missing]";
function submitChanges(e) {
const newTitle = document.querySelector('span').innerHTML;
const newText = document.querySelector('pre').innerHTML;
postMutation.mutate({
id: pagenumber,
title: newTitle,
description: newText
});
navigate(`/${pagenumber}`, {replace: true})
}
function submitDelete(e) {
e.preventDefault();
deleteMutation.mutate(pagenumber);
navigate(`/`);
}
return (
<div className="main-column">
<header>
<h1>
<a href="/" {...noLoad}>🌳</a>
{pagenumber}.
<span
contentEditable={editing}
dangerouslySetInnerHTML={{__html: readyToShow ? title : "..." }} />
</h1>
{readyToShow && editid && `saved at ${time} by user #${author}`}
<hr/>
</header>
<section className="page-contents">
{ readyToShow ?
(
editing ?
<pre
contentEditable="true"
dangerouslySetInnerHTML={{__html: description}} />
:
<div
dangerouslySetInnerHTML={{__html: html}}
{...noLoad} />
)
:
"..."
}
</section>
<button
onClick={() => navigate(`/${pagenumber}/history`)}>
History
</button>
{editing && (
<button
disabled={postMutation.isPending}
onClick={submitChanges}>
{postMutation.isPending ? "Updating..." : "Update"}
</button>)}
{!editing && !editid && (
<button
disabled={!loggedIn}
onClick={() => navigate(`/${pagenumber}/edit`)}>
Edit Page
</button>)}
{loggedIn && (
<button
disabled={deleteMutation.isPending}
onClick={submitDelete}>
{deleteMutation.isPending ? "Deleting..." : "Delete this page and entire edit history (no backsies)"}
</button>)}
</div>
);
}
export default Page;

View File

@ -1,97 +0,0 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useParams, useNavigate } from 'react-router-dom';
import { apiUrl, fetchPage, postPage, deletePage } from '../apiTools.jsx';
import './Pages.css';
function PageEdit() {
const queryClient = useQueryClient();
const navigate = useNavigate();
const { pagenumber } = useParams();
const fetchQuery = useQuery({ // fetch the currrent values
queryKey: ['page', pagenumber],
queryFn: () => fetchPage(pagenumber)
})
const postMutation = useMutation({ // for changing the value when we're done with it
mutationFn: ({id, title, description}) => postPage({id, title, description}),
onSettled: async (data, error, variables) => {
// Invalidate and refetch
await queryClient.invalidateQueries({ queryKey: ['page', variables.id] })
},
});
const deleteMutation = useMutation({ // for changing the value when we're done with it
mutationFn: (id) => deletePage(id),
onSettled: async (data, error, variables) => {
// Invalidate and refetch
await queryClient.invalidateQueries({ queryKey: ['pages'] })
},
});
const ready = !(fetchQuery.error || fetchQuery.isPending);
let {id, title, description, html} = fetchQuery.data || {};
if (!title) title = "&nbsp;";
if (!description) description = "&nbsp;";
function submitChanges(e) {
const newTitle = document.querySelector('span').innerHTML;
const newText = document.querySelector('pre').innerHTML;
postMutation.mutate({
id: pagenumber,
title: newTitle,
description: newText,
});
navigate(`/${pagenumber}`, {replace: true})
}
function submitDelete(e) {
e.preventDefault();
deleteMutation.mutate(pagenumber);
navigate(`/`);
}
return (
<div className="main-column">
<header>
<h1>
<a href="/">🌳</a>
{pagenumber}.&nbsp;
<span
contentEditable="true"
dangerouslySetInnerHTML={{__html: ready ? title : "..." }} />
</h1>
<hr/>
</header>
<section>
{ ready ?
<>
<div className="page-contents">
<pre
contentEditable="true"
dangerouslySetInnerHTML={{__html: description}} />
</div>
<button
disabled={postMutation.isPending}
onClick={submitChanges}>
{postMutation.isPending ? "Updating..." : "Update"}
</button>
<button
disabled={deleteMutation.isPending}
onClick={submitDelete}>
{deleteMutation.isPending ? "Deleting..." : "Delete this page (no backsies)"}
</button>
</>
:
<div className="page-contents">
{ fetchQuery.isPending ? "Loading..." : JSON.stringify(fetchQuery.error) }
</div>
}
</section>
</div>
);
}
export default PageEdit;

View File

@ -1,48 +0,0 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useParams, useNavigate } from 'react-router-dom';
import { apiUrl, fetchPage, postPage } from '../apiTools.jsx';
import { useFixLinks } from '../clientStuff.jsx';
import './Pages.css';
function PageView() {
const queryClient = useQueryClient();
const navigate = useNavigate();
const noLoad = useFixLinks();
const { pagenumber } = useParams();
const { isPending, error, data } = useQuery({ // fetch the currrent values
queryKey: ['page', pagenumber],
queryFn: () => fetchPage(pagenumber)
})
const ready = !(error || isPending);
let {id, title, description, html} = data || {};
return (
<div className="main-column">
<div>
<header>
<h1>
<a href="/" {...noLoad}>🌳</a>{pagenumber}. {ready ? (title || " ") : "..."}</h1>
<hr/>
</header>
<section>
{ ready ?
<div
className="page-contents"
dangerouslySetInnerHTML={{ __html: (html || " ") }}
{...noLoad}
/>
:
(isPending ? "Loading..." : JSON.stringify(error))
}
<button onClick={(e) => { e.preventDefault(); navigate(`/${pagenumber}/edit`, {replace: true})}}>Edit this page!</button>
</section>
</div>
</div>
);
}
export default PageView;

View File

@ -12,9 +12,19 @@ app.use(page_routes);
const user_routes = require('./users.js');
app.use(user_routes);
const graphQuery = db.prepare(`
select p.number, p.html, p.time
from pages p
where time >= (
select max(s.time)
from pages s
where s.number = p.number
)
order by p.time desc
`);
app.get('/graph', (req, res) => {
try {
const rows = db.prepare('select number, html from pages').all();
const rows = graphQuery.all();
const graph = graphFromList(rows);
res.status(200).json(graph);
} catch (error) {

View File

@ -30,8 +30,8 @@ app.get('/pages', (req, res) => {
app.post('/page/new', loginRequired, (req, res) => {
try {
const maxPageNumber = db.prepare('select max(number) as maximum from pages').get()
const newPageNumber = maxPageNumber.maximum + 1;
const maxPage = db.prepare('select max(number) as maximum from pages').get()
const newPageNumber = maxPage.maximum + 1;
const newPage = db.prepare('insert into pages (number, title, description, author) values (?, ?, ?, ?) returning number')
.get(newPageNumber, "new page", "this page is new!", req.session.userId);
res.status(200).json(newPageNumber);
@ -61,9 +61,9 @@ app.post('/page/:number', loginRequired, (req, res) => {
}
});
app.delete('/page/:id', loginRequired, (req, res) => {
app.delete('/page/:number', loginRequired, (req, res) => {
try {
const changes = db.prepare('delete from pages where number = ?').run(req.params.id);
const changes = db.prepare('delete from pages where number = ?').run(req.params.number);
res.status(200).json({id: req.params.id});
} catch (error) {
res.status(500).json({"error": error});
@ -72,7 +72,7 @@ app.delete('/page/:id', loginRequired, (req, res) => {
app.get('/page/:number/history', (req, res) => {
try {
const all = db.prepare('select number, id, time, author from pages where number=:number order by time desc').all(req.params);
const all = db.prepare('select * from pages where number=:number order by time desc').all(req.params);
res.status(200).json(all);
} catch (error) {
res.status(500).json({"error": error});