This commit is contained in:
Michael Zhang 2024-05-25 11:13:07 -05:00
parent 5bb6ae6aa3
commit ec375f14f7
10 changed files with 206 additions and 26 deletions

View file

@ -18,6 +18,8 @@
"@tanstack/react-query": "^5.37.1", "@tanstack/react-query": "^5.37.1",
"@tauri-apps/api": "^1", "@tauri-apps/api": "^1",
"@uiw/react-md-editor": "^4.0.4", "@uiw/react-md-editor": "^4.0.4",
"classnames": "^2.5.1",
"hast-util-to-jsx-runtime": "^2.3.0",
"hast-util-to-mdast": "^10.1.0", "hast-util-to-mdast": "^10.1.0",
"javascript-time-ago": "^2.5.10", "javascript-time-ago": "^2.5.10",
"jotai": "^2.8.1", "jotai": "^2.8.1",
@ -26,10 +28,14 @@
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-markdown": "^9.0.1", "react-markdown": "^9.0.1",
"react-time-ago": "^7.3.3" "react-time-ago": "^7.3.3",
"remark": "^15.0.1",
"remark-rehype": "^11.1.0",
"vfile": "^6.0.1"
}, },
"devDependencies": { "devDependencies": {
"@tauri-apps/cli": "^1", "@tauri-apps/cli": "^1",
"@types/mdast": "^4.0.4",
"@types/react": "^18.2.15", "@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7", "@types/react-dom": "^18.2.7",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.2.1",

View file

@ -1,9 +1,6 @@
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import styles from "./NodeDisplay.module.scss"; import styles from "./NodeDisplay.module.scss";
import ReactTimeAgo from "react-time-ago"; import ReactTimeAgo from "react-time-ago";
import Markdown from "react-markdown";
import MDEditor, { commands } from "@uiw/react-md-editor";
import { useState } from "react";
import JournalPage from "./nodes/JournalPage"; import JournalPage from "./nodes/JournalPage";
export interface NodeDisplayProps { export interface NodeDisplayProps {
@ -19,6 +16,7 @@ export default function NodeDisplay({ id }: NodeDisplayProps) {
return json; return json;
}, },
}); });
const { isSuccess, status, data } = query; const { isSuccess, status, data } = query;
return ( return (
@ -31,7 +29,7 @@ export default function NodeDisplay({ id }: NodeDisplayProps) {
)} )}
</div> </div>
<div className={styles.title}> <div className={styles.title}>
{data.title ?? <span className={styles.untitled}>(untitled)</span>} {data?.title ?? <span className={styles.untitled}>(untitled)</span>}
</div> </div>
<div className={styles.body}> <div className={styles.body}>
{isSuccess ? ( {isSuccess ? (

View file

@ -7,3 +7,22 @@
.mdEditor { .mdEditor {
flex-grow: 1; flex-grow: 1;
} }
.block {
padding: 12px;
&:not(.isEditing) {
user-select: none;
-webkit-user-select: none;
}
&.isEditing {
background-color: rgb(235, 243, 246);
outline: 2px solid skyblue;
}
&:hover {
background-color: rgb(235, 243, 246);
cursor: text;
}
}

View file

@ -1,21 +1,59 @@
import { createContext, useCallback, useContext, useState } from "react"; import {
ReactNode,
createContext,
useCallback,
useContext,
useEffect,
useRef,
useState,
} from "react";
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
import styles from "./JournalPage.module.scss"; import styles from "./JournalPage.module.scss";
import MDEditor from "@uiw/react-md-editor"; import MDEditor from "@uiw/react-md-editor";
import Markdown from "react-markdown"; import Markdown from "react-markdown";
import { toMdast } from "hast-util-to-mdast"; import { toMdast } from "hast-util-to-mdast";
import { Node as MdastNode } from "mdast";
import { fromMarkdown } from "mdast-util-from-markdown"; import { fromMarkdown } from "mdast-util-from-markdown";
import { toMarkdown } from "mdast-util-to-markdown"; import { toMarkdown } from "mdast-util-to-markdown";
import { toJsxRuntime } from "hast-util-to-jsx-runtime";
import remarkRehype from "remark-rehype";
import { VFile } from "vfile";
import { common } from "@mui/material/colors";
import classNames from "classnames";
const MDContext = createContext(null); interface MDContextValue {
isEditing: boolean;
}
// biome-ignore lint/style/noNonNullAssertion: <explanation>
const MDContext = createContext<MDContextValue>(null!);
const emptyContent = { type: "root", children: [] };
export default function JournalPage({ id, data }) { export default function JournalPage({ id, data }) {
const [content, setContent] = useState(() => data.content); const [content, setContent] = useState(() => data.content);
const [isEditing, setIsEditing] = useState(() => false); const [isEditing, setIsEditing] = useState(() => false);
const [currentlyFocused, setCurrentlyFocused] = useState<string | undefined>(
() => undefined,
);
const tree = fromMarkdown(data.content); useEffect(() => {
console.log("tree", tree); if (content === null) {
setContent(() => ({
type: "root",
children: [
{ type: "paragraph", children: [{ type: "text", value: "" }] },
],
}));
setCurrentlyFocused(".children[0]");
setIsEditing(true);
}
}, [content]);
const contextValue = { content, setContent, isEditing, setIsEditing }; const contextValue = { content, setContent, isEditing, setIsEditing };
const jsxContent = convertToJsx(content, { currentlyFocused });
return ( return (
<> <>
<details> <details>
@ -23,7 +61,79 @@ export default function JournalPage({ id, data }) {
<pre>{JSON.stringify(data, null, 2)}</pre> <pre>{JSON.stringify(data, null, 2)}</pre>
</details> </details>
<div className={styles.mdContent} data-color-mode="light"></div> <div className={styles.mdContent} data-color-mode="light">
<MDContext.Provider value={contextValue}>
{jsxContent}
</MDContext.Provider>
<pre>{JSON.stringify(content, null, 2)}</pre>
</div>
</> </>
); );
} }
interface ConvertToJsxOpts {
currentlyFocused?: string | undefined;
parent?: MdastNode | undefined;
}
function convertToJsx(
tree: MdastNode,
opts?: ConvertToJsxOpts | undefined,
): ReactNode {
console.log("tree", tree);
if (tree === null) return;
const commonProps = {
node: tree,
parent: opts?.parent,
};
switch (tree.type) {
case "root":
return tree.children.map((child) =>
convertToJsx(child, { parent: tree }),
);
case "paragraph":
return <Paragraph {...commonProps} />;
default:
throw new Error(`unhandled ${tree.type}`);
}
}
function Paragraph({ ...args }) {
// const { isEditing } = useContext(MDContext);
const [isEditing, setIsEditing] = useState(() => false);
const [localValue, setLocalValue] = useState(null);
const onDoubleClick = useCallback(() => {
if (!isEditing) {
setIsEditing(true);
}
}, [isEditing]);
const save = useCallback(() => {
console.log("saving!", localValue);
});
const onPaste = useCallback((evt) => {
console.log("pasted");
}, []);
return (
<div>
<div
className={classNames(styles.block, isEditing && styles.isEditing)}
contentEditable={isEditing}
onDoubleClick={onDoubleClick}
onPaste={onPaste}
onBlur={save}
>
<br />
</div>
</div>
);
}

View file

@ -0,0 +1,9 @@
import { ReactNode } from "react";
import { Nodes as MdastNodes } from "mdast";
export function convertToJsx(tree: MdastNodes): ReactNode {
console.log("tree", tree);
switch (tree.type) {
}
}

View file

@ -3,7 +3,6 @@ use axum::{
response::{IntoResponse, Response}, response::{IntoResponse, Response},
}; };
pub type AppResult<T, E = AppError> = std::result::Result<T, E>; pub type AppResult<T, E = AppError> = std::result::Result<T, E>;
// Make our own error that wraps `anyhow::Error`. // Make our own error that wraps `anyhow::Error`.
@ -13,6 +12,7 @@ pub struct AppError(miette::Report);
impl IntoResponse for AppError { impl IntoResponse for AppError {
fn into_response(self) -> Response { fn into_response(self) -> Response {
eprintln!("Encountered error: {}", self.0); eprintln!("Encountered error: {}", self.0);
eprintln!("{:?}", self.0);
( (
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", self.0), format!("Something went wrong: {}", self.0),

View file

@ -14,7 +14,7 @@ pub async fn get_todays_journal_id(
let result = state.db.run_script( let result = state.db.run_script(
" "
?[node_id] := *journal_days[day, node_id], day = $day ?[node_id] := *journal_day[day, node_id], day = $day
", ",
btmap! { btmap! {
"day".to_owned() => today.clone().into(), "day".to_owned() => today.clone().into(),
@ -29,27 +29,28 @@ pub async fn get_todays_journal_id(
let uuid = Uuid::now_v7(); let uuid = Uuid::now_v7();
let node_id = uuid.to_string(); let node_id = uuid.to_string();
let _result = state.db.run_script_fold_err( state.db.run_script(
" "
{ {
?[id, type] <- [[$node_id, 'panorama/journal/page']] ?[id, title, type] <- [[$node_id, $title, 'panorama/journal/page']]
:put node { id, type } :put node { id, title, type }
} }
{ {
?[node_id, content] <- [[$node_id, 'Default **content**']] ?[node_id, content] <- [[$node_id, {}]]
:put journal { node_id => content } :put journal { node_id => content }
} }
{ {
?[day, node_id] <- [[$day, $node_id]] ?[day, node_id] <- [[$day, $node_id]]
:put journal_days { day => node_id } :put journal_day { day => node_id }
} }
", ",
btmap! { btmap! {
"node_id".to_owned() => node_id.clone().into(), "node_id".to_owned() => node_id.clone().into(),
"day".to_owned() => today.clone().into(), "day".to_owned() => today.clone().into(),
"title".to_owned() => today.clone().into(),
}, },
ScriptMutability::Mutable, ScriptMutability::Mutable,
); )?;
return Ok(Json(json!({ return Ok(Json(json!({
"node_id": node_id "node_id": node_id
@ -58,7 +59,8 @@ pub async fn get_todays_journal_id(
let node_id = result.rows[0][0].get_str().unwrap(); let node_id = result.rows[0][0].get_str().unwrap();
Ok(Json(json!({ Ok(Json(json!({
"node_id": node_id "node_id": node_id,
"day": today,
}))) })))
} }

View file

@ -119,11 +119,11 @@ fn migration_01(db: &DbInstance) -> Result<()> {
{ :create node_refers_to { node_id: String => other_node_id: String } } { :create node_refers_to { node_id: String => other_node_id: String } }
# Create journal type # Create journal type
{ :create journal { node_id: String => content: String } } { :create journal { node_id: String => content: Json } }
{ :create journal_days { day: String => node_id: String } } { :create journal_day { day: String => node_id: String } }
{ {
::fts create journal:text_index { ::fts create journal:text_index {
extractor: content, extractor: dump_json(content),
extract_filter: !is_null(content), extract_filter: !is_null(content),
tokenizer: Simple, tokenizer: Simple,
filters: [Lowercase, Stemmer('english'), Stopwords('en')], filters: [Lowercase, Stemmer('english'), Stopwords('en')],

View file

@ -19,12 +19,12 @@ pub async fn get_node(
j[content] := *journal{ node_id, content }, node_id = $node_id j[content] := *journal{ node_id, content }, node_id = $node_id
j[content] := not *journal{ node_id }, node_id = $node_id, content = null j[content] := not *journal{ node_id }, node_id = $node_id, content = null
jd[day] := *journal_days{ node_id, day }, node_id = $node_id jd[day] := *journal_day{ node_id, day }, node_id = $node_id
jd[day] := not *journal_days{ node_id }, node_id = $node_id, day = null jd[day] := not *journal_day{ node_id }, node_id = $node_id, day = null
?[ ?[
extra_data, content, day, created_at, updated_at, type extra_data, content, day, created_at, updated_at, type, title
] := *node{ id, type, created_at, updated_at, extra_data }, ] := *node{ id, type, title, created_at, updated_at, extra_data },
j[content], j[content],
jd[day], jd[day],
id = $node_id id = $node_id
@ -52,6 +52,7 @@ pub async fn get_node(
"created_at": row[3].get_float(), "created_at": row[3].get_float(),
"updated_at": row[4].get_float(), "updated_at": row[4].get_float(),
"type": row[5].get_str(), "type": row[5].get_str(),
"title": row[6].get_str(),
})), })),
)) ))
} }

View file

@ -35,6 +35,12 @@ importers:
'@uiw/react-md-editor': '@uiw/react-md-editor':
specifier: ^4.0.4 specifier: ^4.0.4
version: 4.0.4(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 4.0.4(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
classnames:
specifier: ^2.5.1
version: 2.5.1
hast-util-to-jsx-runtime:
specifier: ^2.3.0
version: 2.3.0
hast-util-to-mdast: hast-util-to-mdast:
specifier: ^10.1.0 specifier: ^10.1.0
version: 10.1.0 version: 10.1.0
@ -62,10 +68,22 @@ importers:
react-time-ago: react-time-ago:
specifier: ^7.3.3 specifier: ^7.3.3
version: 7.3.3(javascript-time-ago@2.5.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 7.3.3(javascript-time-ago@2.5.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
remark:
specifier: ^15.0.1
version: 15.0.1
remark-rehype:
specifier: ^11.1.0
version: 11.1.0
vfile:
specifier: ^6.0.1
version: 6.0.1
devDependencies: devDependencies:
'@tauri-apps/cli': '@tauri-apps/cli':
specifier: ^1 specifier: ^1
version: 1.5.14 version: 1.5.14
'@types/mdast':
specifier: ^4.0.4
version: 4.0.4
'@types/react': '@types/react':
specifier: ^18.2.15 specifier: ^18.2.15
version: 18.3.3 version: 18.3.3
@ -836,6 +854,9 @@ packages:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'} engines: {node: '>= 8.10.0'}
classnames@2.5.1:
resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==}
clsx@2.1.1: clsx@2.1.1:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -1440,6 +1461,9 @@ packages:
remark-stringify@11.0.0: remark-stringify@11.0.0:
resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==}
remark@15.0.1:
resolution: {integrity: sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==}
resolve-from@4.0.0: resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'} engines: {node: '>=4'}
@ -2316,6 +2340,8 @@ snapshots:
optionalDependencies: optionalDependencies:
fsevents: 2.3.3 fsevents: 2.3.3
classnames@2.5.1: {}
clsx@2.1.1: {} clsx@2.1.1: {}
color-convert@1.9.3: color-convert@1.9.3:
@ -3298,6 +3324,15 @@ snapshots:
mdast-util-to-markdown: 2.1.0 mdast-util-to-markdown: 2.1.0
unified: 11.0.4 unified: 11.0.4
remark@15.0.1:
dependencies:
'@types/mdast': 4.0.4
remark-parse: 11.0.0
remark-stringify: 11.0.0
unified: 11.0.4
transitivePeerDependencies:
- supports-color
resolve-from@4.0.0: {} resolve-from@4.0.0: {}
resolve@1.22.8: resolve@1.22.8: