diff --git a/package-lock.json b/package-lock.json index 8ccfafc..f502bad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index e00a324..a967666 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src-tauri/src/kanji.rs b/src-tauri/src/kanji.rs index 55bf2fc..72d4f93 100644 --- a/src-tauri/src/kanji.rs +++ b/src-tauri/src/kanji.rs @@ -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, + srs_info: Option, } #[derive(Debug, Serialize, Deserialize)] @@ -37,22 +48,33 @@ pub struct GetKanjiResult { kanji: Vec, } +#[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, ) -> Result { 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 = Vec::with_capacity(result.len()); let mut last_character: Option = 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())?; diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index bb165d6..8bee33c 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -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() { diff --git a/src-tauri/src/srs.rs b/src-tauri/src/srs.rs index 605251b..5a18bf1 100644 --- a/src-tauri/src/srs.rs +++ b/src-tauri/src/srs.rs @@ -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 { .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 { 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, +) -> 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(()) +} diff --git a/src-tauri/src/utils.rs b/src-tauri/src/utils.rs new file mode 100644 index 0000000..a1f7775 --- /dev/null +++ b/src-tauri/src/utils.rs @@ -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 { + 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 for Ticks { + fn into(self) -> Duration { + Duration::from_nanos(self.0 as u64) + } +} + +impl From 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 >::ArgumentBuffer, + ) -> IsNull { + self.0.encode_by_ref(buf) + } +} + +impl Type for Ticks { + fn type_info() -> ::TypeInfo { + i64::type_info() + } +} + +impl Add for Ticks { + type Output = Ticks; + + fn add(self, rhs: Duration) -> Self::Output { + let rhs_ticks = Ticks::from(rhs); + Ticks(self.0 + rhs_ticks.0) + } +} diff --git a/src/App.module.scss b/src/App.module.scss index 00f2847..1acf3df 100644 --- a/src/App.module.scss +++ b/src/App.module.scss @@ -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; } diff --git a/src/App.tsx b/src/App.tsx index f7eecb0..99fee16 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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 ( -
  • +
  • {navLink.title} @@ -49,7 +49,7 @@ export default function App() { return route.subPaths.map((subRoute, idx2) => { return ( + ); } })} diff --git a/src/components/DashboardReviewStats.tsx b/src/components/DashboardReviewStats.tsx index a659b07..a53dae7 100644 --- a/src/components/DashboardReviewStats.tsx +++ b/src/components/DashboardReviewStats.tsx @@ -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 ( @@ -67,12 +72,16 @@ export default function DashboardReviewStats() { reviews available {srsStats.reviews_available} - {children}}> - - - + ( + {children} + )} + elseWrapper={(children) => {children}} + > + diff --git a/src/components/KanjiDisplay.tsx b/src/components/KanjiDisplay.tsx index 1d4d841..c2919e6 100644 --- a/src/components/KanjiDisplay.tsx +++ b/src/components/KanjiDisplay.tsx @@ -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(command, { options: { character } }), + invoke(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 = ( + + ); + if (kanji.srs_info) { + const nextAnswerDate = new Date(kanji.srs_info.next_answer_date); + srsPart = ( + +

    This character is being tracked in SRS!

    +

    + (Next test: ) +

    +
    + ); + } + return ( <> -
    {kanji.character}
    +
    + Debug +
    {JSON.stringify(kanji, null, 2)}
    +
    - {kanji.meanings.map(m => m.meaning).join(", ")} + {kanji.character} - + {kanji.meanings.map((m) => m.meaning).join(", ")} + +
    {srsPart}
    ); } diff --git a/src/components/KanjiList.module.scss b/src/components/KanjiList.module.scss new file mode 100644 index 0000000..128c800 --- /dev/null +++ b/src/components/KanjiList.module.scss @@ -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%); +} diff --git a/src/components/KanjiList.tsx b/src/components/KanjiList.tsx new file mode 100644 index 0000000..7c8a863 --- /dev/null +++ b/src/components/KanjiList.tsx @@ -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 ( + + + + {kanji.character} + + {kanji.meanings[0].meaning} + + #{kanji.most_used_rank} most used + + + + ); + }; + + return ( + <> + + Displaying {data.kanji.length} of {data.count} results. + + +
    +
    + {data.kanji.map((kanji) => { + const active = kanji.character == selectedCharacter; + return renderKanjiItem(kanji, active); + })} +
    +
    + + ); +} diff --git a/src/lib/ConditionalWrapper.tsx b/src/lib/ConditionalWrapper.tsx index 1c875e8..fe1f98b 100644 --- a/src/lib/ConditionalWrapper.tsx +++ b/src/lib/ConditionalWrapper.tsx @@ -1,3 +1,21 @@ -export default function ConditionalWrapper({ condition, wrapper, children }) { - return condition ? wrapper(children) : children -} \ No newline at end of file +export interface ConditionalWrapperProps { + condition: boolean; + wrapper?: (_: S) => T; + elseWrapper?: (_: S) => T; + children: S; +} + +export default function ConditionalWrapper({ + condition, + wrapper, + children, + elseWrapper, +}: ConditionalWrapperProps) { + return condition + ? wrapper + ? wrapper(children) + : children + : elseWrapper + ? elseWrapper(children) + : children; +} diff --git a/src/lib/SelectOnClick.tsx b/src/lib/SelectOnClick.tsx new file mode 100644 index 0000000..1b3f627 --- /dev/null +++ b/src/lib/SelectOnClick.tsx @@ -0,0 +1,30 @@ +import { HTMLAttributes, PropsWithChildren, useCallback, useState } from "react"; + +export default function SelectOnClick({ + children, + ...props +}: PropsWithChildren>) { + const [container, setContainer] = useState(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 ( +
    + {children} +
    + ); +} diff --git a/src/panes/KanjiPane.module.scss b/src/panes/KanjiPane.module.scss index ad8a1d7..e69de29 100644 --- a/src/panes/KanjiPane.module.scss +++ b/src/panes/KanjiPane.module.scss @@ -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%); -} \ No newline at end of file diff --git a/src/panes/KanjiPane.tsx b/src/panes/KanjiPane.tsx index ed500f4..79e9994 100644 --- a/src/panes/KanjiPane.tsx +++ b/src/panes/KanjiPane.tsx @@ -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 ( - <> - - Displaying {data.kanji.length} of {data.count} results. - - -
    -
    - {data.kanji.map((kanji) => { - const active = kanji.character == selectedCharacter; - const className = classNames(styles['kanji-link'], active && styles['kanji-link-active']) - return - - - {kanji.character} - - {kanji.meanings[0].meaning} - - - #{kanji.most_used_rank} most used - - - - - })} -
    -
    - - ); -} - export default function KanjiPane() { const { selectedKanji } = useParams(); const { data, error, isLoading } = useSWR("get_kanji", invoke); + if (error) { + console.error(error); + } + return ( <> - - + + {data && } - + - + {selectedKanji ? : "nothing selected"} - - + + ); } diff --git a/src/types/Kanji.ts b/src/types/Kanji.ts index ba1f8f7..954a0d7 100644 --- a/src/types/Kanji.ts +++ b/src/types/Kanji.ts @@ -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; +}