eslint + more vocab

This commit is contained in:
Michael Zhang 2023-06-14 23:29:59 -05:00
parent 5987e746db
commit 00ec52a9a4
30 changed files with 3846 additions and 660 deletions

14
eslint.config.js Normal file
View file

@ -0,0 +1,14 @@
import js from "@eslint/js";
import reactRecommended from "eslint-plugin-react/configs/recommended.js";
import reactJsxRuntime from "eslint-plugin-react/configs/jsx-runtime.js";
export default [
{ ignores: ["src-tauri/**/*", "dist/**/*"] },
{ settings: { react: { version: "detect" } } },
{
files: ["**/*.js"],
rules: js.configs.recommended.rules,
},
reactRecommended,
reactJsxRuntime,
];

4218
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -18,7 +18,6 @@
"chakra-react-select": "^4.6.0", "chakra-react-select": "^4.6.0",
"classnames": "^2.3.2", "classnames": "^2.3.2",
"date-fns": "^2.30.0", "date-fns": "^2.30.0",
"flowbite": "^1.6.5",
"framer-motion": "^10.12.16", "framer-motion": "^10.12.16",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"react": "^18.2.0", "react": "^18.2.0",
@ -29,19 +28,23 @@
"swr": "^2.1.5" "swr": "^2.1.5"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^8.42.0",
"@tauri-apps/cli": "^1.4.0", "@tauri-apps/cli": "^1.4.0",
"@types/lodash-es": "^4.17.7", "@types/lodash-es": "^4.17.7",
"@types/node": "^18.7.10", "@types/node": "^18.7.10",
"@types/react": "^18.0.15", "@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6", "@types/react-dom": "^18.0.6",
"@types/react-timeago": "^4.1.3", "@types/react-timeago": "^4.1.3",
"@typescript-eslint/eslint-plugin": "^5.59.11",
"@typescript-eslint/parser": "^5.59.11",
"@vitejs/plugin-react": "^3.0.0", "@vitejs/plugin-react": "^3.0.0",
"@vitejs/plugin-react-swc": "^3.3.2", "@vitejs/plugin-react-swc": "^3.3.2",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"eslint": "^8.42.0",
"eslint-plugin-react": "^7.32.2",
"postcss": "^8.4.24", "postcss": "^8.4.24",
"prettier": "^2.8.8", "prettier": "^2.8.8",
"sass": "^1.62.1", "sass": "^1.62.1",
"tailwindcss": "^3.3.2",
"typescript": "^4.9.5", "typescript": "^4.9.5",
"vite": "^4.2.1" "vite": "^4.2.1"
} }

View file

@ -1,6 +0,0 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

1
src-tauri/Cargo.lock generated
View file

@ -1643,6 +1643,7 @@ dependencies = [
"derivative", "derivative",
"dirs", "dirs",
"flate2", "flate2",
"itertools",
"serde", "serde",
"serde_json", "serde_json",
"sqlx", "sqlx",

View file

@ -26,6 +26,7 @@ derivative = "2.2.0"
flate2 = "1.0.26" flate2 = "1.0.26"
base64 = "0.21.2" base64 = "0.21.2"
tantivy = { version = "0.20.2", default-features = false, features = ["memmap2"] } tantivy = { version = "0.20.2", default-features = false, features = ["memmap2"] }
itertools = "0.10.5"
[features] [features]
# this feature is used for production builds or when `devPath` points to the filesystem # this feature is used for production builds or when `devPath` points to the filesystem

View file

@ -50,6 +50,7 @@ fn default_how_many() -> u32 {
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct Kanji { pub struct Kanji {
id: u32,
character: String, character: String,
meanings: Vec<KanjiMeaning>, meanings: Vec<KanjiMeaning>,
srs_info: Option<KanjiSrsInfo>, srs_info: Option<KanjiSrsInfo>,
@ -270,6 +271,7 @@ pub async fn get_kanji(
} }
new_vec.push(Kanji { new_vec.push(Kanji {
id,
character, character,
meanings: vec![meaning], meanings: vec![meaning],

View file

@ -12,13 +12,12 @@ pub mod utils;
pub mod vocab; pub mod vocab;
use std::process; use std::process;
use std::str::FromStr;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions}; use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
use tauri::{ use tauri::{
CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu,
SystemTrayMenuItem, WindowEvent, WindowEvent,
}; };
use tokio::fs; use tokio::fs;
@ -78,6 +77,7 @@ async fn main() -> Result<()> {
srs::generate_review_batch, srs::generate_review_batch,
srs::update_srs_item, srs::update_srs_item,
kanji::get_kanji, kanji::get_kanji,
vocab::get_vocab,
utils::application_info, utils::application_info,
]) ])
.on_window_event(|event| match event.event() { .on_window_event(|event| match event.event() {

View file

@ -1,14 +1,8 @@
use sqlx::Row;
use tauri::State; use tauri::State;
use crate::kanji::KanjiDb; use crate::kanji::KanjiDb;
#[derive(Debug, Derivative, Serialize, Deserialize)]
#[derivative(Default)]
pub struct GetVocabOptions {
#[serde(default)]
kanji_id: Option<u32>,
}
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct Vocab { pub struct Vocab {
kanji_writing: String, kanji_writing: String,
@ -21,8 +15,16 @@ pub struct Vocab {
frequency_rank: u32, frequency_rank: u32,
} }
#[derive(Debug, Derivative, Serialize, Deserialize)]
#[derivative(Default)]
pub struct GetVocabOptions {
#[serde(default)]
kanji_id: Option<u32>,
}
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct GetVocabResult { pub struct GetVocabResult {
count: u32,
vocab: Vec<Vocab>, vocab: Vec<Vocab>,
} }
@ -33,5 +35,46 @@ pub async fn get_vocab(
) -> Result<GetVocabResult, String> { ) -> Result<GetVocabResult, String> {
let opts = options.unwrap_or_default(); let opts = options.unwrap_or_default();
Ok(GetVocabResult { vocab: vec![] }) let query_string = format!(
r#"
"#
);
// Count query
let count = {
let kanji_filter_clause = match opts.kanji_id {
Some(_) => format!(
r#"
JOIN KanjiEntityVocabEntity ON KanjiEntityVocabEntity.Vocabs_ID = VocabSet.ID
WHERE KanjiEntityVocabEntity.Kanji_ID = ?
"#
),
None => String::new(),
};
let count_query_string = format!(
r#"
SELECT COUNT(*) AS Count
FROM VocabSet
{kanji_filter_clause}
"#
);
println!(
"count query: {count_query_string}\n bind {:?}",
opts.kanji_id
);
let mut count_query = sqlx::query(&count_query_string);
if let Some(kanji_id) = opts.kanji_id {
count_query = count_query.bind(kanji_id);
}
let row = count_query
.fetch_one(&kanji_db.0)
.await
.map_err(|err| err.to_string())?;
row.get("Count")
};
Ok(GetVocabResult {
count,
vocab: vec![],
})
} }

View file

@ -1,4 +1,4 @@
import { Link, RouterProvider, createHashRouter } from "react-router-dom"; import { Link, RouterProvider } from "react-router-dom";
import classNames from "classnames"; import classNames from "classnames";
import { ChakraProvider, Flex } from "@chakra-ui/react"; import { ChakraProvider, Flex } from "@chakra-ui/react";
import { createBrowserRouter } from "react-router-dom"; import { createBrowserRouter } from "react-router-dom";

View file

@ -1,12 +1,8 @@
import { Button, Grid, GridItem, Stat, StatLabel, StatNumber, Tooltip } from "@chakra-ui/react"; import { Button, Grid, GridItem, Stat, StatLabel, StatNumber, Tooltip } from "@chakra-ui/react";
import { ArrowRightIcon } from "@chakra-ui/icons"; import { ArrowRightIcon } from "@chakra-ui/icons";
import styles from "./DashboardReviewStats.module.scss";
import useSWR from "swr";
import { invoke } from "@tauri-apps/api/tauri";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import ConditionalWrapper from "./utils/ConditionalWrapper"; import ConditionalWrapper from "./utils/ConditionalWrapper";
import ReactTimeago, { Formatter } from "react-timeago"; import ReactTimeago, { Formatter } from "react-timeago";
import { isValid } from "date-fns";
import { SrsStats } from "../panes/SrsPane"; import { SrsStats } from "../panes/SrsPane";
export interface DashboardReviewStatsProps { export interface DashboardReviewStatsProps {
@ -15,7 +11,7 @@ export interface DashboardReviewStatsProps {
interface Stat { interface Stat {
label: string; label: string;
value: any; value: string | number | JSX.Element;
} }
export default function DashboardReviewStats({ srsStats }: DashboardReviewStatsProps) { export default function DashboardReviewStats({ srsStats }: DashboardReviewStatsProps) {

View file

@ -1,6 +1,9 @@
$kanjiDisplaySize: 120px; $kanjiDisplaySize: 120px;
.main { .main {
width: 100%;
height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16px; gap: 16px;
@ -79,8 +82,15 @@ $kanjiDisplaySize: 120px;
.vocabSection { .vocabSection {
flex-grow: 1; flex-grow: 1;
display: flex;
flex-direction: column;
gap: 8px;
h2 { h2 {
font-size: 1.5em; font-size: 1.5em;
} }
.vocabList {
flex-grow: 1;
}
} }

View file

@ -6,6 +6,7 @@ import SelectOnClick from "./utils/SelectOnClick";
import classNames from "classnames"; import classNames from "classnames";
import Strokes from "./Strokes"; import Strokes from "./Strokes";
import SrsPart from "./SrsPart"; import SrsPart from "./SrsPart";
import VocabList from "./VocabList";
interface KanjiDisplayProps { interface KanjiDisplayProps {
kanjiCharacter: string; kanjiCharacter: string;
@ -44,11 +45,6 @@ export default function KanjiDisplay({ kanjiCharacter }: KanjiDisplayProps) {
return ( return (
<> <>
<details>
<summary>Debug</summary>
<pre>{JSON.stringify(kanji, null, 2)}</pre>
</details>
<main className={styles.main}> <main className={styles.main}>
<div className={styles.topRow}> <div className={styles.topRow}>
<SelectOnClick className={styles.display}>{kanji.character}</SelectOnClick> <SelectOnClick className={styles.display}>{kanji.character}</SelectOnClick>
@ -89,6 +85,8 @@ export default function KanjiDisplay({ kanjiCharacter }: KanjiDisplayProps) {
<div className={styles.vocabSection}> <div className={styles.vocabSection}>
<h2>Related Vocab</h2> <h2>Related Vocab</h2>
<VocabList kanjiId={kanji.id} className={styles.vocabList} />
</div> </div>
</main> </main>
</> </>

View file

@ -1,12 +1,10 @@
import { GetKanjiResult } from "../panes/KanjiPane";
import classNames from "classnames"; import classNames from "classnames";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Badge, Grid, GridItem } from "@chakra-ui/layout"; import { Badge, Grid, GridItem } from "@chakra-ui/layout";
import styles from "./KanjiList.module.scss"; import styles from "./KanjiList.module.scss";
import { Kanji } from "../lib/kanji"; import { Kanji } from "../lib/kanji";
import { Input, Spinner } from "@chakra-ui/react"; import { Spinner } from "@chakra-ui/react";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import SearchBar from "./SearchBar";
import LevelBadge from "./utils/LevelBadge"; import LevelBadge from "./utils/LevelBadge";
export interface KanjiListProps { export interface KanjiListProps {

View file

@ -1,6 +1,6 @@
import { SearchIcon } from "@chakra-ui/icons"; import { SearchIcon } from "@chakra-ui/icons";
import { Input, InputGroup, InputRightElement, Spinner } from "@chakra-ui/react"; import { Input, InputGroup, InputRightElement, Spinner } from "@chakra-ui/react";
import { FormEvent, useEffect, useState } from "react"; import { FormEvent, useState } from "react";
export interface SearchBarProps { export interface SearchBarProps {
isLoading: boolean; isLoading: boolean;

View file

@ -0,0 +1,15 @@
.main {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
gap: 8px;
}
.vocabList {
flex-grow: 1;
border: 1px solid lightgray;
padding: 12px;
overflow-y: scroll;
}

View file

@ -0,0 +1,36 @@
import { Input, StylesProvider } from "@chakra-ui/react";
import { GetVocabResult } from "../lib/vocab";
import { invoke } from "@tauri-apps/api";
import useSWR from "swr";
import styles from "./VocabList.module.scss";
import classNames from "classnames";
export interface VocabListProps {
kanjiId?: number;
className?: string;
}
export default function VocabList({ className, kanjiId }: VocabListProps) {
const { data, isLoading, error } = useSWR(["get_vocab", kanjiId], () =>
invoke<GetVocabResult>("get_vocab", {
options: { kanji_id: kanjiId },
}),
);
if (!data) {
console.error(error);
return;
}
return (
<main className={classNames(styles.main, className)}>
<Input placeholder="Filter..." autoFocus />
<div>
Displaying {data.vocab.length} of {data.count} results. ({kanjiId})
</div>
<div className={styles.vocabList}></div>
</main>
);
}

View file

@ -0,0 +1,3 @@
.badge {
user-select: none;
}

View file

@ -1,11 +1,14 @@
import { Badge, BadgeProps } from "@chakra-ui/react"; import { Badge, BadgeProps } from "@chakra-ui/react";
import { srsLevels } from "../../lib/srs"; import { srsLevels } from "../../lib/srs";
import classNames from "classnames";
import styles from "./LevelBadge.module.scss";
export interface LevelBadgeProps extends BadgeProps { export interface LevelBadgeProps extends BadgeProps {
grade?: number; grade?: number;
} }
export default function LevelBadge({ grade, ...props }: LevelBadgeProps) { export default function LevelBadge({ grade, className, ...props }: LevelBadgeProps) {
if (grade == undefined) return null; if (grade == undefined) return null;
const levelInfo = srsLevels.get(grade); const levelInfo = srsLevels.get(grade);
@ -14,7 +17,12 @@ export default function LevelBadge({ grade, ...props }: LevelBadgeProps) {
const { color, name } = levelInfo; const { color, name } = levelInfo;
return ( return (
<Badge backgroundColor={color} color="white" {...props}> <Badge
backgroundColor={color}
color="white"
className={classNames(styles.badge, className)}
{...props}
>
{name} {name}
</Badge> </Badge>
); );

View file

@ -1,4 +1,5 @@
export interface Kanji { export interface Kanji {
id: number;
character: string; character: string;
meanings: KanjiMeaning[]; meanings: KanjiMeaning[];

6
src/lib/vocab.ts Normal file
View file

@ -0,0 +1,6 @@
export interface GetVocabResult {
count: number;
vocab: Vocab[];
}
export interface Vocab {}

View file

@ -8,9 +8,10 @@
gap: 16px; gap: 16px;
} }
.kanji-pane-list { .kanjiPaneList {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px;
min-height: 0; min-height: 0;
min-width: 280px; min-width: 280px;
@ -19,3 +20,13 @@
.right-side { .right-side {
flex-grow: 5; flex-grow: 5;
} }
.searchContainer {
display: flex;
flex-direction: column;
}
.advancedSearch {
font-size: 0.8em;
cursor: pointer;
}

View file

@ -1,6 +1,6 @@
import { invoke } from "@tauri-apps/api/tauri"; import { invoke } from "@tauri-apps/api/tauri";
import useSWR from "swr"; import useSWR from "swr";
import { Box, Flex, Grid, GridItem, LinkBox, Stack } from "@chakra-ui/layout"; import { Box, Flex } from "@chakra-ui/layout";
import styles from "./KanjiPane.module.scss"; import styles from "./KanjiPane.module.scss";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
@ -9,6 +9,7 @@ import { Kanji } from "../lib/kanji";
import { KanjiList } from "../components/KanjiList"; import { KanjiList } from "../components/KanjiList";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import SearchBar from "../components/SearchBar"; import SearchBar from "../components/SearchBar";
import { QuestionIcon, UpDownIcon } from "@chakra-ui/icons";
export interface GetKanjiResult { export interface GetKanjiResult {
count: number; count: number;
@ -55,9 +56,17 @@ export function Component() {
return ( return (
<Flex className={styles["kanji-pane-container"]}> <Flex className={styles["kanji-pane-container"]}>
<Box className={styles["kanji-pane-list"]}> <Box className={styles.kanjiPaneList}>
<div className={styles["search-container"]}> <div className={styles.searchContainer}>
<SearchBar isLoading={isLoading} setSearchQuery={setSearchQuery} /> <SearchBar isLoading={isLoading} setSearchQuery={setSearchQuery} />
<details>
<summary className={styles.advancedSearch}>Advanced</summary>
<a href="">
Help <QuestionIcon />
</a>
</details>
</div> </div>
{kanjiList && ( {kanjiList && (

View file

@ -1,5 +1,5 @@
import { invoke } from "@tauri-apps/api/tauri"; import { invoke } from "@tauri-apps/api/tauri";
import useSWR, { useSWRConfig } from "swr"; import useSWR from "swr";
import SelectOnClick from "../components/utils/SelectOnClick"; import SelectOnClick from "../components/utils/SelectOnClick";
interface ApplicationInfo { interface ApplicationInfo {

View file

@ -20,11 +20,11 @@ export interface SrsStats {
} }
export function Component() { export function Component() {
const { const { data: srsStats, error } = useSWR(["get_srs_stats"], ([command]) =>
data: srsStats, invoke<SrsStats>(command),
error, );
isLoading,
} = useSWR(["get_srs_stats"], ([command]) => invoke<SrsStats>(command)); if (error) console.error(error);
if (!srsStats) return <>Loading...</>; if (!srsStats) return <>Loading...</>;

View file

@ -1,22 +1,11 @@
import { import { Button, Container, Progress, Spinner, useDisclosure } from "@chakra-ui/react";
Button,
Container,
Input,
InputGroup,
InputLeftElement,
InputRightElement,
Progress,
Spinner,
useDisclosure,
} from "@chakra-ui/react";
import styles from "./SrsReviewPane.module.scss"; import styles from "./SrsReviewPane.module.scss";
import { ChangeEvent, FormEvent, useEffect, useState } from "react"; import { FormEvent, useEffect, useState } from "react";
import { invoke } from "@tauri-apps/api/tauri"; import { invoke } from "@tauri-apps/api/tauri";
import { Link, ScrollRestoration, useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { ArrowBackIcon } from "@chakra-ui/icons"; import { ArrowBackIcon } from "@chakra-ui/icons";
import ConfirmQuitModal from "../components/utils/ConfirmQuitModal"; import ConfirmQuitModal from "../components/utils/ConfirmQuitModal";
import * as _ from "lodash-es"; import * as _ from "lodash-es";
import { romajiToKana } from "../lib/kanaHelper";
import { import {
ReviewItem, ReviewItem,
ReviewItemType, ReviewItemType,
@ -26,12 +15,9 @@ import {
groupUpdatedLevel, groupUpdatedLevel,
isGroupCorrect, isGroupCorrect,
} from "../lib/srs"; } from "../lib/srs";
import classNames from "classnames";
import InputBox from "../components/srsReview/InputBox"; import InputBox from "../components/srsReview/InputBox";
import SelectOnClick from "../components/utils/SelectOnClick"; import SelectOnClick from "../components/utils/SelectOnClick";
const batchSize = 10;
export function Component() { export function Component() {
// null = has not started, (.length == 0) = finished // null = has not started, (.length == 0) = finished
const [reviewQueue, setReviewQueue] = useState<ReviewItem[] | null>(null); const [reviewQueue, setReviewQueue] = useState<ReviewItem[] | null>(null);
@ -124,7 +110,7 @@ export function Component() {
delay: newLevel.delay, delay: newLevel.delay,
}; };
const result = await invoke("update_srs_item", params); await invoke("update_srs_item", params);
} }
// If it's wrong this time // If it's wrong this time

View file

@ -0,0 +1,4 @@
.main {
width: 100%;
padding: 16px;
}

View file

@ -1,5 +1,12 @@
import VocabList from "../components/VocabList";
import styles from "./VocabPane.module.scss";
export function Component() { export function Component() {
return <></>; return (
<main className={styles.main}>
<VocabList />
</main>
);
} }
Component.displayName = "VocabPane"; Component.displayName = "VocabPane";

View file

@ -1,7 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
html, html,
body { body {
width: 100%; width: 100%;

View file

@ -1,10 +0,0 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ["./src/**/*.{html,js,jsx,ts,tsx}", "./node_modules/flowbite/**/*.js"],
theme: {
extend: {},
},
plugins: [require("flowbite/plugin")],
};