added history view

main
Shoofle 1 week ago
parent 13fb4db1d1
commit 090ded9e85
  1. 11
      client/src/App.jsx
  2. 32
      client/src/apiTools.jsx
  3. 1
      client/src/login/Profile.jsx
  4. 49
      client/src/page/HistoryView.jsx
  5. 116
      client/src/page/Page.jsx
  6. 97
      client/src/page/PageEdit.jsx
  7. 48
      client/src/page/PageView.jsx
  8. 12
      server/routes/api.js
  9. 10
      server/routes/pages.js

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

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

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

@ -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;

@ -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;

@ -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;

@ -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;

@ -12,9 +12,19 @@ app.use(page_routes);
const user_routes = require('./users.js'); const user_routes = require('./users.js');
app.use(user_routes); 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) => { app.get('/graph', (req, res) => {
try { try {
const rows = db.prepare('select number, html from pages').all(); const rows = graphQuery.all();
const graph = graphFromList(rows); const graph = graphFromList(rows);
res.status(200).json(graph); res.status(200).json(graph);
} catch (error) { } catch (error) {

@ -30,8 +30,8 @@ app.get('/pages', (req, res) => {
app.post('/page/new', loginRequired, (req, res) => { app.post('/page/new', loginRequired, (req, res) => {
try { try {
const maxPageNumber = db.prepare('select max(number) as maximum from pages').get() const maxPage = db.prepare('select max(number) as maximum from pages').get()
const newPageNumber = maxPageNumber.maximum + 1; const newPageNumber = maxPage.maximum + 1;
const newPage = db.prepare('insert into pages (number, title, description, author) values (?, ?, ?, ?) returning number') 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); .get(newPageNumber, "new page", "this page is new!", req.session.userId);
res.status(200).json(newPageNumber); 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 { 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}); res.status(200).json({id: req.params.id});
} catch (error) { } catch (error) {
res.status(500).json({"error": 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) => { app.get('/page/:number/history', (req, res) => {
try { 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); res.status(200).json(all);
} catch (error) { } catch (error) {
res.status(500).json({"error": error}); res.status(500).json({"error": error});

Loading…
Cancel
Save