added history view
This commit is contained in:
parent
13fb4db1d1
commit
090ded9e85
@ -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>
|
||||
|
@ -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}) {
|
||||
|
@ -30,6 +30,7 @@ function Profile() {
|
||||
<label>
|
||||
Id:
|
||||
<input {...register("id")} value={id} disabled/>
|
||||
<br/>
|
||||
</label>
|
||||
<label>
|
||||
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');
|
||||
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) {
|
||||
|
@ -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});
|
||||
|
Loading…
Reference in New Issue
Block a user