Compare commits

..

70 Commits

Author SHA1 Message Date
Greg Shuflin
fdea831e43 Remove exa from dependencies 2023-09-15 00:50:57 -07:00
Greg Shuflin
e29d799aa5 Minor change 2023-08-29 22:59:48 -07:00
Greg Shuflin
b103785226 add back prebuild 2023-08-29 22:52:12 -07:00
Greg Shuflin
55c0cdd045 Add back typecheck 2023-08-29 22:51:31 -07:00
Greg Shuflin
25ecd17795 Package as flake 2023-08-29 22:49:21 -07:00
Greg Shuflin
cbc3585a0d Upgrade parcel 2023-08-29 22:35:37 -07:00
Greg Shuflin
3256154e2c Upgrade packages 2023-08-29 17:30:23 -07:00
Greg Shuflin
2d72b36e1d Add justfile 2022-09-16 21:08:22 -07:00
Greg Shuflin
00729ca37f Add lang=en tag 2022-06-04 10:46:59 -07:00
Greg Shuflin
789872fc8d eslint: ignore dist 2022-05-28 00:03:46 -07:00
Greg Shuflin
b7d5f48c07 Add @parcel/core dependency 2022-05-10 00:50:54 -07:00
Greg Shuflin
d0cd2b222a Upgrade some packages 2022-05-08 03:47:06 -07:00
Greg Shuflin
c612e5329a eslint should lint its own config file 2022-05-01 16:13:18 -07:00
Greg Shuflin
43cb50c09b Fix eslints 2022-05-01 16:12:04 -07:00
Greg Shuflin
044d409620 Add .parcel-cache to gitignore 2022-05-01 15:58:18 -07:00
Greg Shuflin
e7b5d532f5 Some CSS changes 2022-04-29 23:34:46 -07:00
Greg Shuflin
408635f255 Fix search shorthand 2021-09-27 02:46:34 -07:00
Greg Shuflin
ac8839ec67 HTML dialog polyfill 2021-09-15 15:24:03 -07:00
Greg Shuflin
3392629749 Fix some type issues 2021-09-15 04:54:32 -07:00
Greg Shuflin
20d3cb3084 Redesigning Saimiar anciliary information 2021-09-15 04:32:47 -07:00
Greg Shuflin
cbfcb762e7 Get rid of margin thing 2021-09-15 04:04:06 -07:00
Greg Shuflin
024b4c11fa Add function for converting special characters 2021-09-15 03:59:06 -07:00
Greg Shuflin
a574747d96 Rename dbtypes -> types 2021-09-15 01:45:48 -07:00
Greg Shuflin
beb9413a90 Generalize to all conlangs 2021-09-15 01:36:12 -07:00
Greg Shuflin
4a73e9983a Generalize entry logic into EntryBase 2021-09-15 01:30:51 -07:00
Greg Shuflin
0b7ee6b3c6 Code reorg 2021-09-15 00:33:29 -07:00
Greg Shuflin
198bb6128a Make editing Saimiar entries' english work 2021-09-15 00:28:47 -07:00
Greg Shuflin
321afa3b12 Add jwt 2021-09-13 13:29:38 -07:00
Greg Shuflin
2ac5b0527a Fix weird type bug 2021-09-13 01:36:16 -07:00
Greg Shuflin
5b3355a651 Use double-quotes 2021-09-13 01:05:41 -07:00
Greg Shuflin
48d8cef2fd Fix result pluralization 2021-09-13 01:04:43 -07:00
Greg Shuflin
0b9cd7ff54 Add ordering, limits 2021-09-13 00:17:25 -07:00
Greg Shuflin
a4c48b3cce Move entry components into separate file 2021-09-13 00:00:51 -07:00
Greg Shuflin
60aa910978 Add --detailed-report flag to parcel 2021-09-12 23:22:00 -07:00
Greg Shuflin
e5b6d99f23 Store language in browser session storage 2021-09-12 23:15:32 -07:00
Greg Shuflin
f4e674c88f Tukvaysi support 2021-09-12 23:09:58 -07:00
Greg Shuflin
9192e070e1 Add Elesu support 2021-09-12 23:06:42 -07:00
Greg Shuflin
4593473b33 Tighten up request logic 2021-09-12 22:55:46 -07:00
Greg Shuflin
bb80900eb0 Explanatory text 2021-09-12 22:17:30 -07:00
Greg Shuflin
7c5e16acc3 Remove console.log 2021-09-12 22:13:04 -07:00
Greg Shuflin
3a261bef95 Convert App to functional component 2021-09-12 22:12:39 -07:00
Greg Shuflin
12207c30b1 Move type definitions 2021-09-12 21:43:26 -07:00
Greg Shuflin
146b8126a2 More typecheck, lint fixes 2021-09-12 21:35:46 -07:00
Greg Shuflin
cf975330f4 Fix eslint rules for typescript 2021-09-12 21:21:55 -07:00
Greg Shuflin
3ec15e30b3 Use enum for search direction 2021-09-12 21:11:52 -07:00
Greg Shuflin
9d52161957 Start supporting Juteyuji 2021-09-12 20:48:11 -07:00
Greg Shuflin
eb46a87c8e General improvements for non-Sai conlangs 2021-09-12 17:27:09 -07:00
Greg Shuflin
010552a4fc Move over App to .tsx 2021-09-12 02:22:50 -07:00
Greg Shuflin
3b2083fa27 Fix indenting 2021-09-12 02:07:44 -07:00
Greg Shuflin
44156694a2 More types 2021-09-12 02:07:29 -07:00
Greg Shuflin
a9703a9529 Lint as script 2021-09-12 01:51:40 -07:00
Greg Shuflin
75403ca383 Have eslint support typescript 2021-09-12 01:50:27 -07:00
Greg Shuflin
0331062120 Use tsconfig file 2021-09-12 01:43:22 -07:00
Greg Shuflin
31e3113ca7 Start actually typechecking 2021-09-12 01:39:52 -07:00
Greg Shuflin
0a2934616e Don't need source maps for prod 2021-09-12 01:19:40 -07:00
Greg Shuflin
077d014899 Start switching over files to typescript 2021-09-12 01:18:32 -07:00
Greg Shuflin
ea7d85c734 Install typescript 2021-09-12 01:16:05 -07:00
Greg Shuflin
0d5e2c90a2 App.jsx improvements 2021-09-12 01:07:30 -07:00
Greg Shuflin
e8c53cda2f Start using eslint 2021-09-12 00:55:17 -07:00
Greg Shuflin
4de6e718ff Fix up some declension issues 2021-09-12 00:28:33 -07:00
Greg Shuflin
188abe2b93 Reorganize src files 2021-09-12 00:05:07 -07:00
Greg Shuflin
232bdd1718 gitignore 2021-09-11 23:49:20 -07:00
Greg Shuflin
69276b0477 Correct parcel scripts 2021-09-11 23:48:49 -07:00
Greg Shuflin
e19abfc3e8 Fresh dependencies 2021-09-11 23:28:33 -07:00
Greg Shuflin
118a2857ce Fresh package.json 2021-09-11 23:18:46 -07:00
Greg Shuflin
d026526e56 Start fixing build setup
Delete everything that seems stateful
2021-09-11 23:16:17 -07:00
Greg Shuflin
b4bd9eb376 Track yarn version 2021-09-11 21:21:20 -07:00
Greg Shuflin
19825ecfd9 Gitignore 2021-09-11 21:15:47 -07:00
Greg Shuflin
5bc543d3f8 Upgrade to latest yarn 2021-09-11 21:13:37 -07:00
Greg Shuflin
01b11fde0b Auto-declension improvements 2021-09-11 20:14:59 -07:00
19 changed files with 3756 additions and 6173 deletions

View File

@ -1,3 +0,0 @@
{
"presets": ["@babel/env", "@babel/react"]
}

33
.eslintrc.js Normal file
View File

@ -0,0 +1,33 @@
module.exports = {
env: {
browser: true,
es2021: true,
},
extends: [
"plugin:react/recommended",
"xo",
],
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 12,
sourceType: "module",
},
plugins: [
"react",
"@typescript-eslint",
],
ignorePatterns: ["dist"],
rules: {
"arrow-parens": ["error", "always"],
indent: ["error", 4],
"unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error", {argsIgnorePattern: "^_"}],
"no-redeclare": "off",
"@typescript-eslint/no-redeclare": ["error"],
quotes: ["error", "double"],
"no-warning-comments": "off",
},
};

11
.gitignore vendored Normal file
View File

@ -0,0 +1,11 @@
dist/
node_modules/
.yarn/*
!.yarn/cache
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
.parcel-cache

181
App.jsx
View File

@ -1,181 +0,0 @@
import React, { Component } from "react";
import './App.scss';
import { declineSaimiar } from './saimiar_morphology.js';
const backendUrl = "https://kucinakobackend.ichigo.everydayimshuflin.com";
function makeRequest(queryString, jsonHandler) {
const effectiveUrl = `${backendUrl}/${queryString}`
fetch(`${effectiveUrl}`)
.then((resp) => {
return resp.json()
})
.then((json) => {
jsonHandler(json);
});
}
function renderConlangName(name) {
if (name == "saimiar") {
return "Saimiar";
}
if (name == "elesu") {
return "Elésu";
}
if (name === "juteyuji") {
return "Juteyuji";
}
if (name === "tukvaysi") {
return "Tukvaysi";
}
}
function Entry(props) {
const conlang = props.conlang;
if (conlang === "saimiar") {
return <SaiEntry entry={ props.entry } />;
}
return <div>Unknown entry type for { conlang }</div>;
}
function SaiEntry(props) {
const entry = props.entry;
const synCategory = entry.syn_category;
const isNominal = synCategory == 'nominal';
console.log(isNominal);
return (
<div className="searchResult" key={ entry.id }>
<b>{ entry.sai }</b> - { entry.eng }
<br />
<span className="synclass">
<i>{ entry.syn_category }</i>
{ entry.morph_type ? `\t\t${entry.morph_type}` : null }
<br/>
{ isNominal ? formatMorphology(entry) : null }
</span>
</div>
);
}
function formatMorphology(entry) {
const decl = declineSaimiar(entry);
if (!decl) {
return '';
}
return `Abs: ${decl.abs}, Erg: ${decl.erg}, Adp: ${decl.adp}`;
}
class Results extends Component {
constructor(props) {
super(props);
this.content = this.content.bind(this);
}
content() {
const conlang = this.props.conlang;
const num = this.props.searchResults.length;
const renderedName = renderConlangName(conlang);
const searchType = (this.props.direction === "toConlang") ? `English -> ${renderedName}` : `${renderedName} -> English`;
const header = (
<div className="searchResultHeader" key="header">
Searched for <b>{ this.props.searchTerm }</b>, { searchType }, found { num } result(s)
</div>);
const entries = this.props.searchResults.map(
(entry, idx) => <Entry entry={ entry } key= { entry.id } conlang={ conlang } />
);
return [header].concat(entries);
}
render() {
const results = this.props.searchResults;
return(
<div className='results'>
{ results ? this.content() : "No search" }
</div>
);
}
}
class App extends Component {
constructor(props) {
super(props);
this.input = React.createRef();
this.handleLangChange = this.handleLangChange.bind(this);
this.searchEng = this.searchEng.bind(this);
this.searchSaimiar = this.searchSaimiar.bind(this);
this.state = {
searchResults: null,
conlang: "saimiar",
direction: null,
searchTerm: null
};
}
searchSaimiar(evt) {
const searchTerm = this.input.current.value;
const request = `saimiar?sai=like.*${searchTerm}*`
if (searchTerm === "") {
this.setState({ searchResults: null, searchTerm: null, direction: null });
} else {
makeRequest(request, (json) => {
this.setState({ searchResults: json, searchTerm, direction: "toEnglish" });
});
}
}
searchEng(evt) {
const searchTerm = this.input.current.value;
const request = `saimiar?eng=like.*${searchTerm}*`
if (searchTerm === "") {
this.setState({ searchResults: null, searchTerm: null, });
} else {
makeRequest(request, (json) => {
this.setState({ searchResults: json, searchTerm, direction: "toConlang" });
});
}
}
handleLangChange(evt) {
const conlang = evt.target.value;
this.setState({ conlang });
}
render() {
return(
<main>
<div className='container'>
<div className='search'>
<h1>Kucinako</h1>
<div className='textInput'>
<input className='textInput' type="text" ref={ this.input } />
</div>
<br/>
<select ref={ this.langSelection } onChange={ this.handleLangChange } defaultValue="saimiar">
<option value="saimiar">Saimiar</option>
<option value="elesu">Elesu</option>
<option value="tukvaysi">Tukvaysi</option>
<option value="juteyuji">Juteyuji</option>
</select>
<button onClick={ this.searchSaimiar } className="searchButton">Saimiar</button>
<button onClick={ this.searchEng } className="searchButton">English</button>
</div>
<Results
searchResults={ this.state.searchResults }
searchTerm= { this.state.searchTerm }
conlang={ this.state.conlang }
direction={ this.state.direction }
/>
</div>
</main>
);
}
}
export default App;

61
flake.lock Normal file
View File

@ -0,0 +1,61 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1692799911,
"narHash": "sha256-3eihraek4qL744EvQXsK1Ha6C3CR7nnT8X2qWap4RNk=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "f9e7cf818399d17d347f847525c5a5a8032e4e44",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1693250523,
"narHash": "sha256-y3up5gXMTbnCsXrNEB5j+7TVantDLUYyQLu/ueiXuyg=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "3efb0f6f404ec8dae31bdb1a9b17705ce0d6986e",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

40
flake.nix Normal file
View File

@ -0,0 +1,40 @@
{
description = "Kucinako - Wordbook of Arzhanai languages";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
node-modules = pkgs.mkYarnPackage {
name = "node-modules";
src = ./.;
};
frontend = pkgs.stdenv.mkDerivation {
name = "frontend";
src = ./.;
buildInputs = [pkgs.yarn node-modules pkgs.lmdb];
buildPhase = ''
ln -s ${node-modules}/libexec/kucinako/node_modules node_modules
${pkgs.yarn}/bin/yarn build
'';
installPhase = ''
mkdir $out
mv dist $out/dist
'';
};
in
{
packages = {
node-modules = node-modules;
default = frontend;
};
}
);
}

View File

@ -1,14 +1,14 @@
<!DOCTYPE html> <!DOCTYPE html>
<!-- use https://www.dafont.com/cabin.font?text=hey+man+get+off+my+back --> <!-- use https://www.dafont.com/cabin.font?text=hey+man+get+off+my+back -->
<html> <html lang="en">
<head> <head>
<title>Kucinako</title> <title>Kucinako</title>
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta charset='utf-8' /> <meta charset='utf-8' />
<link rel="shortcut icon" href="/favicon.png" /> <link rel="shortcut icon" href="./favicon.png" />
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
<script src="./index.js"></script> <script type="module" src="./index.js"></script>
</body> </body>
</html> </html>

View File

@ -1,8 +1,6 @@
import React from "react"; import React from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import App from "./App.jsx"; import App from "./src/App.tsx";
console.log("Starting..");
const root = document.getElementById("root"); const root = document.getElementById("root");
ReactDOM.render(<App />, root); ReactDOM.render(<App />, root);

5
justfile Normal file
View File

@ -0,0 +1,5 @@
default:
just --list
copy-to-marjvena:
rsync --progress -r dist greg@marjvena.lan:/home/greg/

View File

@ -1,26 +1,42 @@
{ {
"name": "gues-kucinako", "name": "kucinako",
"version": "1.0.0", "version": "0.1.0",
"main": "index.js", "description": "Dictionary for Arzhanai conlangs",
"author": "greg <greg.shuflin@protonmail.com>", "repository": "gitea@gitea.everydayimshuflin.com:greg/gues-kucinako.git",
"author": "Greg Shuflin <greg.shuflin@protonmail.com>",
"license": "MIT", "license": "MIT",
"private": true, "private": true,
"dependencies": { "scripts": {
"@babel/preset-react": "^7.0.0", "start": "parcel index.html",
"parcel": "^1.12.3", "build": "parcel build index.html --no-source-maps --detailed-report",
"react": "^16.7.0", "prebuild": "yarn run typecheck",
"react-dom": "^16.7.0" "typecheck": "tsc --noEmit --jsx preserve",
"lint": "eslint --ext .ts,.tsx,.js .",
"lint-fix": "eslint --ext .ts,.tsx,.js . --fix"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.1.6", "@parcel/core": "2.9.3",
"@babel/preset-env": "^7.1.6", "@parcel/transformer-image": "2.9.3",
"@babel/preset-react": "^7.0.0", "@parcel/transformer-sass": "2.9.3",
"parcel-bundler": "^1.11.0", "@typescript-eslint/eslint-plugin": "^4.31.0",
"sass": "^1.16.1" "@typescript-eslint/parser": "^4.31.0",
"buffer": "^6.0.3",
"crypto-browserify": "^3.12.0",
"eslint": "^7.32.0",
"eslint-config-xo": "^0.38.0",
"eslint-plugin-react": "^7.25.1",
"events": "^3.3.0",
"parcel": "2.9.3",
"process": "^0.11.10",
"stream-browserify": "^3.0.0",
"tsc": "^2.0.4",
"typescript": "^4.4.3",
"util": "^0.12.4"
}, },
"scripts": { "dependencies": {
"dev": "parcel index.html", "dialog-polyfill": "^0.5.6",
"build": "parcel build index.html", "jsonwebtoken": "^8.5.1",
"deploy": "sudo cp dist/* /srv/http-kucinako/ && sudo chown -R http:http /srv/http-kucinako" "react": "^17.0.2",
"react-dom": "^17.0.2"
} }
} }

View File

@ -1,61 +0,0 @@
const vowelLetters = ['a', 'e', 'ê', 'i', 'o', 'ô', 'u', 'y', 'ø'];
const rootEndingPair = (str) => {
return { root: str.slice(0, -1), ending: str.slice(-1) };
};
function declineSaimiar(entry) {
const sai = entry.sai;
const morph = entry.morph_type;
if (morph == '-V') {
return vowelDeclension(sai);
} else if (morph == '-a/i') {
return aiDeclension(sai)
} else if (morph == "e-") {
return initalDeclension(sai);
} else if (morph == "-C") {
return consonantDeclension(sai);
} else {
console.warn(`Can't decline entry '${entry.sai}'`);
console.log(entry)
return null;
}
}
function vowelDeclension(sai) {
const { root, ending } = rootEndingPair(sai);
return {
"abs": `${root}${ending}`,
"erg": `${root}${ending}na`,
"adp": `${root}${ending}s`,
};
}
function aiDeclension(sai) {
const { root, ending } = rootEndingPair(sai);
return {
"abs": `${root}${ending}`,
"erg": `${root}${ending}na`,
"adp": `${root}${ending}s`,
};
}
function consonantDeclension(sai) {
const { root, ending } = rootEndingPair(sai);
return {
"abs": `${root}${ending}`,
"erg": `${root}${ending}na`,
"adp": `${root}${ending}s`,
};
}
function initalDeclension(sai) {
const { root, ending } = rootEndingPair(sai);
return {
"abs": `${root}${ending}`,
"erg": `${root}${ending}na`,
"adp": `${root}${ending}s`,
};
}
export { declineSaimiar };

View File

@ -1,6 +1,11 @@
body { body {
background-color: #f0f0b8; background-color: #f0f0b8;
font-size: 14pt; font-size: 18pt;
font-family: "Biwa";
}
input {
font-family: "Biwa";
} }
main { main {
@ -26,10 +31,16 @@ input {
width: 100%; width: 100%;
} }
.searchDropdown {
font-size: 22px;
font-family: "Biwa";
}
.searchButton { .searchButton {
padding: 5px; padding: 5px;
margin: 10px; margin: 10px;
font-dize: 22px; font-size: 22px;
font-family: "Biwa";
} }
.searchResult { .searchResult {
@ -37,10 +48,23 @@ input {
padding: 5px; padding: 5px;
} }
.synclass { .searchResultHeader {
padding-bottom: 1em;
}
.additionalNotes {
color: #a63333;
}
.semField {
font-variant: small-caps;
color: #6a3131;
}
.saimiarNounMorpho {
color: #a63333; color: #a63333;
i { i {
margin-right: 10px; color: #6a3131;
} }
} }

201
src/App.tsx Normal file
View File

@ -0,0 +1,201 @@
import React, {useState} from "react";
import dialogPolyfill from "dialog-polyfill";
import "./App.scss";
import {SaiEntryProps, JutEntryProps, ElesuEntryProps, TukEntryProps, Conlang, SearchDirection} from "./types";
import {SaiEntry, JutEntry, ElesuEntry, TukEntry} from "./Entries";
import {setPassword, searchEntry} from "./requests";
const PasswordDialog = (_props) => {
const [password, setPasswordStr] = useState("");
const save = () => {
setPassword(password);
const modal: any = document.querySelector(".passwordDialog");
dialogPolyfill.registerDialog(modal);
modal.close();
location.reload(); // TODO this is a hack
};
const cancel = () => {
const modal: any = document.querySelector(".passwordDialog");
dialogPolyfill.registerDialog(modal);
modal.close();
};
return (
<dialog className="passwordDialog">
<input type="password" placeholder="enter password" value={password} onChange={ (evt) => setPasswordStr(evt.target.value) } />
<button onClick={save}>Save</button>
<button onClick={cancel}>Cancel</button>
</dialog>);
};
const renderConlang = (conlang: Conlang): string => {
if (conlang === Conlang.Saimiar) {
return "Saimiar";
}
if (conlang === Conlang.Elesu) {
return "Elésu";
}
if (conlang === Conlang.Juteyuji) {
return "Juteyuji";
}
if (conlang === Conlang.Tukvaysi) {
return "Tukvaysi";
}
};
interface EntryProps {
conlang: Conlang;
entry: SaiEntryProps | JutEntryProps | ElesuEntryProps | TukEntryProps;
key: string;
}
const Entry = (props: EntryProps) => {
const {conlang} = props;
if (conlang === Conlang.Saimiar) {
return <SaiEntry entry={ props.entry as SaiEntryProps } />;
}
if (conlang === Conlang.Juteyuji) {
return <JutEntry entry={ props.entry as JutEntryProps } />;
}
if (conlang === Conlang.Elesu) {
return <ElesuEntry entry={ props.entry as ElesuEntryProps } />;
}
if (conlang === Conlang.Tukvaysi) {
return <TukEntry entry={ props.entry as TukEntryProps } />;
}
};
interface ResultsProps {
searchResults: Array<any>;
searchTerm: string;
conlang: Conlang;
direction: SearchDirection;
}
const Results = (props: ResultsProps) => {
const content = () => {
const {conlang} = props;
const num = props.searchResults.length;
const renderedName = renderConlang(conlang);
const searchType = (props.direction === SearchDirection.ToConlang) ? `English -> ${renderedName}` : `${renderedName} -> English`;
const result = num === 1 ? "result" : "results";
const header = (
<div className="searchResultHeader" key="header">
Searched for <b>{ props.searchTerm }</b>, { searchType }, found { num } { result }
</div>);
const entries = props.searchResults.map(
(entry, _idx) => <Entry entry={ entry } key= { entry.id } conlang={ conlang } />,
);
return [header].concat(entries);
};
const results = props.searchResults;
return (
<div className="results">
{ results ? content() : "No search" }
</div>
);
};
const convertSearchBoxShorthand = (input: string, conlang: Conlang): string => {
if (conlang === Conlang.Saimiar) {
return (input as any)
.replaceAll(/ee/g, "ê")
.replaceAll(/oo/g, "ô")
.replaceAll(/o'/g, "ø")
.replaceAll(/c'/g, "ç")
.replaceAll(/n'/g, "ŋ");
}
return input;
};
const App = (_props) => {
const defaultConlang = window.sessionStorage.getItem("conlang") as Conlang || Conlang.Saimiar;
const [searchResults, setSearchResults] = useState(null);
const [conlang, setConlangState] = useState(defaultConlang);
const [direction, setDirection] = useState(null);
const [searchTerm, setSearchTerm] = useState(null);
const [searchBoxInput, setSearchBoxInput] = useState("");
const setConlang = (conlang: Conlang) => {
setConlangState(conlang);
window.sessionStorage.setItem("conlang", conlang);
};
const handleSearch = (direction: SearchDirection) => {
const searchTerm = direction === SearchDirection.ToEnglish ? convertSearchBoxShorthand(searchBoxInput, conlang) : searchBoxInput;
if (searchTerm === "") {
setSearchResults(null);
setSearchTerm(null);
setDirection(null);
} else {
searchEntry(searchTerm, conlang, direction, (json) => {
setSearchResults(json);
setSearchTerm(searchTerm);
setDirection(direction);
});
}
};
const handleLangChange = (evt) => {
const conlang: Conlang = evt.target.value as Conlang;
setConlang(conlang);
setSearchResults(null);
};
const conlangs = [Conlang.Saimiar, Conlang.Elesu, Conlang.Tukvaysi, Conlang.Juteyuji];
const langSelectDropdown = (
<select className="searchDropdown" value={conlang} onChange={ handleLangChange }>
{conlangs.map((conlang) => <option value={conlang} key={conlang}>{renderConlang(conlang)}</option>)}
</select>
);
const showPasswordBox = () => {
const modal: any = document.querySelector(".passwordDialog");
dialogPolyfill.registerDialog(modal);
modal.showModal();
};
return (
<main>
<PasswordDialog />
<div className="container">
<div className="search">
<h1>Kucinako - Wordbook of Arzhanai languages</h1>
<p><b>Kucinako</b> (<i>Saimiar</i> "word-book") is a dictionary of words in various languages of the world Arzhanø, and their English
equivalents.</p>
<div className="textInput">
<input className="textInput" type="text" value={ searchBoxInput } onChange={ (evt) => {
setSearchBoxInput(evt.target.value);
} } />
</div>
<br/>
{ langSelectDropdown }
<button onClick={ () => handleSearch(SearchDirection.ToEnglish) } className="searchButton">{renderConlang(conlang)}</button>
<button onClick={ () => handleSearch(SearchDirection.ToConlang) } className="searchButton">English</button>
<button onClick={ showPasswordBox } className="searchButton">Password</button>
</div>
<Results
searchResults={ searchResults }
searchTerm= { searchTerm }
conlang={ conlang }
direction={ direction }
/>
</div>
</main>
);
};
export default App;

149
src/Entries.tsx Normal file
View File

@ -0,0 +1,149 @@
import React, {useState} from "react";
import {updateEntry, getPassword} from "./requests";
import {declineSaimiar} from "./saimiar_morphology";
import {SaiEntryProps, JutEntryProps, ElesuEntryProps, TukEntryProps, Conlang} from "./types";
interface BaseProps {
id: number;
conlang: Conlang;
conlangEntry: string;
english: string;
langSpecific: React.ReactNode;
}
const EntryBase = (props: BaseProps) => {
const [editing, setEditing] = useState(false);
const [english, setEnglish] = useState(props.english);
const mainEntryStyle = {
display: "flex",
justifyContent: "space-between",
flexDirection: "row",
};
const controlStyle = {
display: "flex",
justifyContent: "space-between",
flexDirection: "row",
minWidth: "20%",
};
const save = () => {
updateEntry(props.conlang, props.id, english);
};
const engTranslation = editing ? <input type="text" value={ english } onChange={ (evt) => setEnglish(evt.target.value) }/>
: english;
const EditControls = ({onSave}: { onSave: () => void }) => {
const cancel = () => setEditing(false);
const edit = (evt) => {
evt.preventDefault();
setEditing(true);
};
if (!getPassword()) {
return null;
}
return (editing ? (<span>
<button onClick={ onSave }>Save</button>
<button onClick={ cancel }>Cancel</button>
</span>)
: <a href="" onClick={edit}>Edit</a>);
};
return (
<div className="searchResult" key={ props.id }>
<div style={mainEntryStyle}>
<span><b>{ props.conlangEntry }</b> { engTranslation }</span>
<span style={controlStyle}>
<EditControls onSave={save} />
</span>
</div>
{ props.langSpecific }
</div>
);
};
const SaiEntry = (props: {entry: SaiEntryProps}) => {
const {entry} = props;
const synCategory = entry.syn_category;
const isNominal = synCategory === "nominal";
const barStyle = {
display: "inline-flex",
gap: "1em",
};
let morphology = null;
if (isNominal) {
const decl = declineSaimiar(entry);
if (decl) {
morphology = (<span className="saimiarNounMorpho">
Abs: <i>{decl.abs}</i>, Erg: <i>{decl.erg}</i>, Adp: <i>{decl.adp}</i>,
All: <i>{decl.all}</i>, Loc: <i>{decl.loc}</i>, Ell: <i>{decl.ell}</i>,
Inst: <i>{decl.inst}</i>, Rel: <i>{decl.rel}</i>
</span>);
}
}
const langSpecific = (
<div className="additionalNotes">
<span style={barStyle}>
<span className="synCategory">
<i>{ entry.syn_category }</i>
</span>
<span className="morphType">
{ entry.morph_type ? `\t\t${entry.morph_type}` : null }
</span>
{ entry.etym ? <span className="etym">etym.: <i>{entry.etym}</i></span>
: null
}
{ entry.semantic_field ? <span className="semField">{entry.semantic_field}</span> : null }
</span>
<br/>
{ morphology }
</div>
);
return <EntryBase id={entry.id} conlang={Conlang.Saimiar} conlangEntry={entry.sai} english={entry.eng} langSpecific={langSpecific} />;
};
const JutEntry = (props: {entry: JutEntryProps}) => {
const {entry} = props;
const langSpecific = (<div>
<span className="synclass">
{entry.syn_category} { entry.syn_category === "noun" ? `- ${entry.gender}` : null }
</span>
</div>);
return <EntryBase id={entry.id} conlang={Conlang.Juteyuji} conlangEntry={entry.jut} english={entry.eng} langSpecific={langSpecific} />;
};
const ElesuEntry = (props: {entry: ElesuEntryProps}) => {
const {entry} = props;
const langSpecific = <div>
<span className="synclass">
{ entry.syn_category }
</span>
</div>;
return <EntryBase id={entry.id} conlang={Conlang.Elesu} conlangEntry={entry.elesu} english={entry.eng} langSpecific={langSpecific} />;
};
const TukEntry = (props: {entry: TukEntryProps}) => {
const {entry} = props;
const langSpecific = <div>
<span className="synclass">
{ entry.syn_category }
</span>
</div>;
return <EntryBase id={entry.id} conlang={Conlang.Tukvaysi} conlangEntry={entry.tuk} english={entry.eng} langSpecific={langSpecific} />;
};
export {SaiEntry, ElesuEntry, JutEntry, TukEntry};

67
src/requests.ts Normal file
View File

@ -0,0 +1,67 @@
import jwt from "jsonwebtoken";
import {Conlang, SearchDirection} from "./types";
const backendUrl = "https://kucinakobackend.ichigo.everydayimshuflin.com";
const getPassword = (): string | null => window.sessionStorage.getItem("password");
const setPassword = (password: string) => {
window.sessionStorage.setItem("password", password);
};
const makeAuthorizationHeader = (key: string): string => {
const unixTime = Date.now() / 1000;
const token = jwt.sign({role: "conlang_postgrest_rw", exp: unixTime + 60}, key);
return `Bearer ${token}`;
};
const updateEntry = (conlang: Conlang, id: number, english: string) => {
const url = `${backendUrl}/${conlang.toString()}?id=eq.${id}`;
const request = new Request(url, {
method: "PATCH",
headers: {
Authorization: makeAuthorizationHeader(getPassword()),
"Content-Type": "application/json",
},
body: JSON.stringify({
eng: english,
}),
});
fetch(request).then((resp) => console.log(resp));
};
function searchEntry(searchTerm: string, conlang: Conlang, direction: SearchDirection, jsonHandler: (json: Object) => void) {
const specForConlang = {
[Conlang.Saimiar]: "sai",
[Conlang.Juteyuji]: "jut",
[Conlang.Tukvaysi]: "tuk",
[Conlang.Elesu]: "elesu",
};
const offset = 0;
const limit = 20;
const conlangDb = conlang.toString();
const conlangSpec = specForConlang[conlang];
const field = direction === SearchDirection.ToConlang ? "eng" : conlangSpec;
const params = new URLSearchParams([
[field, `like.*${searchTerm}*`],
["order", conlangSpec],
["limit", limit],
["offset", offset],
] as string[][]);
const effectiveUri = `${backendUrl}/${conlangDb}?${params}`;
fetch(`${effectiveUri}`)
.then((resp) => resp.json())
.then((json) => {
jsonHandler(json);
});
}
export {backendUrl, updateEntry, getPassword, setPassword, searchEntry};

110
src/saimiar_morphology.ts Normal file
View File

@ -0,0 +1,110 @@
const rootEndingPair = (str) => ({root: str.slice(0, -1), ending: str.slice(-1)});
type SaimiarDeclension = {
abs: string;
erg: string;
adp: string;
all: string;
loc: string;
ell: string;
inst: string;
rel: string;
};
function declineSaimiar(entry): SaimiarDeclension {
const split = entry.sai.split(" ");
const sai = split.at(-1);
const morph = entry.morph_type;
if (morph === "-V") {
return vowelDeclension(sai);
}
if (morph === "-a/i") {
return aiDeclension(sai);
}
if (morph === "e-") {
return initalDeclension(sai);
}
if (morph === "-C") {
return consonantDeclension(sai);
}
console.warn(`Can't decline entry '${entry.sai}'`);
console.log(entry);
return null;
}
function vowelDeclension(sai: string): SaimiarDeclension {
const {root, ending} = rootEndingPair(sai);
const adpEnding = ending === "u" ? "ys" : `${ending}s`;
return {
abs: `${root}${ending}`,
erg: `${root}${ending}na`,
adp: `${root}${adpEnding}`,
all: `so${root}${adpEnding}`,
loc: `${root}${ending}xa`,
ell: `tlê${root}${adpEnding}`,
inst: `${root}${ending}ŕa`,
rel: `${root}${ending}źi`,
};
}
function aiDeclension(sai: string): SaimiarDeclension {
const {root, ending} = rootEndingPair(sai);
return {
abs: `${root}${ending}`,
erg: `${root}iad`,
adp: `${root}i`,
all: `so${root}i`,
loc: `${root}iath`,
ell: `tlê${root}i`,
inst: `${root}iar`,
rel: `${root}iai`,
};
}
function consonantDeclension(sai: string): SaimiarDeclension {
const split = rootEndingPair(sai);
const root = split.ending === "ø" ? split.root : sai;
const absFinal = split.ending === "ø" ? "ø" : "";
return {
abs: `${root}${absFinal}`,
erg: `${root}ad`,
adp: `${root}e`,
all: `so${root}i`,
loc: `${root}ak`,
ell: `tlê${root}i`,
inst: `${root}ar`,
rel: `${root}ai`,
};
}
const vowels = ["a", "e", "ê", "i", "o", "ô", "u", "y"];
function initalDeclension(sai: string): SaimiarDeclension {
const initial = sai.slice(0, 1);
const root = sai.slice(1);
const finalRootSound = root.slice(-1);
const finalVowel = vowels.includes(finalRootSound);
const instEnding = finalVowel ? "ŕø" : "ar";
const relEnding = finalVowel ? "źi" : "ai";
return {
abs: `${initial}${root}`,
erg: `da${root}`,
adp: `i${root}`,
all: `so${root}`,
loc: `xa${root}`,
ell: `tlê${root}`,
inst: `i${root}${instEnding}`,
rel: `${initial}${root}${relEnding}`,
};
}
export {declineSaimiar, SaimiarDeclension};

54
src/types.ts Normal file
View File

@ -0,0 +1,54 @@
/* eslint-disable camelcase */
enum Conlang {
Saimiar = "saimiar",
Elesu = "elesu",
Tukvaysi = "tukvaysi",
Juteyuji = "juteyuji",
}
enum SearchDirection {
ToConlang,
ToEnglish
}
interface SaiEntryProps {
id: number;
sai: string;
eng: string;
syn_category: string;
morph_type: string;
etym: string;
semantic_field: string;
notes: string;
}
interface JutEntryProps {
id: number;
jut: string;
eng: string;
syn_category: string;
gender: string;
notes: string;
}
interface ElesuEntryProps {
id: number;
elesu: string;
eng: string;
syn_category: string;
gender: string;
sai_borrowing: string;
notes: string;
proto_southern_root: string;
}
interface TukEntryProps {
id: number;
tuk: string;
eng: string;
syn_category: string;
notes: string;
}
export {SaiEntryProps, JutEntryProps, ElesuEntryProps, TukEntryProps, Conlang, SearchDirection};

6
tsconfig.json Normal file
View File

@ -0,0 +1,6 @@
{
"compilerOptions": {
"lib": ["ES2020", "dom"]
},
"include": ["src/**/*"]
}

8853
yarn.lock

File diff suppressed because it is too large Load Diff