From 011a964c23f1665f41b1b56f5079b60cf3bd96c5 Mon Sep 17 00:00:00 2001 From: Justin Silvestre Date: Mon, 25 Nov 2024 17:05:35 +0100 Subject: [PATCH] Analysis, popper ux, keyword, and copy tweaks --- app/components/FigurePopover.tsx | 49 ++++- app/components/fadeInOut.module.css | 11 +- app/components/popoverFadeIn.module.css | 5 +- .../browse/getAtomicFigureBadgeFigures.ts | 145 ++----------- ...dgeFiguresByPriorityGroupedWithVariants.ts | 130 +++++++++++ app/features/browse/useManyFiguresPopover.ts | 2 + app/features/dictionary/RadicalSection.tsx | 15 +- .../SingleFigureDictionaryEntry.tsx | 4 +- app/lib/dic/componentsDictionary.yml | 24 +-- app/lib/dic/kanjijumpSpecificVariants.ts | 9 +- app/lib/dic/kanjivgExtractedComponents.ts | 4 +- app/lib/patchKanjiDbIds.ts | 23 +- app/routes/FiguresGroupedByVariantsList.tsx | 104 +++++++++ app/routes/browse.atomic-components.tsx | 82 +------ app/routes/browse.compound-components.tsx | 202 +++++++++++------- 15 files changed, 489 insertions(+), 320 deletions(-) create mode 100644 app/features/browse/getBadgeFiguresByPriorityGroupedWithVariants.ts create mode 100644 app/routes/FiguresGroupedByVariantsList.tsx diff --git a/app/components/FigurePopover.tsx b/app/components/FigurePopover.tsx index 5065a4c2..489ba6b5 100644 --- a/app/components/FigurePopover.tsx +++ b/app/components/FigurePopover.tsx @@ -1,5 +1,11 @@ import clsx from "clsx"; -import { PropsWithChildren, useEffect, createElement, useState } from "react"; +import { + PropsWithChildren, + useEffect, + createElement, + useState, + useRef, +} from "react"; import { createPortal } from "react-dom"; import { useFetcher } from "react-router"; @@ -79,6 +85,31 @@ export function useFigurePopover({ const popper = usePaddedPopper(); const { setReferenceElement, isOpen, open, close, update } = popper; + const [isClosing, setClosing] = useState(false); + const closeWarnTimer = useRef(null); + const closeTimer = useRef(null); + const scheduleClose = () => { + cancelClose(); + + closeWarnTimer.current = window.setTimeout(() => { + setClosing(true); + closeTimer.current = window.setTimeout(() => { + close(); + }, 2000); + }, 15000); + }; + const cancelClose = () => { + if (closeWarnTimer.current) { + window.clearTimeout(closeWarnTimer.current); + closeWarnTimer.current = null; + } + if (closeTimer.current) { + window.clearTimeout(closeTimer.current); + closeTimer.current = null; + } + setClosing(false); + }; + const { fetcher, loadFigure, badgeProps } = usePopoverFigureFetcher(initialBadgeProps); @@ -108,10 +139,17 @@ export function useFigurePopover({ open(); } }, + onMouseLeave: () => { + scheduleClose(); + }, + onMouseOver: () => { + cancelClose(); + }, }); return { figure: fetchedFigure, + isClosing, loadFigure, fetcher, badgeProps, @@ -127,12 +165,14 @@ export function FigurePopoverWindow({ figure, popper, fetcher, + isClosing, }: { badgeProps?: BadgeProps | null; loadFigure: ReturnType["loadFigure"]; figure: PopoverFigure | undefined; popper: ReturnType; fetcher: ReturnType["fetcher"]; + isClosing?: boolean; }) { const firstClassComponents = figure?.firstClassComponents; const headingsMeanings = figure ? getHeadingsMeanings(figure) : null; @@ -145,7 +185,7 @@ export function FigurePopoverWindow({ return ( // eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events - (
@@ -234,7 +277,7 @@ export function FigurePopoverWindow({ />
- ) + ); } diff --git a/app/components/fadeInOut.module.css b/app/components/fadeInOut.module.css index d097c833..d8cb6735 100644 --- a/app/components/fadeInOut.module.css +++ b/app/components/fadeInOut.module.css @@ -2,26 +2,19 @@ 0% { opacity: 0; pointer-events: none; - transform: scaleY(0%) scaleX(0%) } 80% { + opacity: 1; pointer-events: none; - transform: scaleY(1) - } 81% { pointer-events: all; } - 100% { - opacity: 1; - - } } .fadeIn { - animation: fadeIn 0.4s; - + animation: fadeIn 0.3s; } .fadeOut { diff --git a/app/components/popoverFadeIn.module.css b/app/components/popoverFadeIn.module.css index eab5a667..39375ebe 100644 --- a/app/components/popoverFadeIn.module.css +++ b/app/components/popoverFadeIn.module.css @@ -1,7 +1,7 @@ @keyframes fadeIn { from { opacity: 0; - transform: scaleY(0) translateY(-.25em); + transform: scaleY(0) translateY(-3em); } to { opacity: 1; @@ -11,7 +11,6 @@ .fadeIn { transform-origin: top; - transition: opacity; animation-name: fadeIn; - animation-duration: .5s; + animation-duration: .3s; } diff --git a/app/features/browse/getAtomicFigureBadgeFigures.ts b/app/features/browse/getAtomicFigureBadgeFigures.ts index 280c0b4d..851c2e63 100644 --- a/app/features/browse/getAtomicFigureBadgeFigures.ts +++ b/app/features/browse/getAtomicFigureBadgeFigures.ts @@ -1,16 +1,8 @@ -import { PrismaClient } from "@prisma/client"; +import type { PrismaClient } from "@prisma/client"; -import { - BadgeProps, - badgeFigureSelect, - getBadgeProps, -} from "~/features/dictionary/badgeFigure"; -import { - FIGURES_VERSION, - FigureKey, - getLatestFigureId, - parseFigureId, -} from "~/models/figure"; +import { FIGURES_VERSION } from "~/models/figure"; + +import { getBadgeFiguresByPriorityGroupedWithVariants } from "./getBadgeFiguresByPriorityGroupedWithVariants"; const isPriorityComponentWhere = { isPriority: true, @@ -25,120 +17,21 @@ const isPriorityComponentWhere = { }; export async function getAtomicFigureBadgeFigures(prisma: PrismaClient) { - const priorityAtomicComponents = await prisma.kanjisenseFigure.findMany({ - select: { - ...badgeFigureSelect, - keyword: true, - mnemonicKeyword: true, - image: true, - }, - orderBy: { aozoraAppearances: "desc" }, - where: { - version: FIGURES_VERSION, - OR: [ - { - ...isPriorityComponentWhere, - componentsTree: { equals: [] }, - }, - { - listsAsCharacter: { isEmpty: false }, - componentsTree: { equals: [] }, - }, - ], - }, - }); - - type QueriedFigure = (typeof priorityAtomicComponents)[number]; - - const atomicFiguresMap: Record = {}; - const nonAtomicVariantsMap: Record = {}; - const variantGroupHeads = new Set(); - - const primaryVariantToRanking: Record = {}; - - for (const figure of priorityAtomicComponents) { - atomicFiguresMap[figure.key] = figure; - if (figure.variantGroupId) variantGroupHeads.add(figure.variantGroupId); - else primaryVariantToRanking[figure.key] = figure.aozoraAppearances ?? 0; - } - const variantGroupsRankings = await prisma.kanjisenseFigure.groupBy({ - by: ["variantGroupId"], - where: { - variantGroupId: { in: [...variantGroupHeads] }, - }, - _sum: { aozoraAppearances: true }, - }); - for (const group of variantGroupsRankings) { - const variantGroupKey = parseFigureId(group.variantGroupId!).key; - const appearances = group._sum?.aozoraAppearances ?? 0; - primaryVariantToRanking[variantGroupKey] = appearances ?? 0; - } - - const variantFigures = await prisma.kanjisenseVariantGroup.findMany({ - where: { id: { in: [...variantGroupHeads] } }, - include: { - figures: { - where: { - version: FIGURES_VERSION, - key: { - notIn: [...priorityAtomicComponents.map((figure) => figure.key)], - }, - listsAsComponent: { isEmpty: false }, - }, - select: { - ...badgeFigureSelect, - keyword: true, - mnemonicKeyword: true, - image: true, - }, + return await getBadgeFiguresByPriorityGroupedWithVariants(prisma, { + version: FIGURES_VERSION, + OR: [ + { + ...isPriorityComponentWhere, + componentsTree: { equals: [] }, }, - }, + { + listsAsCharacter: { isEmpty: false }, + componentsTree: { equals: [] }, + }, + // { + // isStandaloneCharacter: true, + // componentsTree: { equals: [] }, + // }, + ], }); - for (const group of variantFigures) { - for (const figure of group.figures) { - nonAtomicVariantsMap[figure.key] = figure; - } - } - - const groups: { - key: string; - appearances: number; - keyword: string; - mnemonicKeyword: string | null; - figures: { - isAtomic: boolean; - figure: BadgeProps; - }[]; - }[] = []; - for (const [key, ranking] of Object.entries(primaryVariantToRanking)) { - const isGroup = variantGroupHeads.has(getLatestFigureId(key)); - const figures = isGroup - ? variantFigures - .find((group) => group.key === key)! - .variants.flatMap( - (v) => atomicFiguresMap[v] || nonAtomicVariantsMap[v] || [], - ) - : [atomicFiguresMap[key] || nonAtomicVariantsMap[key]]; - - const groupHead = figures[0]; - if (!groupHead) console.error("No group head for", key); - - groups.push({ - key, - appearances: ranking, - keyword: groupHead.keyword, - mnemonicKeyword: groupHead.mnemonicKeyword, - figures: figures.map((figure) => ({ - isAtomic: Boolean(atomicFiguresMap[figure.key]), - figure: getBadgeProps(figure), - })), - }); - } - - return { - atomicComponentsAndVariants: groups.sort( - (a, b) => b.appearances - a.appearances, - ), - totalAtomicComponents: priorityAtomicComponents.length, - }; } diff --git a/app/features/browse/getBadgeFiguresByPriorityGroupedWithVariants.ts b/app/features/browse/getBadgeFiguresByPriorityGroupedWithVariants.ts new file mode 100644 index 00000000..18025cd9 --- /dev/null +++ b/app/features/browse/getBadgeFiguresByPriorityGroupedWithVariants.ts @@ -0,0 +1,130 @@ +import type { Prisma, PrismaClient } from "@prisma/client"; + +import { + FigureKey, + parseFigureId, + FIGURES_VERSION, + getLatestFigureId, +} from "~/models/figure"; + +import { + badgeFigureSelect, + BadgeProps, + getBadgeProps, +} from "../dictionary/badgeFigure"; + +export async function getBadgeFiguresByPriorityGroupedWithVariants< + Q extends Prisma.KanjisenseFigureWhereInput, +>(prisma: PrismaClient, whereQuery: Q) { + const matchedFigures = await prisma.kanjisenseFigure.findMany({ + select: { + ...badgeFigureSelect, + keyword: true, + mnemonicKeyword: true, + image: true, + }, + orderBy: { aozoraAppearances: "desc" }, + where: whereQuery, + }); + + type QueriedFigure = (typeof matchedFigures)[number]; + + const matchedFiguresMap: Record = {}; + const nonMatchedFiguresMap: Record = {}; + const variantGroupHeads = new Set(); + + const primaryVariantToRanking: Record = {}; + + for (const figure of matchedFigures) { + matchedFiguresMap[figure.key] = figure; + if (figure.variantGroupId) variantGroupHeads.add(figure.variantGroupId); + else primaryVariantToRanking[figure.key] = figure.aozoraAppearances ?? 0; + } + const variantGroupsRankings = await prisma.kanjisenseFigure.groupBy({ + by: ["variantGroupId"], + where: { + variantGroupId: { in: [...variantGroupHeads] }, + }, + _sum: { aozoraAppearances: true }, + }); + for (const group of variantGroupsRankings) { + const variantGroupKey = parseFigureId(group.variantGroupId!).key; + const appearances = group._sum?.aozoraAppearances ?? 0; + primaryVariantToRanking[variantGroupKey] = appearances ?? 0; + } + + const variantFigures = await prisma.kanjisenseVariantGroup.findMany({ + where: { id: { in: [...variantGroupHeads] } }, + include: { + figures: { + where: { + version: FIGURES_VERSION, + key: { + notIn: [...matchedFigures.map((figure) => figure.key)], + }, + OR: [ + { + listsAsCharacter: { isEmpty: false }, + }, + { + listsAsComponent: { isEmpty: false }, + }, + ], + }, + select: { + ...badgeFigureSelect, + keyword: true, + mnemonicKeyword: true, + image: true, + }, + }, + }, + }); + for (const group of variantFigures) { + for (const figure of group.figures) { + nonMatchedFiguresMap[figure.key] = figure; + } + } + + const groups: { + key: string; + appearances: number; + keyword: string; + mnemonicKeyword: string | null; + figures: { + isMatch: boolean; + figure: BadgeProps; + }[]; + }[] = []; + for (const [key, ranking] of Object.entries(primaryVariantToRanking)) { + const isGroup = variantGroupHeads.has(getLatestFigureId(key)); + const figures = isGroup + ? variantFigures + .find((group) => group.key === key)! + .variants.flatMap( + (v) => matchedFiguresMap[v] || nonMatchedFiguresMap[v] || [], + ) + : [matchedFiguresMap[key] || nonMatchedFiguresMap[key]]; + + const groupHead = figures[0]; + if (!groupHead) console.error("No group head for", key); + + groups.push({ + key, + appearances: ranking, + keyword: groupHead.keyword, + mnemonicKeyword: groupHead.mnemonicKeyword, + figures: figures.map((figure) => ({ + isMatch: Boolean(matchedFiguresMap[figure.key]), + figure: getBadgeProps(figure), + })), + }); + } + + return { + matchedFiguresAndVariants: groups.sort( + (a, b) => b.appearances - a.appearances, + ), + matchedFiguresCount: matchedFigures.length, + }; +} diff --git a/app/features/browse/useManyFiguresPopover.ts b/app/features/browse/useManyFiguresPopover.ts index ad277881..d3b2f9f6 100644 --- a/app/features/browse/useManyFiguresPopover.ts +++ b/app/features/browse/useManyFiguresPopover.ts @@ -3,6 +3,8 @@ import React from "react"; import { useFigurePopover } from "~/components/FigurePopover"; import { BadgeProps } from "~/features/dictionary/badgeFigure"; +export type ManyFiguresPopover = ReturnType; + export function useManyFiguresPopover() { const figurePopover = useFigurePopover(); diff --git a/app/features/dictionary/RadicalSection.tsx b/app/features/dictionary/RadicalSection.tsx index 7eb8c0b2..09b69802 100644 --- a/app/features/dictionary/RadicalSection.tsx +++ b/app/features/dictionary/RadicalSection.tsx @@ -1,5 +1,6 @@ /* eslint-disable react/no-unescaped-entities */ import clsx from "clsx"; +import { Fragment } from "react"; import { DictLink } from "~/components/AppLink"; import { PopperOptions } from "~/components/usePaddedPopper"; @@ -73,12 +74,14 @@ export function RadicalSection({ )} >

- {radicalIndexes - .map( - (r) => - `radical #${r.radical.number} ${r.radical.character} + ${r.remainder} additional strokes`, - ) - .join(", ")} + {radicalIndexes.map((r, i) => ( + + Kangxi radical: {r.radical.character} (number{" "} + {r.radical.number}) +
+ additional strokes: {r.remainder} +
+ ))}

Knowing a character's traditional "radical" is not as useful today diff --git a/app/features/dictionary/SingleFigureDictionaryEntry.tsx b/app/features/dictionary/SingleFigureDictionaryEntry.tsx index 1f0bb8d1..c76a5d69 100644 --- a/app/features/dictionary/SingleFigureDictionaryEntry.tsx +++ b/app/features/dictionary/SingleFigureDictionaryEntry.tsx @@ -68,12 +68,12 @@ export function SingleFigureDictionaryEntry({ >

-
+
setAnimationIsShowing((b) => !b)} diff --git a/app/lib/dic/componentsDictionary.yml b/app/lib/dic/componentsDictionary.yml index 87849efd..8759ce20 100644 --- a/app/lib/dic/componentsDictionary.yml +++ b/app/lib/dic/componentsDictionary.yml @@ -782,8 +782,8 @@ CDP-8BD6: # 奥,向,CDP-8D7C,襖,奧 mnemonic: chopping hand tag: hands 夭: - historical: youth - tag: null + historical: calamity + tag: nature CDP-8BB8: # 愛,受,舜,曖,授,綬,瞬 historical: (various) mnemonic: crown of thorns @@ -793,8 +793,7 @@ CDP-8BB8: # 愛,受,舜,曖,授,綬,瞬 mnemonic: opening mouth tag: body 㑒: - historical: everyone - mnemonic: unanimous + historical: unanimous tag: personal 坴: historical: clod of earth @@ -833,7 +832,7 @@ CDP-8BE8: # 竜,電,滝,奄,CDP-89BD,俺,庵,亀,縄 standin: 咲 tag: plants 𠀎: - mnemonic: straw + mnemonic: guardrail tag: place 枼: historical: leafy tree @@ -925,9 +924,8 @@ GWS-U2FFA-U200CA-U7C73: # 継,断,繼,斷 tag: geo 𠂢: historical: tributary - mnemonic: vein - standin: 脈 - tag: body + standin: 派 + tag: geo 亚: historical: (various) mnemonic: trimmed hedge @@ -1061,8 +1059,7 @@ GWS-U4343-VAR-001: # 揺,謡,遥,瑶,搖,遙,謠 mnemonic: open jar tag: tools GWS-U5B5A-G: - historical: trust - mnemonic: suckling babe + historical: brood over eggs reference: 乳 tag: personal 离: @@ -1175,9 +1172,8 @@ GWS-U6EA5-VAR-003: standin: 塞 tag: place 冏: - historical: jute plant - mnemonic: merchant - reference: 商 + historical: window + mnemonic: window curtains tag: personal GWS-CDP-8CA9-07: # 隋,惰,楕,遀,墮,隨,髓 historical: (various) @@ -1483,7 +1479,7 @@ GWS-U97F1-VAR-001: # 繊,懺,纖 tag: plants CDP-8C6A: # 款,隷 historical: crabapple - mnemonic: dirty altar + mnemonic: noble priest tag: place 癸: historical: tenth Heavenly Stem diff --git a/app/lib/dic/kanjijumpSpecificVariants.ts b/app/lib/dic/kanjijumpSpecificVariants.ts index 80e22cec..f89a0ef3 100644 --- a/app/lib/dic/kanjijumpSpecificVariants.ts +++ b/app/lib/dic/kanjijumpSpecificVariants.ts @@ -143,7 +143,7 @@ export const kanjijumpSpecificVariants = [ ["頁", "𦣻", "GWS-JUSTINSILVESTRE_U268FB-03-TOP"], ["里", "CDP-88D4"], ["缶", "𠙻"], - ["菐", "CDP-8BBD", "GWS-U4E35-G"], + ["菐", "GWS-U4E35-G", "CDP-8BBD"], ["卯", "CDP-8C69"], ["𮥶", "雚"], ["卓", "𠦝"], @@ -174,7 +174,7 @@ export const kanjijumpSpecificVariants = [ ["全", "GWS-AJ1-13890"], ["弱", "GWS-AJ1-13811"], ["朝", "GWS-U671D-UE0101"], - ["GWS-U5B5A-G", "孚"], + ["孵", "GWS-U5B5A-G", "孚"], ["GWS-U655D-G", "敝"], ["GWS-U914B-G", "酋"], ["甬", "GWS-U752C-03-VAR-001"], @@ -202,7 +202,7 @@ export const kanjijumpSpecificVariants = [ ["高", "CDP-8C4D"], ["囱", "囟"], ["次", "㳄"], - ["厓", "厂"], + ["厂", "厓"], ["稟", "禀"], ["荘", "庄"], ["壮", "壯"], @@ -252,5 +252,6 @@ export const kanjijumpSpecificVariants = [ ["叟", "GWS-U53DF-G"], ["巷", "GWS-U5DF7-UE0100"], ["猪", "猪", "豬"], - ["啓", "GWS-U22F04-03-VAR-001"] + ["啓", "GWS-U22F04-03-VAR-001"], + ["罒", "𦉰", "𦉪"], ] diff --git a/app/lib/dic/kanjivgExtractedComponents.ts b/app/lib/dic/kanjivgExtractedComponents.ts index 7221edd2..04ba42cf 100644 --- a/app/lib/dic/kanjivgExtractedComponents.ts +++ b/app/lib/dic/kanjivgExtractedComponents.ts @@ -75,6 +75,8 @@ export const kanjivgExtractedComponents: { "GWS-U27607-VAR-010": ['派', [6, 9], takeWideRightElement], 'GWS-CDP-8C66-VAR-001': ['旅', [7, 10], takeBottomRightElement], 'GWS-U22F04-03-VAR-001': ["啓", [1, 8]], - '𠂆': ['盾', [1, 2]] + '𠂆': ['盾', [1, 2]], + "𦉰": ["岡", [1, 5]], + "𦉪": ["冏", [1, 4]], }; diff --git a/app/lib/patchKanjiDbIds.ts b/app/lib/patchKanjiDbIds.ts index e6e0d912..780d13b1 100644 --- a/app/lib/patchKanjiDbIds.ts +++ b/app/lib/patchKanjiDbIds.ts @@ -374,8 +374,9 @@ export const patchIds = (patchedIds: PatchedIds) => { ["黙", "⿺黒犬"], ["勲", "⿺𤋱力"], ["菫", "⿻𦰌一"], - ["𦰌", "⿱艹⿻⿱口一土"], - ["𦰌", "⿱廿⿻⿱口一土"], + ["𦰌", "⿱艹⿻口龶"], + ["堇", "⿱廿⿻中龶"], + ["菫", "⿳艹一⿻中龶"], ["營", "⿱𤇾呂"], ["串", "⿻中口"], ["捌", "⿰扌別"], @@ -418,7 +419,9 @@ export const patchIds = (patchedIds: PatchedIds) => { // CDP-8BD1 &CDP-8BD1; ⿹耳壬 ⿹耳王 ["CDP-8BD1", "⿹耳王"], - ["鬲", "⿱𠮛⿵冂⿱儿丁[J]"], + ["鬲", "⿳一口⿵𦉪丅[GTJ] ⿳一口⿵冂&CDP-89C4;[K]"], + ["冏", "⿵冂㕣[G] ⿵冂⿱儿口[K] ⿵𦉪口[J]"], + ["册", "⿻⿰冂冂一"], ]) .extractFigureFromIdsSegment({ componentIdsSegment: "⿰王", @@ -608,9 +611,9 @@ export const patchIds = (patchedIds: PatchedIds) => { }) .replaceIds("艮", "⿻ヨ&CDP-8CC6;") - .replaceIds("CDP-8B7C", "⿻日厶") - .addIdsAfterTransforms("GWS-CDP-8B7C-VAR-001", "⿻日⿰丨二") - .replaceIds("CDP-89CE", "⿻日丿") + .replaceIds("CDP-8B7C", "⿻ヨ厶") + .addIdsAfterTransforms("GWS-CDP-8B7C-VAR-001", "⿻ヨ⿰丨二") + .replaceIds("CDP-89CE", "⿻ヨ丿") // U+810A 脊 ⿱&CDP-88D2;月[GJK] ⿱&CDP-88D2;⺼[T] .replaceIds("脊", "⿱⿻人⿰二二月") @@ -657,7 +660,7 @@ export const patchIds = (patchedIds: PatchedIds) => { .replaceIds("⺼", "⿵冂⺀") .replaceEverywhere("⺆", "冂") - .replaceIds("𠀎", "⿻艹二") + .replaceIds("𠀎", "⿱井一") .replaceManyIds([ ["豹", "⿰豸勺"], @@ -715,6 +718,12 @@ export const patchIds = (patchedIds: PatchedIds) => { newCompleteIds: "⿰戸攵", }) + .replaceIds("罔", "⿵𦉰亡") + .replaceIds("岡", "⿵𦉰山") + + .replaceIds("CDP-89C6", "⿴&CDP-8BF5;二") + // + // JIS 2004 // checking via https://www.asahi-net.or.jp/~ax2s-kmtn/ref/jis2000-2004.html // 逢 - correct in ids-cdp diff --git a/app/routes/FiguresGroupedByVariantsList.tsx b/app/routes/FiguresGroupedByVariantsList.tsx new file mode 100644 index 00000000..c25d5964 --- /dev/null +++ b/app/routes/FiguresGroupedByVariantsList.tsx @@ -0,0 +1,104 @@ +/* eslint-disable react/no-unescaped-entities */ +import clsx from "clsx"; +import { Link } from "react-router"; + +import { DictPreviewLink } from "~/components/AppLink"; +import { FigureBadge } from "~/components/FigureBadge"; +import { getBadgeFiguresByPriorityGroupedWithVariants } from "~/features/browse/getBadgeFiguresByPriorityGroupedWithVariants"; +import { ManyFiguresPopover } from "~/features/browse/useManyFiguresPopover"; +import { parseAnnotatedKeywordText } from "~/features/dictionary/getHeadingsMeanings"; + +export function FiguresGroupedByVariantsList({ + figuresAndVariants: atomicComponentsAndVariants, + popover, +}: { + figuresAndVariants: Awaited< + ReturnType + >["matchedFiguresAndVariants"]; + popover: ManyFiguresPopover; +}) { + return ( +
+ {atomicComponentsAndVariants.map( + ({ figures, keyword, mnemonicKeyword }, i) => { + const parsedKeyword = mnemonicKeyword + ? parseAnnotatedKeywordText(mnemonicKeyword) + : null; + const keywordDisplay = ( +
+ {parsedKeyword ? ( + <> + "{parsedKeyword?.text}" + {!parsedKeyword.reference ? null : ( + <> + {" "} + ({parsedKeyword.referenceTypeText}{" "} + + {parsedKeyword.reference} + + ) + + )} + + ) : ( + <>{keyword} + )} +
+ ); + return figures.length > 1 ? ( +
+
+ {figures.map(({ figure, isMatch }) => ( + // + + + + ))} +
+ {keywordDisplay} +
+ ) : ( +
+ + + + {/* */} + {keywordDisplay} +
+ ); + }, + )} +
+ ); +} diff --git a/app/routes/browse.atomic-components.tsx b/app/routes/browse.atomic-components.tsx index c98dda09..e3f47286 100644 --- a/app/routes/browse.atomic-components.tsx +++ b/app/routes/browse.atomic-components.tsx @@ -1,5 +1,4 @@ /* eslint-disable react/no-unescaped-entities */ -import clsx from "clsx"; import { createPortal } from "react-dom"; import type { LoaderFunction, MetaFunction } from "react-router"; import { @@ -13,20 +12,19 @@ import { BrowseCharactersLink, BrowseCompoundComponentsLink, DictLink, - DictPreviewLink, } from "~/components/AppLink"; import DictionaryLayout from "~/components/DictionaryLayout"; import A from "~/components/ExternalLink"; -import { FigureBadge } from "~/components/FigureBadge"; import { FigurePopoverWindow } from "~/components/FigurePopover"; import { prisma } from "~/db.server"; import CollapsibleInfoSection from "~/features/browse/CollapsibleInfoSection"; import { useManyFiguresPopover } from "~/features/browse/useManyFiguresPopover"; -import { parseAnnotatedKeywordText } from "~/features/dictionary/getHeadingsMeanings"; import { TOTAL_ATOMIC_COMPONENTS_COUNT } from "~/features/dictionary/TOTAL_ATOMIC_COMPONENTS_COUNT"; import { getAtomicFigureBadgeFigures } from "../features/browse/getAtomicFigureBadgeFigures"; +import { FiguresGroupedByVariantsList } from "./FiguresGroupedByVariantsList"; + export const meta: MetaFunction = () => [ { title: `The ${TOTAL_ATOMIC_COMPONENTS_COUNT} atomic kanji components | Kanjisense`, @@ -55,7 +53,10 @@ function AtomicComponentsPageContent({ }: { loaderData: LoaderData; }) { - const { atomicComponentsAndVariants, totalAtomicComponents } = loaderData; + const { + matchedFiguresAndVariants: atomicComponentsAndVariants, + matchedFiguresCount: totalAtomicComponents, + } = loaderData; const popover = useManyFiguresPopover(); const content = ( <> @@ -211,73 +212,10 @@ function AtomicComponentsPageContent({

(not counting variants: {atomicComponentsAndVariants.length} total)

-
- {atomicComponentsAndVariants.map( - ({ figures, keyword, mnemonicKeyword }, i) => { - const keywordDisplay = ( -
- {mnemonicKeyword ? ( - <>"{parseAnnotatedKeywordText(mnemonicKeyword)?.text}" - ) : ( - <>{keyword} - )} -
- ); - return figures.length > 1 ? ( -
-
- {figures.map(({ figure, isAtomic }) => ( - // - - - - ))} -
- {keywordDisplay} -
- ) : ( -
- - - - {/* */} - {keywordDisplay} -
- ); - }, - )} -
+ ); diff --git a/app/routes/browse.compound-components.tsx b/app/routes/browse.compound-components.tsx index de97f282..db3d1fe2 100644 --- a/app/routes/browse.compound-components.tsx +++ b/app/routes/browse.compound-components.tsx @@ -18,6 +18,7 @@ import DictionaryLayout from "~/components/DictionaryLayout"; import { FigureBadge } from "~/components/FigureBadge"; import { FigurePopoverWindow } from "~/components/FigurePopover"; import { prisma } from "~/db.server"; +import { getBadgeFiguresByPriorityGroupedWithVariants } from "~/features/browse/getBadgeFiguresByPriorityGroupedWithVariants"; import { BadgePropsFigure, badgeFigureSelect, @@ -32,6 +33,8 @@ import { FIGURES_VERSION } from "~/models/figure"; import { useManyFiguresPopover } from "../features/browse/useManyFiguresPopover"; +import { FiguresGroupedByVariantsList } from "./FiguresGroupedByVariantsList"; + type LoaderData = Awaited>; const isPriorityComponentWhere = { @@ -68,39 +71,68 @@ async function getAllListCharacterBadgeFigures(prisma: PrismaClient) { ], }, }); - const priorityCompoundComponents = await prisma.kanjisenseFigure.findMany({ - select: commonSelect, - orderBy: { aozoraAppearances: "desc" }, - where: { - version: FIGURES_VERSION, - ...isPriorityComponentWhere, - componentsTree: { not: [] }, - id: { - notIn: priorityCompoundComponentCharacters.map((c) => c.id), - }, - }, - }); - const compoundComponentsMap: Record< - string, - BadgePropsFigure & KeywordDisplayFigure - > = {}; const compoundComponentCharactersMap: Record< string, BadgePropsFigure & KeywordDisplayFigure > = {}; - for (const figure of priorityCompoundComponents) { - compoundComponentsMap[figure.id] = figure; - } for (const figure of priorityCompoundComponentCharacters) { compoundComponentCharactersMap[figure.id] = figure; } + + const compoundComponentsWithStandaloneCharactersAsVariant = + await getBadgeFiguresByPriorityGroupedWithVariants(prisma, { + version: FIGURES_VERSION, + ...isPriorityComponentWhere, + componentsTree: { not: [] }, + id: { + notIn: priorityCompoundComponentCharacters.map((c) => c.id), + }, + variantGroup: { + // TODO: confirm no group has "head" which is not standalone, while also having a standalone variant + figures: { + some: { + listsAsCharacter: { isEmpty: false }, + }, + }, + }, + }); + + const compoundComponentsWithNoStandaloneCharacterVariants = + await getBadgeFiguresByPriorityGroupedWithVariants(prisma, { + version: FIGURES_VERSION, + ...isPriorityComponentWhere, + componentsTree: { not: [] }, + key: { + notIn: priorityCompoundComponentCharacters + .map((c) => c.key) + .concat( + compoundComponentsWithStandaloneCharactersAsVariant.matchedFiguresAndVariants.map( + (f) => f.key, + ), + ), + }, + OR: [ + { variantGroup: null }, + { + variantGroup: { + // TODO: confirm no group has "head" which is not standalone, while also having a standalone variant + figures: { + none: { + listsAsCharacter: { isEmpty: false }, + }, + }, + }, + }, + ], + }); + return { - compoundComponents: compoundComponentsMap, compoundComponentCharacters: compoundComponentCharactersMap, - totalCompoundComponents: priorityCompoundComponents.length, totalCompoundComponentCharacters: priorityCompoundComponentCharacters.length, + compoundComponentsWithNoStandaloneCharacterVariants, + compoundComponentsWithStandaloneCharactersAsVariant, }; } @@ -125,12 +157,23 @@ function CompoundComponentsPageContent({ loaderData: LoaderData; }) { const { - compoundComponents, + compoundComponentsWithNoStandaloneCharacterVariants: + nonCharacterCompoundComponents, + compoundComponentsWithStandaloneCharactersAsVariant: + compoundComponentCharacterVariants, compoundComponentCharacters, - totalCompoundComponents, totalCompoundComponentCharacters, } = loaderData; + const totalCompoundComponents = + compoundComponentCharacterVariants.matchedFiguresCount + + nonCharacterCompoundComponents.matchedFiguresCount + + totalCompoundComponentCharacters; + + const totalCompoundNonCharacterVariantComponents = + compoundComponentCharacterVariants.matchedFiguresCount + + nonCharacterCompoundComponents.matchedFiguresCount; + const popover = useManyFiguresPopover(); const content = ( @@ -144,15 +187,12 @@ function CompoundComponentsPageContent({ : null}

- The{" "} - {( - totalCompoundComponents + totalCompoundComponentCharacters - ).toLocaleString()}{" "} - "compound" kanji components + The {totalCompoundComponents.toLocaleString()} "compound" kanji + components

-

+

All the {(3530).toLocaleString()} most important kanji can be broken down into just{" "} @@ -164,11 +204,10 @@ function CompoundComponentsPageContent({ atomic components, learning to recognize these "compound" components will help you to learn new kanji much more easily.

-

- The problem is that, as with the characters themselves, there are - more than a thousand of these compound components. Therefore, they - don't lend themselves well to rote memorization. But the good news - is that{" "} +

+ The problem is that, as with the characters themselves, there are a + multitude of these compound components. Therefore, they don't lend + themselves well to rote memorization. But the good news is that{" "} most of the compound kanji components double as standalone characters @@ -178,8 +217,8 @@ function CompoundComponentsPageContent({

{" "} - {totalCompoundComponentCharacters} compound components doubling as - standalone characters + {totalCompoundComponentCharacters} compound components{" "} + doubling as standalone characters

@@ -206,49 +245,66 @@ function CompoundComponentsPageContent({ )}
-

- As for the remaining compound components in the{" "} - {(3530).toLocaleString()} most important kanji, which{" "} - do not appear as standalone characters in modern - Japanese, there are just {totalCompoundComponents} of them listed in - Kanjisense. Many of these components were indeed once used as - characters long ago, but have since fallen out of use. Others are - simply graphic elements that appear in multiple characters at once - for historical reasons. +

+ About a quarter of the remaining compound components may be + considered variants of standalone characters. + Sometimes they are identical in form, minus a few strokes—in + other words, an abbreviated form. Other times, the variant form is a + more complex historical form of the currently used character. + Sometimes, the historical relationship between variant forms is more + complicated. But in any case, it's usually easy to see the relation + between the two once it's pointed out.

-

- In any case, I still don't recommend trying to memorize these - compounds by rote. Rather, it would probably be easiest to focus on - these components{" "} - as you come across them "in the wild". If you 1) - learn one character containing one of these components, and 2) have - that component pointed out to you as such, that should be enough to - help you recognize the component in other characters. +

+ Again, I don't recommend trying to memorize these variants by rote. + But as you learn more and more characters, you might find it helpful + to return to this list and take note of the patterns in these + variations, to reinforce the connections between characters in your + memory.

- {totalCompoundComponents} compound components not doubling as - standalone characters + {compoundComponentCharacterVariants.matchedFiguresCount} compound + components which are{" "} + variants of standalone characters +

+ + + + +

+ As for the remaining compound components in the{" "} + {(3530).toLocaleString()} most important kanji, which{" "} + do not appear as standalone characters in everyday + modern Japanese, in any form, they are listed below. +

+

+ In many cases, these components originally derive from standalone + characters. When the graphical form of the component is especially + evocative and easy to relate to the original character's meaning, I + have used in in Kanjisense as the mnemonic keyword for that character. + But in so many cases, the modern forms are so hard to visually relate + their archaic meanings, that the effort to do so outweighs the + benefit. So, for the most part, the mnemonic keywords here relate to + the modern form of the character, and to the context in which you are + likely to encounter it as a beginner in Japanese. +

+

+ {nonCharacterCompoundComponents.matchedFiguresCount} compound + components without standalone characters as variants

-
- {Object.entries(compoundComponents).map(([id, figure]) => { - const badgeProps = getBadgeProps(figure); - return ( -
- - - -
- -
-
- ); - })} -
+