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",
"classnames": "^2.3.2",
"date-fns": "^2.30.0",
"flowbite": "^1.6.5",
"framer-motion": "^10.12.16",
"lodash-es": "^4.17.21",
"react": "^18.2.0",
@ -29,19 +28,23 @@
"swr": "^2.1.5"
},
"devDependencies": {
"@eslint/js": "^8.42.0",
"@tauri-apps/cli": "^1.4.0",
"@types/lodash-es": "^4.17.7",
"@types/node": "^18.7.10",
"@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6",
"@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-swc": "^3.3.2",
"autoprefixer": "^10.4.14",
"eslint": "^8.42.0",
"eslint-plugin-react": "^7.32.2",
"postcss": "^8.4.24",
"prettier": "^2.8.8",
"sass": "^1.62.1",
"tailwindcss": "^3.3.2",
"typescript": "^4.9.5",
"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",
"dirs",
"flate2",
"itertools",
"serde",
"serde_json",
"sqlx",

View file

@ -26,6 +26,7 @@ derivative = "2.2.0"
flate2 = "1.0.26"
base64 = "0.21.2"
tantivy = { version = "0.20.2", default-features = false, features = ["memmap2"] }
itertools = "0.10.5"
[features]
# 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)]
pub struct Kanji {
id: u32,
character: String,
meanings: Vec<KanjiMeaning>,
srs_info: Option<KanjiSrsInfo>,
@ -270,6 +271,7 @@ pub async fn get_kanji(
}
new_vec.push(Kanji {
id,
character,
meanings: vec![meaning],

View file

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

View file

@ -1,14 +1,8 @@
use sqlx::Row;
use tauri::State;
use crate::kanji::KanjiDb;
#[derive(Debug, Derivative, Serialize, Deserialize)]
#[derivative(Default)]
pub struct GetVocabOptions {
#[serde(default)]
kanji_id: Option<u32>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Vocab {
kanji_writing: String,
@ -21,8 +15,16 @@ pub struct Vocab {
frequency_rank: u32,
}
#[derive(Debug, Derivative, Serialize, Deserialize)]
#[derivative(Default)]
pub struct GetVocabOptions {
#[serde(default)]
kanji_id: Option<u32>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GetVocabResult {
count: u32,
vocab: Vec<Vocab>,
}
@ -33,5 +35,46 @@ pub async fn get_vocab(
) -> Result<GetVocabResult, String> {
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 { ChakraProvider, Flex } from "@chakra-ui/react";
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 { 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 ConditionalWrapper from "./utils/ConditionalWrapper";
import ReactTimeago, { Formatter } from "react-timeago";
import { isValid } from "date-fns";
import { SrsStats } from "../panes/SrsPane";
export interface DashboardReviewStatsProps {
@ -15,7 +11,7 @@ export interface DashboardReviewStatsProps {
interface Stat {
label: string;
value: any;
value: string | number | JSX.Element;
}
export default function DashboardReviewStats({ srsStats }: DashboardReviewStatsProps) {

View file

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

View file

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

View file

@ -1,12 +1,10 @@
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 "../lib/kanji";
import { Input, Spinner } from "@chakra-ui/react";
import { Spinner } from "@chakra-ui/react";
import { useCallback, useEffect, useState } from "react";
import SearchBar from "./SearchBar";
import LevelBadge from "./utils/LevelBadge";
export interface KanjiListProps {

View file

@ -1,6 +1,6 @@
import { SearchIcon } from "@chakra-ui/icons";
import { Input, InputGroup, InputRightElement, Spinner } from "@chakra-ui/react";
import { FormEvent, useEffect, useState } from "react";
import { FormEvent, useState } from "react";
export interface SearchBarProps {
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 { srsLevels } from "../../lib/srs";
import classNames from "classnames";
import styles from "./LevelBadge.module.scss";
export interface LevelBadgeProps extends BadgeProps {
grade?: number;
}
export default function LevelBadge({ grade, ...props }: LevelBadgeProps) {
export default function LevelBadge({ grade, className, ...props }: LevelBadgeProps) {
if (grade == undefined) return null;
const levelInfo = srsLevels.get(grade);
@ -14,7 +17,12 @@ export default function LevelBadge({ grade, ...props }: LevelBadgeProps) {
const { color, name } = levelInfo;
return (
<Badge backgroundColor={color} color="white" {...props}>
<Badge
backgroundColor={color}
color="white"
className={classNames(styles.badge, className)}
{...props}
>
{name}
</Badge>
);

View file

@ -1,4 +1,5 @@
export interface Kanji {
id: number;
character: string;
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;
}
.kanji-pane-list {
.kanjiPaneList {
display: flex;
flex-direction: column;
gap: 12px;
min-height: 0;
min-width: 280px;
@ -19,3 +20,13 @@
.right-side {
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 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 { useParams } from "react-router-dom";
@ -9,6 +9,7 @@ import { Kanji } from "../lib/kanji";
import { KanjiList } from "../components/KanjiList";
import { useEffect, useState } from "react";
import SearchBar from "../components/SearchBar";
import { QuestionIcon, UpDownIcon } from "@chakra-ui/icons";
export interface GetKanjiResult {
count: number;
@ -55,9 +56,17 @@ export function Component() {
return (
<Flex className={styles["kanji-pane-container"]}>
<Box className={styles["kanji-pane-list"]}>
<div className={styles["search-container"]}>
<Box className={styles.kanjiPaneList}>
<div className={styles.searchContainer}>
<SearchBar isLoading={isLoading} setSearchQuery={setSearchQuery} />
<details>
<summary className={styles.advancedSearch}>Advanced</summary>
<a href="">
Help <QuestionIcon />
</a>
</details>
</div>
{kanjiList && (

View file

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

View file

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

View file

@ -1,22 +1,11 @@
import {
Button,
Container,
Input,
InputGroup,
InputLeftElement,
InputRightElement,
Progress,
Spinner,
useDisclosure,
} from "@chakra-ui/react";
import { Button, Container, Progress, Spinner, useDisclosure } from "@chakra-ui/react";
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 { Link, ScrollRestoration, useNavigate } from "react-router-dom";
import { useNavigate } from "react-router-dom";
import { ArrowBackIcon } from "@chakra-ui/icons";
import ConfirmQuitModal from "../components/utils/ConfirmQuitModal";
import * as _ from "lodash-es";
import { romajiToKana } from "../lib/kanaHelper";
import {
ReviewItem,
ReviewItemType,
@ -26,12 +15,9 @@ import {
groupUpdatedLevel,
isGroupCorrect,
} from "../lib/srs";
import classNames from "classnames";
import InputBox from "../components/srsReview/InputBox";
import SelectOnClick from "../components/utils/SelectOnClick";
const batchSize = 10;
export function Component() {
// null = has not started, (.length == 0) = finished
const [reviewQueue, setReviewQueue] = useState<ReviewItem[] | null>(null);
@ -124,7 +110,7 @@ export function Component() {
delay: newLevel.delay,
};
const result = await invoke("update_srs_item", params);
await invoke("update_srs_item", params);
}
// 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() {
return <></>;
return (
<main className={styles.main}>
<VocabList />
</main>
);
}
Component.displayName = "VocabPane";

View file

@ -1,7 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
html,
body {
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")],
};