Compare commits

..

No commits in common. "77018ef252b442804caeeb03873bb1cd6608f5e7" and "49b405331e7da053d9c77edc1b708bcb3637e240" have entirely different histories.

10 changed files with 352 additions and 462 deletions

View File

@ -9,7 +9,7 @@
"version": "1.0.0", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@lingdocs/inflect": "7.0.3", "@lingdocs/inflect": "6.0.12",
"base64url": "^3.0.1", "base64url": "^3.0.1",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"connect-redis": "^6.0.0", "connect-redis": "^6.0.0",
@ -126,9 +126,9 @@
} }
}, },
"node_modules/@lingdocs/inflect": { "node_modules/@lingdocs/inflect": {
"version": "7.0.3", "version": "6.0.12",
"resolved": "https://npm.lingdocs.com/@lingdocs/inflect/-/inflect-7.0.3.tgz", "resolved": "https://npm.lingdocs.com/@lingdocs/inflect/-/inflect-6.0.12.tgz",
"integrity": "sha512-Ia4upjGHRUf6zuJ626V4g1AcaQQ5uKm8aHRn5qWYw1xEfBtSPHTvLd8Y6TXUjglv7HZ8udUXoP2LIGPWlD2aYA==", "integrity": "sha512-ljOlZ8DuhZuUs86LFioQLSNs7HlGpvPRUHNF4EWOroEfLvNoTAHRdlgoVX1ovQS3JBUN7fFWSkovG/HWYPJZOQ==",
"dependencies": { "dependencies": {
"fp-ts": "^2.16.0", "fp-ts": "^2.16.0",
"pbf": "^3.2.1", "pbf": "^3.2.1",
@ -3109,9 +3109,9 @@
} }
}, },
"@lingdocs/inflect": { "@lingdocs/inflect": {
"version": "7.0.3", "version": "6.0.12",
"resolved": "https://npm.lingdocs.com/@lingdocs/inflect/-/inflect-7.0.3.tgz", "resolved": "https://npm.lingdocs.com/@lingdocs/inflect/-/inflect-6.0.12.tgz",
"integrity": "sha512-Ia4upjGHRUf6zuJ626V4g1AcaQQ5uKm8aHRn5qWYw1xEfBtSPHTvLd8Y6TXUjglv7HZ8udUXoP2LIGPWlD2aYA==", "integrity": "sha512-ljOlZ8DuhZuUs86LFioQLSNs7HlGpvPRUHNF4EWOroEfLvNoTAHRdlgoVX1ovQS3JBUN7fFWSkovG/HWYPJZOQ==",
"requires": { "requires": {
"fp-ts": "^2.16.0", "fp-ts": "^2.16.0",
"pbf": "^3.2.1", "pbf": "^3.2.1",

View File

@ -12,7 +12,7 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@lingdocs/inflect": "7.0.3", "@lingdocs/inflect": "6.0.12",
"base64url": "^3.0.1", "base64url": "^3.0.1",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"connect-redis": "^6.0.0", "connect-redis": "^6.0.0",

View File

@ -45,10 +45,10 @@
"@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/resolve-uri" "^3.0.3"
"@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/sourcemap-codec" "^1.4.10"
"@lingdocs/inflect@7.0.3": "@lingdocs/inflect@6.0.12":
"integrity" "sha512-Ia4upjGHRUf6zuJ626V4g1AcaQQ5uKm8aHRn5qWYw1xEfBtSPHTvLd8Y6TXUjglv7HZ8udUXoP2LIGPWlD2aYA==" "integrity" "sha512-ljOlZ8DuhZuUs86LFioQLSNs7HlGpvPRUHNF4EWOroEfLvNoTAHRdlgoVX1ovQS3JBUN7fFWSkovG/HWYPJZOQ=="
"resolved" "https://npm.lingdocs.com/@lingdocs/inflect/-/inflect-7.0.3.tgz" "resolved" "https://npm.lingdocs.com/@lingdocs/inflect/-/inflect-6.0.12.tgz"
"version" "7.0.3" "version" "6.0.12"
dependencies: dependencies:
"fp-ts" "^2.16.0" "fp-ts" "^2.16.0"
"pbf" "^3.2.1" "pbf" "^3.2.1"

View File

@ -7,7 +7,7 @@
"name": "functions", "name": "functions",
"dependencies": { "dependencies": {
"@google-cloud/storage": "^5.8.1", "@google-cloud/storage": "^5.8.1",
"@lingdocs/inflect": "7.0.3", "@lingdocs/inflect": "6.0.12",
"@types/cors": "^2.8.10", "@types/cors": "^2.8.10",
"@types/google-spreadsheet": "^3.0.2", "@types/google-spreadsheet": "^3.0.2",
"@types/react": "^18.0.21", "@types/react": "^18.0.21",
@ -1468,9 +1468,9 @@
} }
}, },
"node_modules/@lingdocs/inflect": { "node_modules/@lingdocs/inflect": {
"version": "7.0.3", "version": "6.0.12",
"resolved": "https://npm.lingdocs.com/@lingdocs/inflect/-/inflect-7.0.3.tgz", "resolved": "https://npm.lingdocs.com/@lingdocs/inflect/-/inflect-6.0.12.tgz",
"integrity": "sha512-Ia4upjGHRUf6zuJ626V4g1AcaQQ5uKm8aHRn5qWYw1xEfBtSPHTvLd8Y6TXUjglv7HZ8udUXoP2LIGPWlD2aYA==", "integrity": "sha512-ljOlZ8DuhZuUs86LFioQLSNs7HlGpvPRUHNF4EWOroEfLvNoTAHRdlgoVX1ovQS3JBUN7fFWSkovG/HWYPJZOQ==",
"dependencies": { "dependencies": {
"fp-ts": "^2.16.0", "fp-ts": "^2.16.0",
"pbf": "^3.2.1", "pbf": "^3.2.1",
@ -8055,9 +8055,9 @@
} }
}, },
"@lingdocs/inflect": { "@lingdocs/inflect": {
"version": "7.0.3", "version": "6.0.12",
"resolved": "https://npm.lingdocs.com/@lingdocs/inflect/-/inflect-7.0.3.tgz", "resolved": "https://npm.lingdocs.com/@lingdocs/inflect/-/inflect-6.0.12.tgz",
"integrity": "sha512-Ia4upjGHRUf6zuJ626V4g1AcaQQ5uKm8aHRn5qWYw1xEfBtSPHTvLd8Y6TXUjglv7HZ8udUXoP2LIGPWlD2aYA==", "integrity": "sha512-ljOlZ8DuhZuUs86LFioQLSNs7HlGpvPRUHNF4EWOroEfLvNoTAHRdlgoVX1ovQS3JBUN7fFWSkovG/HWYPJZOQ==",
"requires": { "requires": {
"fp-ts": "^2.16.0", "fp-ts": "^2.16.0",
"pbf": "^3.2.1", "pbf": "^3.2.1",

View File

@ -15,7 +15,7 @@
"main": "lib/functions/src/index.js", "main": "lib/functions/src/index.js",
"dependencies": { "dependencies": {
"@google-cloud/storage": "^5.8.1", "@google-cloud/storage": "^5.8.1",
"@lingdocs/inflect": "7.0.3", "@lingdocs/inflect": "6.0.12",
"@types/cors": "^2.8.10", "@types/cors": "^2.8.10",
"@types/google-spreadsheet": "^3.0.2", "@types/google-spreadsheet": "^3.0.2",
"@types/react": "^18.0.21", "@types/react": "^18.0.21",

View File

@ -7,7 +7,7 @@
"private": true, "private": true,
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^5.15.2", "@fortawesome/fontawesome-free": "^5.15.2",
"@lingdocs/ps-react": "7.0.3", "@lingdocs/ps-react": "6.0.12",
"@testing-library/jest-dom": "^5.11.4", "@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0", "@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10", "@testing-library/user-event": "^12.1.10",

View File

@ -7,39 +7,37 @@
*/ */
import ExtraEntryInfo from "../components/ExtraEntryInfo"; import ExtraEntryInfo from "../components/ExtraEntryInfo";
import classNames from "classnames"; import classNames from "classnames";
import { Types as T, InlinePs } from "@lingdocs/ps-react"; import {
Types as T,
InlinePs,
} from "@lingdocs/ps-react";
function Entry({ function Entry({ entry, textOptions, nonClickable, isolateEntry }: {
entry, entry: T.DictionaryEntry,
textOptions, textOptions: T.TextOptions,
nonClickable, nonClickable?: boolean,
isolateEntry, isolateEntry?: (ts: number) => void,
}: {
entry: T.DictionaryEntry;
textOptions: T.TextOptions;
nonClickable?: boolean;
isolateEntry?: (ts: number) => void;
}) { }) {
return ( return (
<div <div
className={classNames("entry", { clickable: !nonClickable })} className={classNames("entry", { clickable: !nonClickable })}
onClick={ onClick={(!nonClickable && isolateEntry) ? () => isolateEntry(entry.ts) : undefined}
!nonClickable && isolateEntry ? () => isolateEntry(entry.ts) : undefined data-testid="entry"
} >
data-testid="entry" <div>
> <strong>
<div> <InlinePs opts={textOptions}>{{ p: entry.p, f: entry.f }}</InlinePs>
<strong> </strong>
<InlinePs opts={textOptions}>{{ p: entry.p, f: entry.f }}</InlinePs> {` `}
</strong> <em>{entry.c}</em>
{` `} </div>
<em>{entry.c}</em> <ExtraEntryInfo
{entry.a && !nonClickable && <i className="ml-2 fas fa-volume-down" />} entry={entry}
</div> textOptions={textOptions}
<ExtraEntryInfo entry={entry} textOptions={textOptions} /> />
<div className="entry-definition">{entry.e}</div> <div className="entry-definition">{entry.e}</div>
</div> </div>
); );
} };
export default Entry; export default Entry;

View File

@ -1,13 +0,0 @@
export default function playStorageAudio(ts: number, callback: () => void) {
if (!ts) return;
let audio = new Audio(`https://storage.lingdocs.com/${ts}.mp3`);
audio.addEventListener("ended", () => {
callback();
audio.remove();
audio.srcObject = null;
});
audio.play().catch((e) => {
console.error(e);
alert("Error playing audio - Connect to the internet and try again");
});
}

View File

@ -8,429 +8,334 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { import {
VPExplorer, VPExplorer,
InflectionsTable, InflectionsTable,
inflectWord, inflectWord,
InlinePs, InlinePs,
Types as T, Types as T,
typePredicates as tp, typePredicates as tp,
getInflectionPattern, getInflectionPattern,
HumanReadableInflectionPattern, HumanReadableInflectionPattern,
} from "@lingdocs/ps-react"; } from "@lingdocs/ps-react";
import { submissionBase, addSubmission } from "../lib/submissions"; import {
submissionBase,
addSubmission,
} from "../lib/submissions";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import Entry from "../components/Entry"; import Entry from "../components/Entry";
import Results from "../screens/Results"; import Results from "../screens/Results";
import WordlistWordEditor from "../components/WordlistWordEditor"; import WordlistWordEditor from "../components/WordlistWordEditor";
import { import {
addToWordlist, addToWordlist,
deleteWordFromWordlist, deleteWordFromWordlist,
hasAttachment, hasAttachment,
} from "../lib/wordlist-database"; } from "../lib/wordlist-database";
import { wordlistEnabled } from "../lib/level-management"; import { wordlistEnabled } from "../lib/level-management";
import AudioPlayButton from "../components/AudioPlayButton"; import AudioPlayButton from "../components/AudioPlayButton";
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { Modal } from "react-bootstrap"; import { Modal } from "react-bootstrap";
import { getTextOptions } from "../lib/get-text-options"; import { getTextOptions } from "../lib/get-text-options";
import { entryFeeder } from "../lib/dictionary"; import {
import { State, DictionaryAPI } from "../types/dictionary-types"; entryFeeder,
import playStorageAudio from "../components/PlayStorageAudio"; } from "../lib/dictionary";
import {
State,
DictionaryAPI,
} from "../types/dictionary-types";
function IsolatedEntry({ function IsolatedEntry({ state, dictionary, isolateEntry }: {
state, state: State,
dictionary, dictionary: DictionaryAPI,
isolateEntry, isolateEntry: (ts: number) => void,
}: {
state: State;
dictionary: DictionaryAPI;
isolateEntry: (ts: number) => void;
}) { }) {
const [exploded, setExploded] = useState<boolean>(false); const [exploded, setExploded] = useState<boolean>(false);
const [playing, setPlaying] = useState<boolean>(false); const [editing, setEditing] = useState<boolean>(false);
const [editing, setEditing] = useState<boolean>(false); const [comment, setComment] = useState<string>("");
const [comment, setComment] = useState<string>(""); const [editSubmitted, setEditSubmitted] = useState<boolean>(false);
const [editSubmitted, setEditSubmitted] = useState<boolean>(false); const [showingDeleteWarning, setShowingDeleteWarning] = useState<boolean>(false);
const [showingDeleteWarning, setShowingDeleteWarning] = const [showClipped, setShowClipped] = useState<string>("");
useState<boolean>(false); useEffect(() => {
const [showClipped, setShowClipped] = useState<string>(""); setEditing(false);
useEffect(() => { setComment("");
setEditing(false); setEditSubmitted(false);
setComment(""); }, [state]);
setEditSubmitted(false); function flashClippedMessage(m: string) {
setPlaying(false); setShowClipped(m);
}, [state]); setTimeout(() => {
function flashClippedMessage(m: string) { setShowClipped("");
setShowClipped(m); }, 1250);
setTimeout(() => {
setShowClipped("");
}, 1250);
}
const wordlistWord = state.wordlist.find(
(w) => w.entry.ts === state.isolatedEntry?.ts
);
const textOptions = getTextOptions(state);
function submitEdit() {
if (!state.isolatedEntry) return;
if (!state.user) return;
addSubmission(
{
...submissionBase(state.user),
type: "edit suggestion",
entry: state.isolatedEntry,
comment,
},
state.user
);
setEditing(false);
setComment("");
setEditSubmitted(true);
}
function handleAddToWordlist() {
if (!state.isolatedEntry) return;
const toAdd = {
entry: state.isolatedEntry,
notes: "",
};
addToWordlist(toAdd);
}
function handleDeleteFromWordlist() {
if (!state.isolatedEntry) return;
if (!wordlistWord) return;
setShowingDeleteWarning(false);
deleteWordFromWordlist(wordlistWord._id);
}
const entry = state.isolatedEntry;
if (!entry) {
return (
<div className="text-center">
<h4 className="mb-4 mt-4">Word not found</h4>
<h5>
<Link to="/">Home</Link>
</h5>
</div>
);
}
const complement = entry.l ? dictionary.findOneByTs(entry.l) : undefined;
const relatedEntries = dictionary.findRelatedEntries(entry);
const inf = ((): T.InflectorOutput | false => {
try {
return inflectWord(entry);
} catch (e) {
console.error("error inflecting entry", entry);
return false;
} }
})(); const wordlistWord = state.wordlist.find((w) => w.entry.ts === state.isolatedEntry?.ts);
const isVerbEntry = tp.isVerbEntry({ entry, complement }); const textOptions = getTextOptions(state);
function DisplayVPExplorer(props: { function submitEdit() {
entry: T.DictionaryEntry; if (!state.isolatedEntry) return;
complement: T.DictionaryEntry | undefined; if (!state.user) return;
}) { addSubmission({
try { ...submissionBase(state.user),
return ( type: "edit suggestion",
<VPExplorer entry: state.isolatedEntry,
verb={{ comment,
// TODO: CLEAN THIS UP! }, state.user);
// @ts-ignore setEditing(false);
entry: props.entry, setComment("");
complement: props.complement, setEditSubmitted(true);
}}
opts={textOptions}
entryFeeder={entryFeeder}
handleLinkClick={isolateEntry}
/>
);
} catch (e) {
console.error("error rendering VPExplorer", e);
return null;
} }
} function handleAddToWordlist() {
function handleClipId() { if (!state.isolatedEntry) return;
if (!entry) return; const toAdd = {
navigator.clipboard.writeText(entry.ts.toString()); entry: state.isolatedEntry,
flashClippedMessage("word id copied to clipboard"); notes: "",
} };
function handleClipEntry() { addToWordlist(toAdd);
if (!entry) return; }
navigator.clipboard.writeText(JSON.stringify(entry)); function handleDeleteFromWordlist() {
flashClippedMessage("entry data copied to clipboard"); if (!state.isolatedEntry) return;
} if (!wordlistWord) return;
function handlePlayStorageAudio() { setShowingDeleteWarning(false);
if (!entry) return; deleteWordFromWordlist(wordlistWord._id);
setPlaying(true); }
playStorageAudio(entry.ts, () => { const entry = state.isolatedEntry;
setPlaying(false); if (!entry) {
}); return <div className="text-center">
} <h4 className="mb-4 mt-4">Word not found</h4>
return ( <h5><Link to="/">Home</Link></h5>
<div className="wide-width-limiter"> </div>;
<Helmet> }
<title>{entry.p} - LingDocs Pashto Dictionary</title> const complement = entry.l
</Helmet> ? dictionary.findOneByTs(entry.l)
<div className="row"> : undefined;
<div className="col-8"> const relatedEntries = dictionary.findRelatedEntries(entry);
<Entry const inf = ((): T.InflectorOutput | false => {
nonClickable try {
entry={exploded ? explodeEntry(entry) : entry} return inflectWord(entry);
textOptions={textOptions} } catch (e) {
isolateEntry={isolateEntry} console.error("error inflecting entry", entry);
/> return false;
</div> }
<div className="col-4"> })();
<div className="d-flex flex-row justify-content-end"> const isVerbEntry = tp.isVerbEntry({ entry, complement });
{entry.a && ( function DisplayVPExplorer(props: {
<div className="clickable mr-3" onClick={handlePlayStorageAudio}> entry: T.DictionaryEntry,
<i complement: T.DictionaryEntry | undefined,
className={`fas fa-lg fa-volume-${playing ? "down" : "off"}`} }) {
/> try {
</div> return <VPExplorer
)} verb={{
<div // TODO: CLEAN THIS UP!
className="clickable mr-3" // @ts-ignore
onClick={() => setExploded((os) => !os)} entry: props.entry,
> complement: props.complement,
<i className={`fas fa-${exploded ? "compress" : "expand"}-alt`} /> }}
</div> opts={textOptions}
<div className="clickable mr-3" onClick={handleClipId}> entryFeeder={entryFeeder}
<i className="fas fa-tag"></i> handleLinkClick={isolateEntry}
</div>
{state.user && state.user.level === "editor" && (
<>
<div className="clickable mr-3" onClick={handleClipEntry}>
<i className="fas fa-code"></i>
</div>
<Link to={`/edit?id=${entry.ts}`} className="plain-link">
<div
className="clickable mr-3"
data-testid="finalEditEntryButton"
>
<i className="fa fa-gavel" />
</div>
</Link>
</>
)}
{state.user && (
<>
<div
className="clickable mr-3"
data-testid="editEntryButton"
onClick={() => setEditing((os) => !os)}
>
<i className="fa fa-pen" />
</div>
{wordlistEnabled(state.user) && (
<div
className="clickable"
data-testid={
wordlistWord ? "fullStarButton" : "emptyStarButton"
}
onClick={
wordlistWord
? () => setShowingDeleteWarning(true)
: () => handleAddToWordlist()
}
>
<i
className={`fa${wordlistWord ? "s" : "r"} fa-star fa-lg`}
/>
</div>
)}
</>
)}
</div>
</div>
</div>
{wordlistWord && (
<>
{hasAttachment(wordlistWord, "audio") && (
<AudioPlayButton word={wordlistWord} />
)}
<WordlistWordEditor word={wordlistWord} />
</>
)}
{editing && (
<div className="mb-3">
<div className="form-group" style={{ maxWidth: "500px" }}>
<label htmlFor="editSuggestionForm">Suggest correction/edit:</label>
<input
type="text"
className="form-control"
id="editSuggestionForm"
data-lpignore="true"
value={comment}
onChange={(e) => setComment(e.target.value)}
/> />
</div> } catch (e) {
<div className="btn-group"> console.error("error rendering VPExplorer", e);
<button return null;
type="button" }
className="btn btn-outline-secondary" }
onClick={comment ? submitEdit : () => null} function handleClipId() {
data-testid="editWordSubmitButton" if (!entry) return
> navigator.clipboard.writeText(entry.ts.toString());
Submit flashClippedMessage("word id copied to clipboard");
</button> }
<button function handleClipEntry() {
type="button" if (!entry) return
className="btn btn-outline-secondary" navigator.clipboard.writeText(JSON.stringify(entry));
onClick={() => { flashClippedMessage("entry data copied to clipboard");
setEditing(false); }
setComment(""); return <div className="wide-width-limiter">
}} <Helmet>
data-testid="editWordCancelButton" <title>{entry.p} - LingDocs Pashto Dictionary</title>
> </Helmet>
Cancel <div className="row">
</button> <div className="col-8">
</div> <Entry
</div> nonClickable
)} entry={exploded ? explodeEntry(entry) : entry}
{editSubmitted && <p>Thank you for your help!</p>}
{inf && (
<>
{inf.inflections &&
(() => {
const pattern = getInflectionPattern(
// @ts-ignore
entry
);
return (
<div>
<a
href={`https://grammar.lingdocs.com/inflection/inflection-patterns/${inflectionSubUrl(
pattern
)}`}
rel="noreferrer"
target="_blank"
>
<div className="badge bg-light mb-2">
Inflection pattern{" "}
{HumanReadableInflectionPattern(pattern, textOptions)}
</div>
</a>
<InflectionsTable
inf={inf.inflections}
textOptions={textOptions} textOptions={textOptions}
/> isolateEntry={isolateEntry}
/>
</div>
<div className="col-4">
<div className="d-flex flex-row justify-content-end">
<div
className="clickable mr-3"
onClick={() => setExploded(os => !os)}
>
<i className={`fas fa-${exploded ? "compress" : "expand"}-alt`} />
</div>
<div className="clickable mr-3" onClick={handleClipId}>
<i className="fas fa-tag"></i>
</div>
{state.user && state.user.level === "editor" && <>
<div className="clickable mr-3" onClick={handleClipEntry}>
<i className="fas fa-code"></i>
</div>
<Link to={`/edit?id=${entry.ts}`} className="plain-link">
<div
className="clickable mr-3"
data-testid="finalEditEntryButton"
>
<i className="fa fa-gavel" />
</div>
</Link>
</>}
{state.user && <>
<div
className="clickable mr-3"
data-testid="editEntryButton"
onClick={() => setEditing(os => !os)}
>
<i className="fa fa-pen" />
</div>
{wordlistEnabled(state.user) && <div
className="clickable"
data-testid={wordlistWord ? "fullStarButton" : "emptyStarButton"}
onClick={wordlistWord
? () => setShowingDeleteWarning(true)
: () => handleAddToWordlist()
}
>
<i className={`fa${wordlistWord ? "s" : "r"} fa-star fa-lg`}/>
</div>}
</>}
</div> </div>
);
})()}
{"plural" in inf && inf.plural !== undefined && (
<div>
<h5>Plural</h5>
<InflectionsTable inf={inf.plural} textOptions={textOptions} />
</div> </div>
)}
{"bundledPlural" in inf && inf.bundledPlural !== undefined && (
<div>
<h5>Bundled Plural</h5>
<InflectionsTable
inf={inf.bundledPlural}
textOptions={textOptions}
/>
</div>
)}
{"arabicPlural" in inf && inf.arabicPlural !== undefined && (
<div>
<h5>Arabic Plural</h5>
<InflectionsTable
inf={inf.arabicPlural}
textOptions={textOptions}
/>
</div>
)}
</>
)}
{isVerbEntry && (
<div className="pb-4">
<DisplayVPExplorer entry={entry} complement={complement} />
</div> </div>
)} {wordlistWord && <>
{showClipped && ( {hasAttachment(wordlistWord, "audio") && <AudioPlayButton word={wordlistWord} />}
<div <WordlistWordEditor word={wordlistWord} />
className="alert alert-primary text-center" </>}
role="alert" {editing &&
style={{ <div className="mb-3">
<div className="form-group" style={{ maxWidth: "500px" }}>
<label htmlFor="editSuggestionForm">Suggest correction/edit:</label>
<input
type="text"
className="form-control"
id="editSuggestionForm"
data-lpignore="true"
value={comment}
onChange={(e) => setComment(e.target.value)}
/>
</div>
<div className="btn-group">
<button
type="button"
className="btn btn-outline-secondary"
onClick={comment ? submitEdit : () => null}
data-testid="editWordSubmitButton"
>
Submit
</button>
<button
type="button"
className="btn btn-outline-secondary"
onClick={() => { setEditing(false); setComment("") }}
data-testid="editWordCancelButton"
>
Cancel
</button>
</div>
</div>
}
{editSubmitted && <p>Thank you for your help!</p>}
{inf && <>
{inf.inflections && (() => {
const pattern = getInflectionPattern(
// @ts-ignore
entry
);
return <div>
<a href={`https://grammar.lingdocs.com/inflection/inflection-patterns/${inflectionSubUrl(pattern)}`} rel="noreferrer" target="_blank">
<div className="badge bg-light mb-2">Inflection pattern {HumanReadableInflectionPattern(pattern, textOptions)}
</div>
</a>
<InflectionsTable inf={inf.inflections} textOptions={textOptions} />
</div>;
})()}
{"plural" in inf && inf.plural !== undefined && <div>
<h5>Plural</h5>
<InflectionsTable inf={inf.plural} textOptions={textOptions} />
</div>}
{"bundledPlural" in inf && inf.bundledPlural !== undefined && <div>
<h5>Bundled Plural</h5>
<InflectionsTable inf={inf.bundledPlural} textOptions={textOptions} />
</div>}
{"arabicPlural" in inf && inf.arabicPlural !== undefined && <div>
<h5>Arabic Plural</h5>
<InflectionsTable inf={inf.arabicPlural} textOptions={textOptions} />
</div>}
</>}
{isVerbEntry && <div className="pb-4">
<DisplayVPExplorer entry={entry} complement={complement} />
</div>}
{showClipped && <div className="alert alert-primary text-center" role="alert" style={{
position: "fixed", position: "fixed",
top: "30%", top: "30%",
left: "50%", left: "50%",
transform: "translate(-50%, -50%)", transform: "translate(-50%, -50%)",
zIndex: 9999999999999, zIndex: 9999999999999,
}} }}>
> {showClipped}
{showClipped} </div>}
</div>
)}
{!!(relatedEntries && relatedEntries.length) ? ( {!!(relatedEntries && relatedEntries.length) ? <>
<> <h4 style={{ marginTop: isVerbEntry ? "10rem" : "5rem" }}>Related Words</h4>
<h4 style={{ marginTop: isVerbEntry ? "10rem" : "5rem" }}> <Results
Related Words state={{ ...state, results: relatedEntries }}
</h4> isolateEntry={isolateEntry}
<Results handleInflectionSearch={() => null}
state={{ ...state, results: relatedEntries }} />
isolateEntry={isolateEntry} </> : <div style={{ height: "500px" }} />}
handleInflectionSearch={() => null} <Modal
/> show={showingDeleteWarning}
</> onHide={() => setShowingDeleteWarning(false)}
) : ( animation={false}
<div style={{ height: "500px" }} /> >
)} <Modal.Header closeButton>
<Modal <Modal.Title>Delete from wordlist?</Modal.Title>
show={showingDeleteWarning} </Modal.Header>
onHide={() => setShowingDeleteWarning(false)} <Modal.Body>Delete <InlinePs
animation={false} opts={textOptions}
> >{{ p: entry.p, f: entry.f }}</InlinePs> from your wordlist?
<Modal.Header closeButton> </Modal.Body>
<Modal.Title>Delete from wordlist?</Modal.Title> <Modal.Footer>
</Modal.Header> <button type="button" className="btn btn-secorndary clb" onClick={() => setShowingDeleteWarning(false)}>
<Modal.Body> Cancel
Delete{" "} </button>
<InlinePs opts={textOptions}>{{ p: entry.p, f: entry.f }}</InlinePs>{" "} <button type="button" data-testid="confirmDeleteFromWordlist" className="btn btn-primary clb" onClick={handleDeleteFromWordlist}>
from your wordlist? Delete
</Modal.Body> </button>
<Modal.Footer> </Modal.Footer>
<button </Modal>
type="button" </div>;
className="btn btn-secorndary clb"
onClick={() => setShowingDeleteWarning(false)}
>
Cancel
</button>
<button
type="button"
data-testid="confirmDeleteFromWordlist"
className="btn btn-primary clb"
onClick={handleDeleteFromWordlist}
>
Delete
</button>
</Modal.Footer>
</Modal>
</div>
);
} }
function explodeEntry(entry: T.DictionaryEntry): T.DictionaryEntry { function explodeEntry(entry: T.DictionaryEntry): T.DictionaryEntry {
return { return {
...entry, ...entry,
p: entry.p.split("").join(" "), p: entry.p.split("").join(" "),
}; };
} }
function inflectionSubUrl(pattern: T.InflectionPattern): string { function inflectionSubUrl(pattern: T.InflectionPattern): string {
return pattern === 0 return pattern === 0
? "" ? ""
: pattern === 1 : pattern === 1
? "#1-basic" ? "#1-basic"
: pattern === 2 : pattern === 2
? "#2-words-ending-in-an-unstressed-ی---ey" ? "#2-words-ending-in-an-unstressed-ی---ey"
: pattern === 3 : pattern === 3
? "#3-words-ending-in-a-stressed-ی---éy" ? "#3-words-ending-in-a-stressed-ی---éy"
: pattern === 4 : pattern === 4
? "#4-words-with-the-pashtoon-pattern" ? "#4-words-with-the-pashtoon-pattern"
: pattern === 5 : pattern === 5
? "#5-shorter-words-that-squish" ? "#5-shorter-words-that-squish"
: // : pattern === 6 // : pattern === 6
"#6-inanimate-feminine-nouns-ending-in-ي---ee"; : "#6-inanimate-feminine-nouns-ending-in-ي---ee"
} }
export default IsolatedEntry; export default IsolatedEntry;

View File

@ -2349,10 +2349,10 @@
"@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/resolve-uri" "^3.0.3"
"@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/sourcemap-codec" "^1.4.10"
"@lingdocs/ps-react@7.0.3": "@lingdocs/ps-react@6.0.12":
version "7.0.3" version "6.0.12"
resolved "https://npm.lingdocs.com/@lingdocs/ps-react/-/ps-react-7.0.3.tgz#9962fca6a7eeec1d9eec7938ca5fbf2c8634c228" resolved "https://npm.lingdocs.com/@lingdocs/ps-react/-/ps-react-6.0.12.tgz#526a962c06ec59bc86884936e8c5bbea43f64461"
integrity sha512-/ASFFGudbqjEaZQ5OSHAwCQaZSdnPPInWv30rLR6ytLA+lS2udXLQHXkW+3ArUQ1UFBQlK1AC2+1tKNxvD2IrQ== integrity sha512-ncOccEYU40woDK7ChyBwtdyiIKrWqAVIlTiIeVGkKa1w6mq14P02SZFCvbqnjJ54pBIK8r0VXRj0LcYkmZn4rw==
dependencies: dependencies:
"@formkit/auto-animate" "^1.0.0-beta.3" "@formkit/auto-animate" "^1.0.0-beta.3"
classnames "^2.2.6" classnames "^2.2.6"