infinite scroll

This commit is contained in:
Michael Zhang 2023-06-09 03:41:50 -05:00
parent f9039a9fa4
commit dda25dcd5d
9 changed files with 141 additions and 28 deletions

View file

@ -1,3 +1,4 @@
max_width = 80
tab_spaces = 2
wrap_comments = true
fn_single_line = true

View file

@ -16,16 +16,23 @@ pub struct GetKanjiOptions {
#[serde(default)]
character: Option<String>,
#[serde(default = "default_skip")]
#[derivative(Default(value = "0"))]
skip: u32,
#[serde(default = "default_how_many")]
#[derivative(Default(value = "10"))]
#[derivative(Default(value = "40"))]
how_many: u32,
#[serde(default)]
include_srs_info: bool,
}
fn default_skip() -> u32 {
0
}
fn default_how_many() -> u32 {
10
40
}
#[derive(Debug, Serialize, Deserialize)]
@ -62,6 +69,7 @@ pub async fn get_kanji(
options: Option<GetKanjiOptions>,
) -> Result<GetKanjiResult, String> {
let opts = options.unwrap_or_default();
println!("opts: {opts:?}");
let looking_for_character_clause = match opts.character {
None => String::new(),
@ -80,7 +88,7 @@ pub async fn get_kanji(
FROM KanjiSet
WHERE MostUsedRank IS NOT NULL
{looking_for_character_clause}
ORDER BY MostUsedRank LIMIT ?
ORDER BY MostUsedRank LIMIT ?, ?
) as Kanji
JOIN KanjiMeaningSet ON Kanji.ID = KanjiMeaningSet.Kanji_ID
ORDER BY MostUsedRank, KanjiMeaningSet.ID
@ -94,6 +102,7 @@ pub async fn get_kanji(
if let Some(character) = &opts.character {
query = query.bind(character.clone());
}
query = query.bind(opts.skip);
query = query.bind(opts.how_many);
let result = query

View file

@ -3,9 +3,10 @@ $navLinkColor: hsl(203, 91%, 91%);
.main {
min-height: 0;
display: inline-flex;
display: flex;
flex-direction: column;
height: 100%;
height: 100vh;
}
.header {
@ -23,9 +24,8 @@ $navLinkColor: hsl(203, 91%, 91%);
.body {
min-height: 0;
flex-basis: 0px;
flex-grow: 1;
display: inline-flex;
display: flex;
}
.link {
@ -50,4 +50,4 @@ $navLinkColor: hsl(203, 91%, 91%);
.link-active {
border-top-color: $navLinkAccentColor;
background-color: $navLinkColor;
}
}

View file

@ -60,7 +60,12 @@ export default function App() {
});
} else {
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",
element: <SrsPane />,
subPaths: [
{ key: "index", path: "/srs" },
{ key: "index", path: "/" },
{ key: "review", path: "/srs/review", element: <SrsReviewPane /> },
],
},

View file

@ -9,7 +9,15 @@
.kanji-link {
padding: 4px 8px;
border-left: 4px solid transparent;
transition: background-color 0.1s ease-out, border-left-color 0.1s ease-out;
user-select: none;
-webkit-user-drag: none;
&:not(.kanji-link-active):hover {
border-top-color: transparent;
background-color: #f4f4f4;
}
}
.kanji-list-scroll {
@ -28,4 +36,17 @@
.kanji-link-active {
border-left-color: #09c;
background-color: hsl(203, 91%, 91%);
}
}
.loading {
padding: 4px 8px;
text-align: center;
}
.result-count {
padding: 4px 8px;
}
.search-container {
padding: 4px 8px;
}

View file

@ -4,13 +4,54 @@ 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";
import { Input, Spinner } from "@chakra-ui/react";
import { useCallback, useEffect, useState } from "react";
export interface KanjiListProps {
data: GetKanjiResult;
kanjiList: Kanji[];
totalCount: number;
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 className = classNames(styles["kanji-link"], active && styles["kanji-link-active"]);
return (
@ -21,7 +62,7 @@ export function KanjiList({ data, selectedCharacter }: KanjiListProps) {
</GridItem>
<GridItem>{kanji.meanings[0].meaning}</GridItem>
<GridItem>
<Badge>#{kanji.most_used_rank} most used</Badge>
<Badge>#{kanji.most_used_rank} common</Badge>
</GridItem>
</Grid>
</Link>
@ -30,16 +71,24 @@ export function KanjiList({ data, selectedCharacter }: KanjiListProps) {
return (
<>
<small>
Displaying {data.kanji.length} of {data.count} results.
<div className={styles["search-container"]}>
<Input autoFocus placeholder="Search..." />
</div>
<small className={styles["result-count"]}>
Displaying {kanjiList.length} of {totalCount} results.
</small>
<div className={styles["kanji-list-scroll"]}>
<div className={styles["kanji-list-inner"]}>
{data.kanji.map((kanji) => {
{kanjiList.map((kanji) => {
const active = kanji.character == selectedCharacter;
return renderKanjiItem(kanji, active);
})}
<div className={styles.loading} ref={loadingCanaryRef}>
<Spinner />
</div>
</div>
</div>
</>

View file

@ -1,16 +1,18 @@
.kanji-pane-container {
display: flex;
align-items: stretch;
width: 100%;
height: 100%;
}
.kanji-list {
// display: flex;
// flex-direction: column;
.kanji-pane-list {
display: flex;
flex-direction: column;
min-height: 0;
width: 240px;
min-width: 280px;
}
.right-side {
flex-grow: 5;
}
}

View file

@ -8,6 +8,7 @@ import KanjiDisplay from "../components/KanjiDisplay";
import { Kanji } from "../types/Kanji";
import classNames from "classnames";
import { KanjiList } from "../components/KanjiList";
import { useEffect, useState } from "react";
export interface GetKanjiResult {
count: number;
@ -16,19 +17,43 @@ export interface GetKanjiResult {
export default function KanjiPane() {
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) {
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 (
<Flex gap={3} className={styles['kanji-pane-container']}>
<Box className={styles["kanji-list"]}>
{data && <KanjiList data={data} selectedCharacter={selectedKanji} />}
<Flex gap={3} className={styles["kanji-pane-container"]}>
<Box className={styles["kanji-pane-list"]}>
{kanjiList && (
<KanjiList
kanjiList={kanjiList}
totalCount={totalCount}
selectedCharacter={selectedKanji}
loadMoreKanji={loadMoreKanji}
/>
)}
</Box>
<Box className={styles['right-side']}>
<Box className={styles["right-side"]}>
{selectedKanji ? <KanjiDisplay kanjiCharacter={selectedKanji} /> : "nothing selected"}
</Box>
</Flex>

View file

@ -5,10 +5,11 @@
html,
body {
width: 100%;
height: 100%;
padding: 0;
}
body {
overflow: hidden;
}
}