infinite scroll
This commit is contained in:
parent
cb5340da17
commit
16dfeca43c
9 changed files with 141 additions and 28 deletions
|
@ -1,3 +1,4 @@
|
||||||
max_width = 80
|
max_width = 80
|
||||||
tab_spaces = 2
|
tab_spaces = 2
|
||||||
wrap_comments = true
|
wrap_comments = true
|
||||||
|
fn_single_line = true
|
|
@ -16,16 +16,23 @@ pub struct GetKanjiOptions {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
character: Option<String>,
|
character: Option<String>,
|
||||||
|
|
||||||
|
#[serde(default = "default_skip")]
|
||||||
|
#[derivative(Default(value = "0"))]
|
||||||
|
skip: u32,
|
||||||
|
|
||||||
#[serde(default = "default_how_many")]
|
#[serde(default = "default_how_many")]
|
||||||
#[derivative(Default(value = "10"))]
|
#[derivative(Default(value = "40"))]
|
||||||
how_many: u32,
|
how_many: u32,
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
include_srs_info: bool,
|
include_srs_info: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_skip() -> u32 {
|
||||||
|
0
|
||||||
|
}
|
||||||
fn default_how_many() -> u32 {
|
fn default_how_many() -> u32 {
|
||||||
10
|
40
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
@ -62,6 +69,7 @@ pub async fn get_kanji(
|
||||||
options: Option<GetKanjiOptions>,
|
options: Option<GetKanjiOptions>,
|
||||||
) -> Result<GetKanjiResult, String> {
|
) -> Result<GetKanjiResult, String> {
|
||||||
let opts = options.unwrap_or_default();
|
let opts = options.unwrap_or_default();
|
||||||
|
println!("opts: {opts:?}");
|
||||||
|
|
||||||
let looking_for_character_clause = match opts.character {
|
let looking_for_character_clause = match opts.character {
|
||||||
None => String::new(),
|
None => String::new(),
|
||||||
|
@ -80,7 +88,7 @@ pub async fn get_kanji(
|
||||||
FROM KanjiSet
|
FROM KanjiSet
|
||||||
WHERE MostUsedRank IS NOT NULL
|
WHERE MostUsedRank IS NOT NULL
|
||||||
{looking_for_character_clause}
|
{looking_for_character_clause}
|
||||||
ORDER BY MostUsedRank LIMIT ?
|
ORDER BY MostUsedRank LIMIT ?, ?
|
||||||
) as Kanji
|
) as Kanji
|
||||||
JOIN KanjiMeaningSet ON Kanji.ID = KanjiMeaningSet.Kanji_ID
|
JOIN KanjiMeaningSet ON Kanji.ID = KanjiMeaningSet.Kanji_ID
|
||||||
ORDER BY MostUsedRank, KanjiMeaningSet.ID
|
ORDER BY MostUsedRank, KanjiMeaningSet.ID
|
||||||
|
@ -94,6 +102,7 @@ pub async fn get_kanji(
|
||||||
if let Some(character) = &opts.character {
|
if let Some(character) = &opts.character {
|
||||||
query = query.bind(character.clone());
|
query = query.bind(character.clone());
|
||||||
}
|
}
|
||||||
|
query = query.bind(opts.skip);
|
||||||
query = query.bind(opts.how_many);
|
query = query.bind(opts.how_many);
|
||||||
|
|
||||||
let result = query
|
let result = query
|
||||||
|
|
|
@ -3,9 +3,10 @@ $navLinkColor: hsl(203, 91%, 91%);
|
||||||
|
|
||||||
.main {
|
.main {
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
display: inline-flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
|
||||||
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
|
@ -23,9 +24,8 @@ $navLinkColor: hsl(203, 91%, 91%);
|
||||||
|
|
||||||
.body {
|
.body {
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
flex-basis: 0px;
|
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
display: inline-flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.link {
|
.link {
|
||||||
|
|
|
@ -60,7 +60,12 @@ export default function App() {
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<Route key={`route-${route.key}`} index={idx == 0} path={route.path} element={route.element} />
|
<Route
|
||||||
|
key={`route-${route.key}`}
|
||||||
|
index={idx == 0}
|
||||||
|
path={route.path}
|
||||||
|
element={route.element}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
|
@ -84,7 +89,7 @@ const navLinks = [
|
||||||
title: "SRS",
|
title: "SRS",
|
||||||
element: <SrsPane />,
|
element: <SrsPane />,
|
||||||
subPaths: [
|
subPaths: [
|
||||||
{ key: "index", path: "/srs" },
|
{ key: "index", path: "/" },
|
||||||
{ key: "review", path: "/srs/review", element: <SrsReviewPane /> },
|
{ key: "review", path: "/srs/review", element: <SrsReviewPane /> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
@ -9,7 +9,15 @@
|
||||||
.kanji-link {
|
.kanji-link {
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
border-left: 4px solid transparent;
|
border-left: 4px solid transparent;
|
||||||
|
transition: background-color 0.1s ease-out, border-left-color 0.1s ease-out;
|
||||||
|
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
|
||||||
|
&:not(.kanji-link-active):hover {
|
||||||
|
border-top-color: transparent;
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.kanji-list-scroll {
|
.kanji-list-scroll {
|
||||||
|
@ -29,3 +37,16 @@
|
||||||
border-left-color: #09c;
|
border-left-color: #09c;
|
||||||
background-color: hsl(203, 91%, 91%);
|
background-color: hsl(203, 91%, 91%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
padding: 4px 8px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-count {
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-container {
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
|
@ -4,13 +4,54 @@ 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 "../types/Kanji";
|
import { Kanji } from "../types/Kanji";
|
||||||
|
import { Input, Spinner } from "@chakra-ui/react";
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
export interface KanjiListProps {
|
export interface KanjiListProps {
|
||||||
data: GetKanjiResult;
|
kanjiList: Kanji[];
|
||||||
|
totalCount: number;
|
||||||
selectedCharacter?: string;
|
selectedCharacter?: string;
|
||||||
|
loadMoreKanji: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function KanjiList({ data, selectedCharacter }: KanjiListProps) {
|
export function KanjiList({
|
||||||
|
kanjiList,
|
||||||
|
totalCount,
|
||||||
|
selectedCharacter,
|
||||||
|
loadMoreKanji,
|
||||||
|
}: KanjiListProps) {
|
||||||
|
// Set up intersection observer
|
||||||
|
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
||||||
|
const [loadingCanary, setLoadingCanary] = useState(null);
|
||||||
|
const loadingCanaryRef = useCallback(
|
||||||
|
(element) => {
|
||||||
|
if (element) setLoadingCanary(element);
|
||||||
|
},
|
||||||
|
[setLoadingCanary],
|
||||||
|
);
|
||||||
|
useEffect(() => {
|
||||||
|
if (loadingCanary && !isLoadingMore) {
|
||||||
|
const observer = new IntersectionObserver((entries) => {
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
console.log("loading more shit");
|
||||||
|
loadMoreKanji();
|
||||||
|
setIsLoadingMore(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
observer.observe(loadingCanary);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.unobserve(loadingCanary);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [loadingCanary, isLoadingMore]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsLoadingMore(false);
|
||||||
|
}, [kanjiList]);
|
||||||
|
|
||||||
const renderKanjiItem = (kanji: Kanji, active: boolean) => {
|
const renderKanjiItem = (kanji: Kanji, active: boolean) => {
|
||||||
const className = classNames(styles["kanji-link"], active && styles["kanji-link-active"]);
|
const className = classNames(styles["kanji-link"], active && styles["kanji-link-active"]);
|
||||||
return (
|
return (
|
||||||
|
@ -21,7 +62,7 @@ export function KanjiList({ data, selectedCharacter }: KanjiListProps) {
|
||||||
</GridItem>
|
</GridItem>
|
||||||
<GridItem>{kanji.meanings[0].meaning}</GridItem>
|
<GridItem>{kanji.meanings[0].meaning}</GridItem>
|
||||||
<GridItem>
|
<GridItem>
|
||||||
<Badge>#{kanji.most_used_rank} most used</Badge>
|
<Badge>#{kanji.most_used_rank} common</Badge>
|
||||||
</GridItem>
|
</GridItem>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -30,16 +71,24 @@ export function KanjiList({ data, selectedCharacter }: KanjiListProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<small>
|
<div className={styles["search-container"]}>
|
||||||
Displaying {data.kanji.length} of {data.count} results.
|
<Input autoFocus placeholder="Search..." />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<small className={styles["result-count"]}>
|
||||||
|
Displaying {kanjiList.length} of {totalCount} results.
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
<div className={styles["kanji-list-scroll"]}>
|
<div className={styles["kanji-list-scroll"]}>
|
||||||
<div className={styles["kanji-list-inner"]}>
|
<div className={styles["kanji-list-inner"]}>
|
||||||
{data.kanji.map((kanji) => {
|
{kanjiList.map((kanji) => {
|
||||||
const active = kanji.character == selectedCharacter;
|
const active = kanji.character == selectedCharacter;
|
||||||
return renderKanjiItem(kanji, active);
|
return renderKanjiItem(kanji, active);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
<div className={styles.loading} ref={loadingCanaryRef}>
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
.kanji-pane-container {
|
.kanji-pane-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.kanji-list {
|
.kanji-pane-list {
|
||||||
// display: flex;
|
display: flex;
|
||||||
// flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
width: 240px;
|
min-width: 280px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.right-side {
|
.right-side {
|
||||||
|
|
|
@ -8,6 +8,7 @@ import KanjiDisplay from "../components/KanjiDisplay";
|
||||||
import { Kanji } from "../types/Kanji";
|
import { Kanji } from "../types/Kanji";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { KanjiList } from "../components/KanjiList";
|
import { KanjiList } from "../components/KanjiList";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export interface GetKanjiResult {
|
export interface GetKanjiResult {
|
||||||
count: number;
|
count: number;
|
||||||
|
@ -16,19 +17,43 @@ export interface GetKanjiResult {
|
||||||
|
|
||||||
export default function KanjiPane() {
|
export default function KanjiPane() {
|
||||||
const { selectedKanji } = useParams();
|
const { selectedKanji } = useParams();
|
||||||
const { data, error, isLoading } = useSWR("get_kanji", invoke<GetKanjiResult>);
|
const { data: baseData, error, isLoading } = useSWR("get_kanji", invoke<GetKanjiResult>);
|
||||||
|
|
||||||
|
const [totalCount, setTotalCount] = useState(0);
|
||||||
|
const [kanjiList, setKanjiList] = useState<Kanji[]>([]);
|
||||||
|
|
||||||
|
// Set the base info
|
||||||
|
useEffect(() => {
|
||||||
|
if (baseData) {
|
||||||
|
setTotalCount(baseData.count);
|
||||||
|
setKanjiList(baseData.kanji);
|
||||||
|
}
|
||||||
|
}, [baseData]);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loadMoreKanji = async () => {
|
||||||
|
const result = await invoke<GetKanjiResult>("get_kanji", { options: {skip: kanjiList.length }});
|
||||||
|
console.log("invoked result", result);
|
||||||
|
setKanjiList([...kanjiList, ...result.kanji])
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex gap={3} className={styles['kanji-pane-container']}>
|
<Flex gap={3} className={styles["kanji-pane-container"]}>
|
||||||
<Box className={styles["kanji-list"]}>
|
<Box className={styles["kanji-pane-list"]}>
|
||||||
{data && <KanjiList data={data} selectedCharacter={selectedKanji} />}
|
{kanjiList && (
|
||||||
|
<KanjiList
|
||||||
|
kanjiList={kanjiList}
|
||||||
|
totalCount={totalCount}
|
||||||
|
selectedCharacter={selectedKanji}
|
||||||
|
loadMoreKanji={loadMoreKanji}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box className={styles['right-side']}>
|
<Box className={styles["right-side"]}>
|
||||||
{selectedKanji ? <KanjiDisplay kanjiCharacter={selectedKanji} /> : "nothing selected"}
|
{selectedKanji ? <KanjiDisplay kanjiCharacter={selectedKanji} /> : "nothing selected"}
|
||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue