add stuff to srs

This commit is contained in:
Michael Zhang 2023-06-11 15:08:17 -05:00
parent 1fe57e2bb6
commit 31728966c5
17 changed files with 485 additions and 133 deletions

58
package-lock.json generated
View file

@ -14,12 +14,14 @@
"@emotion/styled": "^11.11.0",
"@tauri-apps/api": "^1.3.0",
"classnames": "^2.3.2",
"date-fns": "^2.30.0",
"flowbite": "^1.6.5",
"framer-motion": "^10.12.16",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router": "^6.11.2",
"react-router-dom": "^6.11.2",
"react-timeago": "^7.1.0",
"swr": "^2.1.5"
},
"devDependencies": {
@ -27,6 +29,7 @@
"@types/node": "^18.7.10",
"@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6",
"@types/react-timeago": "^4.1.3",
"@vitejs/plugin-react": "^3.0.0",
"autoprefixer": "^10.4.14",
"postcss": "^8.4.24",
@ -2059,6 +2062,15 @@
"@types/react": "*"
}
},
"node_modules/@types/react-timeago": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/@types/react-timeago/-/react-timeago-4.1.3.tgz",
"integrity": "sha512-XaaMBzuXLw7lxPPDs/fenlohcf3NDqM5qP4oOL/Meu+Hb1QChW4Igw/SruS1llEqch18RQB3wDTIwvqq4nivvw==",
"dev": true,
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/scheduler": {
"version": "0.16.3",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz",
@ -2438,6 +2450,21 @@
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
"integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
},
"node_modules/date-fns": {
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
"dependencies": {
"@babel/runtime": "^7.21.0"
},
"engines": {
"node": ">=0.11"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/date-fns"
}
},
"node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@ -3520,6 +3547,14 @@
}
}
},
"node_modules/react-timeago": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/react-timeago/-/react-timeago-7.1.0.tgz",
"integrity": "sha512-rouF7MiEm55fH791Y8cg+VobIJgx8gtNJ+gjr86R4ZqO1WKPkXiXjdT/lRzrvEkUzsxT1exHqV2V+Zdi114H3A==",
"peerDependencies": {
"react": "^16.0.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@ -5503,6 +5538,15 @@
"@types/react": "*"
}
},
"@types/react-timeago": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/@types/react-timeago/-/react-timeago-4.1.3.tgz",
"integrity": "sha512-XaaMBzuXLw7lxPPDs/fenlohcf3NDqM5qP4oOL/Meu+Hb1QChW4Igw/SruS1llEqch18RQB3wDTIwvqq4nivvw==",
"dev": true,
"requires": {
"@types/react": "*"
}
},
"@types/scheduler": {
"version": "0.16.3",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz",
@ -5771,6 +5815,14 @@
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
"integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
},
"date-fns": {
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
"requires": {
"@babel/runtime": "^7.21.0"
}
},
"debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@ -6516,6 +6568,12 @@
"tslib": "^2.0.0"
}
},
"react-timeago": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/react-timeago/-/react-timeago-7.1.0.tgz",
"integrity": "sha512-rouF7MiEm55fH791Y8cg+VobIJgx8gtNJ+gjr86R4ZqO1WKPkXiXjdT/lRzrvEkUzsxT1exHqV2V+Zdi114H3A==",
"requires": {}
},
"read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",

View file

@ -16,12 +16,14 @@
"@emotion/styled": "^11.11.0",
"@tauri-apps/api": "^1.3.0",
"classnames": "^2.3.2",
"date-fns": "^2.30.0",
"flowbite": "^1.6.5",
"framer-motion": "^10.12.16",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router": "^6.11.2",
"react-router-dom": "^6.11.2",
"react-timeago": "^7.1.0",
"swr": "^2.1.5"
},
"devDependencies": {
@ -29,6 +31,7 @@
"@types/node": "^18.7.10",
"@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6",
"@types/react-timeago": "^4.1.3",
"@vitejs/plugin-react": "^3.0.0",
"autoprefixer": "^10.4.14",
"postcss": "^8.4.24",

View file

@ -1,6 +1,13 @@
use std::collections::HashMap;
use sqlx::{sqlite::SqliteRow, Encode, Row, SqlitePool, Type};
use tauri::State;
use crate::{
srs::SrsDb,
utils::{EpochMs, Ticks},
};
pub struct KanjiDb(pub SqlitePool);
#[derive(Debug, Derivative, Serialize, Deserialize)]
@ -12,6 +19,9 @@ pub struct GetKanjiOptions {
#[serde(default = "default_how_many")]
#[derivative(Default(value = "10"))]
how_many: u32,
#[serde(default)]
include_srs_info: bool,
}
fn default_how_many() -> u32 {
@ -23,6 +33,7 @@ pub struct Kanji {
character: String,
most_used_rank: u32,
meanings: Vec<KanjiMeaning>,
srs_info: Option<KanjiSrsInfo>,
}
#[derive(Debug, Serialize, Deserialize)]
@ -37,22 +48,33 @@ pub struct GetKanjiResult {
kanji: Vec<Kanji>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct KanjiSrsInfo {
id: u32,
next_answer_date: EpochMs,
associated_kanji: String,
}
#[tauri::command]
pub async fn get_kanji(
db: State<'_, KanjiDb>,
kanji_db: State<'_, KanjiDb>,
srs_db: State<'_, SrsDb>,
options: Option<GetKanjiOptions>,
) -> Result<GetKanjiResult, String> {
let opts = options.unwrap_or_default();
println!("Options: {:?}", opts);
let looking_for_character_clause = match opts.character {
None => String::new(),
Some(_) => format!(" AND KanjiSet.Character = ?"),
Some(_) => format!("AND KanjiSet.Character = ?"),
};
let query_string = format!(
r#"
SELECT *, KanjiMeaningSet.ID as KanjiMeaningID
SELECT
Character,
KanjiMeaningSet.Meaning,
MostUsedRank,
KanjiMeaningSet.ID as KanjiMeaningID
FROM (
SELECT *
FROM KanjiSet
@ -69,21 +91,68 @@ pub async fn get_kanji(
// Do all the binds
if let Some(character) = opts.character {
println!("Bind {}", character);
query = query.bind(character);
if let Some(character) = &opts.character {
query = query.bind(character.clone());
}
println!("Bind {}", opts.how_many);
query = query.bind(opts.how_many);
println!("Query: {query_string}");
let result = query
.fetch_all(&db.0)
.fetch_all(&kanji_db.0)
.await
.map_err(|err| err.to_string())?;
// Look for SRS info
let mut srs_info_map = HashMap::new();
if opts.include_srs_info {
let looking_for_character_clause = match opts.character {
None => String::new(),
Some(_) => format!("AND AssociatedKanji = ?"),
};
let query_string = format!(
r#"
SELECT ID, NextAnswerDate, AssociatedKanji, CurrentGrade FROM SrsEntrySet
WHERE 1=1
{}
"#,
looking_for_character_clause
);
let mut query = sqlx::query(&query_string);
if let Some(character) = &opts.character {
query = query.bind(character.clone());
}
let result = query
.fetch_all(&srs_db.0)
.await
.map_err(|err| err.to_string())?;
for row in result {
let associated_kanji: String = match row.get("AssociatedKanji") {
Some(v) => v,
None => continue,
};
let id = row.get("ID");
let next_answer_date: i64 = row.get("NextAnswerDate");
let next_answer_date = Ticks(next_answer_date).epoch_ms();
srs_info_map.insert(
associated_kanji.clone(),
KanjiSrsInfo {
id,
next_answer_date,
associated_kanji,
},
);
}
println!("SRS MAP: {srs_info_map:?}");
};
// Put it all together
let kanji = {
let mut new_vec: Vec<Kanji> = Vec::with_capacity(result.len());
let mut last_character: Option<String> = None;
@ -110,10 +179,13 @@ pub async fn get_kanji(
if let Some(kanji) = same_as {
kanji.meanings.push(meaning);
} else {
let srs_info = srs_info_map.remove(&character);
new_vec.push(Kanji {
character,
most_used_rank,
meanings: vec![meaning],
srs_info,
});
}
}
@ -121,10 +193,8 @@ pub async fn get_kanji(
new_vec
};
println!("Result: {kanji:?}");
let count = sqlx::query("SELECT COUNT(*) FROM KanjiSet")
.fetch_one(&db.0)
.fetch_one(&kanji_db.0)
.await
.map_err(|err| err.to_string())?;
let count = count.try_get(0).map_err(|err| err.to_string())?;

View file

@ -6,8 +6,9 @@ extern crate derivative;
#[macro_use]
extern crate serde;
mod kanji;
mod srs;
pub mod kanji;
pub mod srs;
pub mod utils;
use std::process;
use std::str::FromStr;
@ -61,6 +62,7 @@ async fn main() -> Result<()> {
.system_tray(tray)
.invoke_handler(tauri::generate_handler![
srs::get_srs_stats,
srs::add_srs_item,
kanji::get_kanji,
])
.on_window_event(|event| match event.event() {

View file

@ -6,6 +6,8 @@ use std::{
use sqlx::{Row, SqlitePool};
use tauri::State;
use crate::utils::Ticks;
pub struct SrsDb(pub SqlitePool);
#[derive(Debug, Serialize, Deserialize)]
@ -38,17 +40,8 @@ pub async fn get_srs_stats(db: State<'_, SrsDb>) -> Result<SrsStats, String> {
.map_err(|err| err.to_string())?;
// reviews query
let utc_now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|err| err.to_string())?
.as_secs() as i64
* 1000000000;
let utc_tomorrow = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|err| err.to_string())?
.add(Duration::from_secs(60 * 60 * 24))
.as_secs() as i64
* 1000000000;
let utc_now = Ticks::now().map_err(|err| err.to_string())?;
let utc_tomorrow = utc_now + Duration::from_secs(60 * 60 * 24);
let row2 = sqlx::query(
r#"
SELECT COUNT(*) AS reviews
@ -79,3 +72,38 @@ pub async fn get_srs_stats(db: State<'_, SrsDb>) -> Result<SrsStats, String> {
num_failure: row.try_get("num_failure").unwrap_or(0),
})
}
#[derive(Debug, Derivative, Serialize, Deserialize)]
#[derivative(Default)]
pub struct AddSrsItemOptions {
character: String,
}
#[tauri::command]
pub async fn add_srs_item(
db: State<'_, SrsDb>,
options: Option<AddSrsItemOptions>,
) -> Result<(), String> {
let opts = options.unwrap_or_default();
println!("Opts: {opts:?}");
let query_string = format!(
r#"
INSERT INTO SrsEntrySet
(CreationDate, AssociatedKanji, NextAnswerDate,
Meanings, Readings)
VALUES
(?, ?, ?, '', '')
"#
);
let utc_now = Ticks::now().map_err(|err| err.to_string())?;
let query = sqlx::query(&query_string)
.bind(&utc_now)
.bind(&opts.character)
.bind(&utc_now);
query.execute(&db.0).await.map_err(|err| err.to_string())?;
Ok(())
}

66
src-tauri/src/utils.rs Normal file
View file

@ -0,0 +1,66 @@
use std::{
ops::Add,
time::{Duration, SystemTime, SystemTimeError, UNIX_EPOCH},
};
use sqlx::{
database::{HasArguments, HasValueRef},
encode::IsNull,
error::BoxDynError,
Decode, Encode, Sqlite, Type,
};
#[derive(Clone, Copy)]
pub struct Ticks(pub i64);
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(transparent)]
pub struct EpochMs(pub u64);
impl Ticks {
pub fn now() -> Result<Self, SystemTimeError> {
Ok(SystemTime::now().duration_since(UNIX_EPOCH)?.into())
}
#[inline]
pub fn epoch_ms(&self) -> EpochMs {
let millis = Duration::from_nanos(self.0 as u64).as_millis() as u64;
EpochMs(millis)
}
}
impl Into<Duration> for Ticks {
fn into(self) -> Duration {
Duration::from_nanos(self.0 as u64)
}
}
impl From<Duration> for Ticks {
fn from(value: Duration) -> Self {
Ticks(value.as_secs() as i64 * 1_000_000_000)
}
}
impl<'q> Encode<'q, Sqlite> for Ticks {
fn encode_by_ref(
&self,
buf: &mut <Sqlite as HasArguments<'q>>::ArgumentBuffer,
) -> IsNull {
self.0.encode_by_ref(buf)
}
}
impl Type<Sqlite> for Ticks {
fn type_info() -> <Sqlite as sqlx::Database>::TypeInfo {
i64::type_info()
}
}
impl Add<Duration> for Ticks {
type Output = Ticks;
fn add(self, rhs: Duration) -> Self::Output {
let rhs_ticks = Ticks::from(rhs);
Ticks(self.0 + rhs_ticks.0)
}
}

View file

@ -1,3 +1,6 @@
$navLinkAccentColor: #09c;
$navLinkColor: hsl(203, 91%, 91%);
main.main {
display: flex;
flex-direction: column;
@ -22,10 +25,20 @@ main.main {
text-align: center;
padding: 12px;
border-top: 4px solid transparent;
transition: background-color 0.1s ease-out, border-top-color 0.1s ease-out;
// Make the top bar seem less web-ish
user-select: none;
cursor: default;
-webkit-user-drag: none;
&:not(.link-active):hover {
border-top-color: transparent;
background-color: #f4f4f4;
}
}
.link-active {
border-top-color: #09c;
background-color: hsl(203, 91%, 91%);
border-top-color: $navLinkAccentColor;
background-color: $navLinkColor;
}

View file

@ -2,7 +2,6 @@ import { Link, RouterProvider, createHashRouter } from "react-router-dom";
import KanjiPane from "./panes/KanjiPane";
import classNames from "classnames";
import { ChakraProvider } from "@chakra-ui/react";
import HomePane from "./panes/HomePane";
import { createBrowserRouter } from "react-router-dom";
import { Outlet, Route, createRoutesFromElements, matchPath, useLocation } from "react-router";
import SrsPane from "./panes/SrsPane";
@ -25,8 +24,9 @@ function Layout() {
).some((item) => matchPath({ path: item.path }, location.pathname));
const mainPath = navLink.subPaths ? navLink.subPaths[0].path : navLink.path;
const className = classNames(styles.link, active && styles["link-active"]);
return (
<li key={navLink.path}>
<li key={`navLink-${navLink.key}`}>
<Link to={mainPath} className={className}>
{navLink.title}
</Link>
@ -49,7 +49,7 @@ export default function App() {
return route.subPaths.map((subRoute, idx2) => {
return (
<Route
key={`${route.key}-${subRoute.key}`}
key={`route-${route.key}-${subRoute.key}`}
index={idx + idx2 == 0}
path={subRoute.path}
element={subRoute.element ?? route.element}
@ -58,7 +58,12 @@ export default function App() {
});
} else {
return (
<Route key={route.path} index={idx == 0} path={route.path} element={route.element} />
<Route
key={`route-${route.key}`}
index={idx == 0}
path={route.path}
element={route.element}
/>
);
}
})}

View file

@ -29,6 +29,11 @@ interface SrsStats {
num_failure: number;
}
interface Stat {
label: string;
value: any;
}
export default function DashboardReviewStats() {
const {
data: srsStats,
@ -44,12 +49,12 @@ export default function DashboardReviewStats() {
</>
);
const averageSuccess = srsStats.num_success / ((srsStats.num_success + srsStats.num_failure) || 1);
const averageSuccess = srsStats.num_success / (srsStats.num_success + srsStats.num_failure || 1);
const averageSuccessStr = `${Math.round(averageSuccess * 10000) / 100}%`;
const canReview = srsStats.reviews_available == 0;
const generateStat = (stat) => {
const generateStat = (stat: Stat) => {
return (
<GridItem>
<Stat>
@ -67,12 +72,16 @@ export default function DashboardReviewStats() {
<Stat>
<StatLabel>reviews available</StatLabel>
<StatNumber>{srsStats.reviews_available}</StatNumber>
<ConditionalWrapper condition={canReview} wrapper={children => <Tooltip label="Add items to start reviewing">{children}</Tooltip>}>
<Link to="/srs/review">
<Button isDisabled={canReview} colorScheme="blue">
Start reviewing <ArrowRightIcon marginLeft={3} />
</Button>
</Link>
<ConditionalWrapper
condition={canReview}
wrapper={(children) => (
<Tooltip label="Add items to start reviewing">{children}</Tooltip>
)}
elseWrapper={(children) => <Link to="/srs/review">{children}</Link>}
>
<Button isDisabled={canReview} colorScheme="blue">
Start reviewing <ArrowRightIcon marginLeft={3} />
</Button>
</ConditionalWrapper>
</Stat>
</GridItem>

View file

@ -1,10 +1,13 @@
import { invoke } from "@tauri-apps/api/tauri";
import { GetKanjiResult } from "../panes/KanjiPane";
import { Kanji } from "../types/Kanji";
import TimeAgo from "react-timeago";
import styles from "./KanjiDisplay.module.scss";
import useSWR from "swr";
import { Button } from "@chakra-ui/button";
import { AddIcon } from "@chakra-ui/icons";
import SelectOnClick from "../lib/SelectOnClick";
import { Alert, AlertIcon } from "@chakra-ui/alert";
interface KanjiDisplayProps {
kanjiCharacter: string;
@ -15,30 +18,59 @@ export default function KanjiDisplay({ kanjiCharacter }: KanjiDisplayProps) {
data: kanjiResult,
error,
isLoading,
mutate,
} = useSWR(["get_kanji", kanjiCharacter], ([command, character]) =>
invoke<GetKanjiResult>(command, { options: { character } }),
invoke<GetKanjiResult>(command, { options: { character, include_srs_info: true } }),
);
if (!kanjiResult || !kanjiResult.kanji) return <>
{JSON.stringify([kanjiResult, error, isLoading])}
Loading...
</>;
if (!kanjiResult || !kanjiResult.kanji)
return (
<>
{JSON.stringify([kanjiResult, error, isLoading])}
Loading...
</>
);
const kanji = kanjiResult.kanji[0];
const addSrsItem = () => {
const addSrsItem = async () => {
await invoke("add_srs_item", {
options: {
character: kanji.character,
},
});
mutate();
};
let srsPart = (
<Button onClick={addSrsItem} colorScheme="green">
<AddIcon /> Add to SRS
</Button>
);
if (kanji.srs_info) {
const nextAnswerDate = new Date(kanji.srs_info.next_answer_date);
srsPart = (
<Alert status="info">
<AlertIcon /> <p>This character is being tracked in SRS!</p>
<p>
(Next test: <TimeAgo date={nextAnswerDate} />)
</p>
</Alert>
);
}
return (
<>
<div className={styles.display}>{kanji.character}</div>
<details>
<summary>Debug</summary>
<pre>{JSON.stringify(kanji, null, 2)}</pre>
</details>
{kanji.meanings.map(m => m.meaning).join(", ")}
<SelectOnClick className={styles.display}>{kanji.character}</SelectOnClick>
<Button onClick={addSrsItem} colorScheme="green">
<AddIcon /> Add to SRS
</Button>
{kanji.meanings.map((m) => m.meaning).join(", ")}
<div>{srsPart}</div>
</>
);
}

View file

@ -0,0 +1,29 @@
.kanji-list {
display: flex;
flex-direction: column;
gap: 2px;
}
.kanji-link {
padding: 4px 8px;
border-left: 4px solid transparent;
user-select: none;
}
.kanji-list-scroll {
direction: rtl;
overflow-y: scroll;
}
.kanji-list-inner {
display: flex;
flex-direction: column;
gap: 4px;
direction: ltr;
}
.kanji-link-active {
border-left-color: #09c;
background-color: hsl(203, 91%, 91%);
}

View file

@ -0,0 +1,47 @@
import { GetKanjiResult } from "../panes/KanjiPane";
import classNames from "classnames";
import { Link } from "react-router-dom";
import { Badge, Grid, GridItem } from "@chakra-ui/layout";
import styles from "./KanjiList.module.scss";
import { Kanji } from "../types/Kanji";
export interface KanjiListProps {
data: GetKanjiResult;
selectedCharacter?: string;
}
export function KanjiList({ data, selectedCharacter }: KanjiListProps) {
const renderKanjiItem = (kanji: Kanji, active: boolean) => {
const className = classNames(styles["kanji-link"], active && styles["kanji-link-active"]);
return (
<Link key={kanji.character} className={className} to={`/kanji/${kanji.character}`}>
<Grid templateRows="repeat(2, 1fr)" templateColumns="1fr 3fr">
<GridItem rowSpan={2} style={{ fontSize: "24px", textAlign: "center" }}>
{kanji.character}
</GridItem>
<GridItem>{kanji.meanings[0].meaning}</GridItem>
<GridItem>
<Badge>#{kanji.most_used_rank} most used</Badge>
</GridItem>
</Grid>
</Link>
);
};
return (
<>
<small>
Displaying {data.kanji.length} of {data.count} results.
</small>
<div className={styles["kanji-list-scroll"]}>
<div className={styles["kanji-list-inner"]}>
{data.kanji.map((kanji) => {
const active = kanji.character == selectedCharacter;
return renderKanjiItem(kanji, active);
})}
</div>
</div>
</>
);
}

View file

@ -1,3 +1,21 @@
export default function ConditionalWrapper({ condition, wrapper, children }) {
return condition ? wrapper(children) : children
export interface ConditionalWrapperProps<S, T> {
condition: boolean;
wrapper?: (_: S) => T;
elseWrapper?: (_: S) => T;
children: S;
}
export default function ConditionalWrapper<S, T>({
condition,
wrapper,
children,
elseWrapper,
}: ConditionalWrapperProps<S, T>) {
return condition
? wrapper
? wrapper(children)
: children
: elseWrapper
? elseWrapper(children)
: children;
}

30
src/lib/SelectOnClick.tsx Normal file
View file

@ -0,0 +1,30 @@
import { HTMLAttributes, PropsWithChildren, useCallback, useState } from "react";
export default function SelectOnClick({
children,
...props
}: PropsWithChildren<HTMLAttributes<HTMLDivElement>>) {
const [container, setContainer] = useState<HTMLDivElement | null>(null);
const containerRef = useCallback((element: HTMLDivElement | null) => {
if (element) setContainer(element);
}, []);
const onClick = useCallback(() => {
if (!container) return;
const range = document.createRange();
range.selectNodeContents(container);
const sel = window.getSelection();
if (!sel) return;
sel.removeAllRanges();
sel.addRange(range);
}, [container]);
return (
<div onClick={onClick} ref={containerRef} {...props}>
{children}
</div>
);
}

View file

@ -1,29 +0,0 @@
.kanji-list {
display: flex;
flex-direction: column;
gap: 2px;
}
.kanji-link {
padding: 4px 8px;
border-left: 4px solid transparent;
user-select: none;
}
.kanji-list-scroll {
direction: rtl;
overflow-y: scroll;
}
.kanji-list-inner {
display: flex;
flex-direction: column;
gap: 4px;
direction: ltr;
}
.kanji-link-active {
border-left-color: #09c;
background-color: hsl(203, 91%, 91%);
}

View file

@ -7,68 +7,32 @@ import { Link, useParams } from "react-router-dom";
import KanjiDisplay from "../components/KanjiDisplay";
import { Kanji } from "../types/Kanji";
import classNames from "classnames";
import { KanjiList } from "../components/KanjiList";
export interface GetKanjiResult {
count: number;
kanji: Kanji[];
}
interface KanjiListProps {
data: GetKanjiResult;
selectedCharacter?: string;
}
function KanjiList({ data, selectedCharacter }: KanjiListProps) {
return (
<>
<small>
Displaying {data.kanji.length} of {data.count} results.
</small>
<div className={styles["kanji-list-scroll"]}>
<div className={styles["kanji-list-inner"]}>
{data.kanji.map((kanji) => {
const active = kanji.character == selectedCharacter;
const className = classNames(styles['kanji-link'], active && styles['kanji-link-active'])
return <Link
key={kanji.character}
className={className}
to={`/kanji/${kanji.character}`}
>
<Grid templateRows="repeat(2, 1fr)" templateColumns="1fr 3fr">
<GridItem rowSpan={2} style={{ fontSize: "24px", textAlign: "center" }}>
{kanji.character}
</GridItem>
<GridItem>{kanji.meanings[0].meaning}</GridItem>
<GridItem>
<small>
#{kanji.most_used_rank} most used
</small>
</GridItem>
</Grid>
</Link>
})}
</div>
</div>
</>
);
}
export default function KanjiPane() {
const { selectedKanji } = useParams();
const { data, error, isLoading } = useSWR("get_kanji", invoke<GetKanjiResult>);
if (error) {
console.error(error);
}
return (
<>
<Stack spacing={7} direction="row">
<Box p={2} className={styles["kanji-list"]}>
<Grid templateRows="1fr" templateColumns="3fr 5fr">
<GridItem className={styles["kanji-list"]}>
{data && <KanjiList data={data} selectedCharacter={selectedKanji} />}
</Box>
</GridItem>
<Box p={5}>
<GridItem>
{selectedKanji ? <KanjiDisplay kanjiCharacter={selectedKanji} /> : "nothing selected"}
</Box>
</Stack>
</GridItem>
</Grid>
</>
);
}

View file

@ -2,9 +2,16 @@ export interface Kanji {
character: string;
most_used_rank: number;
meanings: KanjiMeaning[];
srs_info?: KanjiSrsInfo;
}
export interface KanjiMeaning {
id: number;
meaning: string;
}
export interface KanjiSrsInfo {
id: number;
next_answer_date: number;
associated_kanji: string;
}