added history view
This commit is contained in:
parent
13fb4db1d1
commit
090ded9e85
@ -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:
|
||||||
|
49
client/src/page/HistoryView.jsx
Normal file
49
client/src/page/HistoryView.jsx
Normal 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
116
client/src/page/Page.jsx
Normal 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;
|
@ -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 = " ";
|
|
||||||
if (!description) description = " ";
|
|
||||||
|
|
||||||
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}.
|
|
||||||
<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…
Reference in New Issue
Block a user