search bar
This commit is contained in:
parent
d0d64cf018
commit
e3c477181d
13 changed files with 2234 additions and 55 deletions
|
@ -9,11 +9,20 @@
|
|||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.11.4",
|
||||
"@emotion/styled": "^11.11.5",
|
||||
"@floating-ui/react": "^0.26.16",
|
||||
"@fontsource/inter": "^5.0.18",
|
||||
"@mui/icons-material": "^5.15.18",
|
||||
"@mui/material": "^5.15.18",
|
||||
"@tanstack/react-query": "^5.37.1",
|
||||
"@tauri-apps/api": "^1",
|
||||
"@uiw/react-md-editor": "^4.0.4",
|
||||
"javascript-time-ago": "^2.5.10",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react-dom": "^18.2.0",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-time-ago": "^7.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^1",
|
||||
|
|
|
@ -6,9 +6,13 @@ import "./global.scss";
|
|||
import { useEffect, useState } from "react";
|
||||
import NodeDisplay from "./components/NodeDisplay";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import TimeAgo from "javascript-time-ago";
|
||||
import en from "javascript-time-ago/locale/en";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
TimeAgo.addDefaultLocale(en);
|
||||
|
||||
function App() {
|
||||
const [nodesOpened, setNodesOpened] = useState<string[]>(() => []);
|
||||
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import styles from "./Header.module.scss";
|
||||
import SearchBar from "./SearchBar";
|
||||
|
||||
export default function Header() {
|
||||
return (
|
||||
<div className={styles.Header}>
|
||||
<span>Panorama</span>
|
||||
<input type="text" placeholder="Search..." />
|
||||
<SearchBar />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,18 +1,37 @@
|
|||
.container {
|
||||
width: 400px;
|
||||
width: 500px;
|
||||
overflow-wrap: break-word;
|
||||
overflow-y: auto;
|
||||
|
||||
border: 1px solid lightgray;
|
||||
border-radius: 4px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.mdContent {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.mdEditor {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 2px 12px;
|
||||
background-color: lightgray;
|
||||
color: gray;
|
||||
color: rgb(106, 103, 160);
|
||||
background: rgb(204, 201, 255);
|
||||
background: linear-gradient(90deg, rgba(204, 201, 255, 1) 0%, rgba(255, 255, 255, 1) 100%);
|
||||
}
|
||||
|
||||
.body {
|
||||
flex-grow: 1;
|
||||
|
||||
padding: 12px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
|
@ -1,5 +1,9 @@
|
|||
import { useQuery } from "@tanstack/react-query";
|
||||
import styles from "./NodeDisplay.module.scss";
|
||||
import ReactTimeAgo from "react-time-ago";
|
||||
import Markdown from "react-markdown";
|
||||
import MDEditor, { commands } from "@uiw/react-md-editor";
|
||||
import { useState } from "react";
|
||||
|
||||
export interface NodeDisplayProps {
|
||||
id: string;
|
||||
|
@ -19,7 +23,11 @@ export default function NodeDisplay({ id }: NodeDisplayProps) {
|
|||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
<small>ID {id}</small>
|
||||
{isSuccess ? (
|
||||
<NodeDisplayHeaderLoaded id={id} data={data} />
|
||||
) : (
|
||||
<small>ID {id}</small>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.body}>
|
||||
{isSuccess ? (
|
||||
|
@ -32,11 +40,51 @@ export default function NodeDisplay({ id }: NodeDisplayProps) {
|
|||
);
|
||||
}
|
||||
|
||||
function NodeDisplayHeaderLoaded({ id, data }) {
|
||||
return (
|
||||
<small>
|
||||
Type {data.type} · Last updated{" "}
|
||||
<ReactTimeAgo date={data.created_at * 1000} /> · {id}
|
||||
</small>
|
||||
);
|
||||
}
|
||||
|
||||
function NodeDisplayLoaded({ id, data }) {
|
||||
const [value, setValue] = useState(() => data.content);
|
||||
const [isEditing, setIsEditing] = useState(() => false);
|
||||
return (
|
||||
<>
|
||||
Node {id}
|
||||
<p>{JSON.stringify(data)}</p>
|
||||
<details>
|
||||
<summary>JSON</summary>
|
||||
<pre>{JSON.stringify(data, null, 2)}</pre>
|
||||
</details>
|
||||
|
||||
<button type="button" onClick={() => setIsEditing((prev) => !prev)}>
|
||||
{isEditing ? "done" : "edit"}
|
||||
</button>
|
||||
|
||||
<div className={styles.mdContent} data-color-mode="light">
|
||||
{
|
||||
isEditing ? (
|
||||
<>
|
||||
<MDEditor
|
||||
data-color-mode="light"
|
||||
className={styles.mdEditor}
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<MDEditor.Markdown
|
||||
source={value}
|
||||
style={{ whiteSpace: "pre-wrap" }}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
// <Markdown>{data.content}</Markdown>
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
9
app/src/components/SearchBar.module.scss
Normal file
9
app/src/components/SearchBar.module.scss
Normal file
|
@ -0,0 +1,9 @@
|
|||
.menu {
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
// box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.25);
|
||||
|
||||
min-width: 500px;
|
||||
min-height: 200px;
|
||||
}
|
66
app/src/components/SearchBar.tsx
Normal file
66
app/src/components/SearchBar.tsx
Normal file
|
@ -0,0 +1,66 @@
|
|||
import styles from "./SearchBar.module.scss";
|
||||
import {
|
||||
FloatingFocusManager,
|
||||
FloatingOverlay,
|
||||
FloatingPortal,
|
||||
autoUpdate,
|
||||
offset,
|
||||
useClick,
|
||||
useDismiss,
|
||||
useFloating,
|
||||
useFocus,
|
||||
useInteractions,
|
||||
} from "@floating-ui/react";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function SearchBar() {
|
||||
const [showMenu, setShowMenu] = useState(() => false);
|
||||
|
||||
const { refs, context, floatingStyles } = useFloating({
|
||||
placement: "bottom-start",
|
||||
open: showMenu,
|
||||
onOpenChange: setShowMenu,
|
||||
whileElementsMounted: autoUpdate,
|
||||
middleware: [offset(10)],
|
||||
});
|
||||
const focus = useFocus(context);
|
||||
const { getReferenceProps, getFloatingProps } = useInteractions([
|
||||
focus,
|
||||
useDismiss(context),
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
onFocus={() => setShowMenu(true)}
|
||||
ref={refs.setReference}
|
||||
{...getReferenceProps()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{showMenu && (
|
||||
<FloatingPortal>
|
||||
<FloatingOverlay>
|
||||
{/* <FloatingFocusManager context={context} modal={false}> */}
|
||||
<div
|
||||
ref={refs.setFloating}
|
||||
className={styles.menu}
|
||||
style={{ ...floatingStyles }}
|
||||
{...getFloatingProps()}
|
||||
>
|
||||
<SearchMenu />
|
||||
</div>
|
||||
{/* </FloatingFocusManager> */}
|
||||
</FloatingOverlay>
|
||||
</FloatingPortal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SearchMenu({}) {
|
||||
return <>Search</>;
|
||||
}
|
|
@ -2,7 +2,7 @@ use axum::{
|
|||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use miette::{IntoDiagnostic, Report};
|
||||
|
||||
|
||||
pub type AppResult<T, E = AppError> = std::result::Result<T, E>;
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
use axum::{extract::State, Json};
|
||||
use chrono::Local;
|
||||
use cozo::{DataValue, ScriptMutability};
|
||||
use cozo::ScriptMutability;
|
||||
use serde_json::Value;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{ensure_ok, error::AppResult, AppState};
|
||||
use crate::{error::AppResult, AppState};
|
||||
|
||||
pub async fn get_todays_journal_id(
|
||||
State(state): State<AppState>,
|
||||
|
@ -29,15 +29,15 @@ pub async fn get_todays_journal_id(
|
|||
let uuid = Uuid::now_v7();
|
||||
let node_id = uuid.to_string();
|
||||
|
||||
let result = state.db.run_script_fold_err(
|
||||
let _result = state.db.run_script_fold_err(
|
||||
"
|
||||
{
|
||||
?[id] <- [[$node_id]]
|
||||
:put node { id }
|
||||
?[id, type] <- [[$node_id, 'panorama/journal/page']]
|
||||
:put node { id, type }
|
||||
}
|
||||
{
|
||||
?[node_id, plaintext] <- [[$node_id, '']]
|
||||
:put journal { node_id => plaintext }
|
||||
?[node_id, content] <- [[$node_id, 'Default **content**']]
|
||||
:put journal { node_id => content }
|
||||
}
|
||||
{
|
||||
?[day, node_id] <- [[$day, $node_id]]
|
||||
|
@ -45,11 +45,15 @@ pub async fn get_todays_journal_id(
|
|||
}
|
||||
",
|
||||
btmap! {
|
||||
"node_id".to_owned() => node_id.into(),
|
||||
"node_id".to_owned() => node_id.clone().into(),
|
||||
"day".to_owned() => today.clone().into(),
|
||||
},
|
||||
ScriptMutability::Mutable,
|
||||
);
|
||||
|
||||
return Ok(Json(json!({
|
||||
"node_id": node_id
|
||||
})));
|
||||
}
|
||||
|
||||
let node_id = result.rows[0][0].get_str().unwrap();
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
#[macro_use]
|
||||
extern crate anyhow;
|
||||
#[macro_use]
|
||||
extern crate serde;
|
||||
#[macro_use]
|
||||
extern crate serde_json;
|
||||
#[macro_use]
|
||||
extern crate sugars;
|
||||
|
@ -13,7 +15,11 @@ mod node;
|
|||
use std::fs;
|
||||
|
||||
use anyhow::Result;
|
||||
use axum::{http::Method, routing::get, Router};
|
||||
use axum::{
|
||||
http::Method,
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
use cozo::DbInstance;
|
||||
use serde_json::Value;
|
||||
use tokio::net::TcpListener;
|
||||
|
@ -21,7 +27,9 @@ use tower::ServiceBuilder;
|
|||
use tower_http::cors::{self, CorsLayer};
|
||||
|
||||
use crate::{
|
||||
journal::get_todays_journal_id, migrations::run_migrations, node::get_node,
|
||||
journal::get_todays_journal_id,
|
||||
migrations::run_migrations,
|
||||
node::{get_node, update_node},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
|
@ -57,6 +65,7 @@ async fn main() -> Result<()> {
|
|||
let app = Router::new()
|
||||
.route("/", get(|| async { "Hello, World!" }))
|
||||
.route("/node/:id", get(get_node))
|
||||
.route("/node/:id", post(update_node))
|
||||
.route("/journal/get_todays_journal_id", get(get_todays_journal_id))
|
||||
.layer(ServiceBuilder::new().layer(cors))
|
||||
.with_state(state);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use anyhow::{bail, Result};
|
||||
use cozo::{DbInstance, ScriptMutability};
|
||||
use futures::Future;
|
||||
use anyhow::Result;
|
||||
use cozo::DbInstance;
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::ensure_ok;
|
||||
|
@ -9,11 +9,8 @@ pub async fn run_migrations(db: &DbInstance) -> Result<()> {
|
|||
let migration_status = check_migration_status(db).await?;
|
||||
println!("migration status: {:?}", migration_status);
|
||||
|
||||
let migrations: Vec<Box<dyn for<'a> Fn(&'a DbInstance) -> Result<()>>> = vec![
|
||||
Box::new(no_op),
|
||||
Box::new(migration_01),
|
||||
Box::new(migration_02),
|
||||
];
|
||||
let migrations: Vec<Box<dyn for<'a> Fn(&'a DbInstance) -> Result<()>>> =
|
||||
vec![Box::new(no_op), Box::new(migration_01)];
|
||||
|
||||
if let MigrationStatus::NoMigrations = migration_status {
|
||||
let result = db.run_script_str(
|
||||
|
@ -108,34 +105,26 @@ fn migration_01(db: &DbInstance) -> Result<()> {
|
|||
:create node {
|
||||
id: String
|
||||
=>
|
||||
type: String,
|
||||
title: String? default null,
|
||||
created_at: Float default now(),
|
||||
updated_at: Float default now(),
|
||||
extra_data: Json default {},
|
||||
}
|
||||
}
|
||||
|
||||
# Inverse mapping from keys to nodes
|
||||
{ :create has_key { key: String => id: String } }
|
||||
# Inverse mappings for easy querying
|
||||
{ :create node_has_key { key: String => id: String } }
|
||||
{ :create node_managed_by_app { node_id: String => app: String } }
|
||||
",
|
||||
"",
|
||||
false,
|
||||
);
|
||||
ensure_ok(&result)?;
|
||||
{ :create node_refers_to { node_id: String => other_node_id: String } }
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn migration_02(db: &DbInstance) -> Result<()> {
|
||||
let result = db.run_script_str(
|
||||
"
|
||||
# Create journal type
|
||||
{ :create journal { node_id: String => plaintext: String } }
|
||||
{ :create journal { node_id: String => content: String } }
|
||||
{ :create journal_days { day: String => node_id: String } }
|
||||
{
|
||||
::fts create journal:text_index {
|
||||
extractor: plaintext,
|
||||
extract_filter: !is_null(plaintext),
|
||||
extractor: content,
|
||||
extract_filter: !is_null(content),
|
||||
tokenizer: Simple,
|
||||
filters: [Lowercase, Stemmer('english'), Stopwords('en')],
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
Json,
|
||||
};
|
||||
use cozo::ScriptMutability;
|
||||
|
@ -10,17 +11,19 @@ use crate::{error::AppResult, AppState};
|
|||
pub async fn get_node(
|
||||
State(state): State<AppState>,
|
||||
Path(node_id): Path<String>,
|
||||
) -> AppResult<Json<Value>> {
|
||||
) -> AppResult<(StatusCode, Json<Value>)> {
|
||||
let result = state.db.run_script(
|
||||
"
|
||||
j[plaintext] := *journal{ node_id, plaintext }, node_id = $node_id
|
||||
j[plaintext] := not *journal{ node_id }, node_id = $node_id, plaintext = null
|
||||
j[content] := *journal{ node_id, content }, node_id = $node_id
|
||||
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] := not *journal_days{ node_id }, node_id = $node_id, day = null
|
||||
|
||||
?[extra_data, plaintext, day] := *node{ id, extra_data },
|
||||
j[plaintext],
|
||||
?[
|
||||
extra_data, content, day, created_at, updated_at, type
|
||||
] := *node{ id, type, created_at, updated_at, extra_data },
|
||||
j[content],
|
||||
jd[day],
|
||||
id = $node_id
|
||||
:limit 1
|
||||
|
@ -29,15 +32,34 @@ pub async fn get_node(
|
|||
ScriptMutability::Immutable,
|
||||
)?;
|
||||
|
||||
if result.rows.len() == 0 {
|
||||
return Ok((StatusCode::NOT_FOUND, Json(json!(null))));
|
||||
}
|
||||
|
||||
let row = &result.rows[0];
|
||||
let extra_data = row[0].get_str();
|
||||
let plaintext = row[1].get_str();
|
||||
let day = row[2].get_str();
|
||||
|
||||
Ok(Json(json!({
|
||||
"node": node_id,
|
||||
"extra_data": extra_data,
|
||||
"plaintext": plaintext,
|
||||
"day": day,
|
||||
})))
|
||||
Ok((
|
||||
StatusCode::OK,
|
||||
Json(json!({
|
||||
"node": node_id,
|
||||
"extra_data": extra_data,
|
||||
"content": row[1].get_str(),
|
||||
"day": day,
|
||||
"created_at": row[3].get_float(),
|
||||
"updated_at": row[4].get_float(),
|
||||
"type": row[5].get_str(),
|
||||
})),
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct NodeUpdate {}
|
||||
|
||||
pub async fn update_node(
|
||||
State(state): State<AppState>,
|
||||
Path(node_id): Path<String>,
|
||||
) -> AppResult<Json<Value>> {
|
||||
Ok(Json(json!({})))
|
||||
}
|
||||
|
|
1999
pnpm-lock.yaml
1999
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue