Compare commits
No commits in common. "master" and "a1a6370eee48f2a244cd6efbd24b8e54274e5b83" have entirely different histories.
master
...
a1a6370eee
33
.eslintrc.js
33
.eslintrc.js
@ -1,33 +0,0 @@
|
|||||||
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",
|
|
||||||
},
|
|
||||||
};
|
|
12
.gitignore
vendored
12
.gitignore
vendored
@ -1,12 +0,0 @@
|
|||||||
dist/
|
|
||||||
node_modules/
|
|
||||||
.yarn/*
|
|
||||||
!.yarn/cache
|
|
||||||
!.yarn/patches
|
|
||||||
!.yarn/plugins
|
|
||||||
!.yarn/releases
|
|
||||||
!.yarn/sdks
|
|
||||||
!.yarn/versions
|
|
||||||
|
|
||||||
.parcel-cache
|
|
||||||
.aider*
|
|
181
App.jsx
Normal file
181
App.jsx
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
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;
|
46
App.scss
Normal file
46
App.scss
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
body {
|
||||||
|
background-color: #f0f0b8;
|
||||||
|
font-size: 14pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
div .container {
|
||||||
|
max-width: 62rem;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
div .results {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
div .textInput {
|
||||||
|
max-width: 70%;
|
||||||
|
margin: auto;
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchButton {
|
||||||
|
padding: 5px;
|
||||||
|
margin: 10px;
|
||||||
|
font-dize: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchResult {
|
||||||
|
border-bottom: 1px solid #b2a336;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.synclass {
|
||||||
|
color: #a63333;
|
||||||
|
i {
|
||||||
|
margin-right: 10px;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
61
flake.lock
generated
61
flake.lock
generated
@ -1,61 +0,0 @@
|
|||||||
{
|
|
||||||
"nodes": {
|
|
||||||
"flake-utils": {
|
|
||||||
"inputs": {
|
|
||||||
"systems": "systems"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1731533236,
|
|
||||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1743964447,
|
|
||||||
"narHash": "sha256-nEo1t3Q0F+0jQ36HJfbJtiRU4OI+/0jX/iITURKe3EE=",
|
|
||||||
"owner": "nixos",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "063dece00c5a77e4a0ea24e5e5a5bd75232806f8",
|
|
||||||
"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
|
|
||||||
}
|
|
42
flake.nix
42
flake.nix
@ -1,42 +0,0 @@
|
|||||||
{
|
|
||||||
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};
|
|
||||||
frontend = pkgs.stdenv.mkDerivation (finalAttrs: {
|
|
||||||
pname = "kucinako";
|
|
||||||
version = "0.1";
|
|
||||||
src = ./.;
|
|
||||||
yarnOfflineCache = pkgs.fetchYarnDeps {
|
|
||||||
yarnLock = finalAttrs.src + "/yarn.lock";
|
|
||||||
hash = "sha256-g5g2xlwDxH8O8zaLJ4meO1+DQdJIomVPqd6RXTAhDuE=";
|
|
||||||
};
|
|
||||||
nativeBuildInputs = with pkgs; [
|
|
||||||
yarnConfigHook
|
|
||||||
yarnBuildHook
|
|
||||||
# Needed for executing package.json scripts
|
|
||||||
nodejs
|
|
||||||
];
|
|
||||||
installPhase = ''
|
|
||||||
mkdir $out
|
|
||||||
mv dist/* $out
|
|
||||||
'';
|
|
||||||
});
|
|
||||||
in {
|
|
||||||
packages = {
|
|
||||||
default = frontend;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
@ -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 lang="en">
|
<html>
|
||||||
<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 type="module" src="./index.js"></script>
|
<script src="./index.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
4
index.js
4
index.js
@ -1,6 +1,8 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import App from "./src/App.tsx";
|
import App from "./App.jsx";
|
||||||
|
|
||||||
|
console.log("Starting..");
|
||||||
|
|
||||||
const root = document.getElementById("root");
|
const root = document.getElementById("root");
|
||||||
ReactDOM.render(<App />, root);
|
ReactDOM.render(<App />, root);
|
||||||
|
9
justfile
9
justfile
@ -1,9 +0,0 @@
|
|||||||
default:
|
|
||||||
just --list
|
|
||||||
|
|
||||||
# Run local instance
|
|
||||||
run-local:
|
|
||||||
yarn start
|
|
||||||
|
|
||||||
copy-to-marjvena:
|
|
||||||
rsync --progress -r dist greg@marjvena.lan:/home/greg/
|
|
52
package.json
52
package.json
@ -1,42 +1,26 @@
|
|||||||
{
|
{
|
||||||
"name": "kucinako",
|
"name": "gues-kucinako",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"description": "Dictionary for Arzhanai conlangs",
|
"main": "index.js",
|
||||||
"repository": "gitea@gitea.everydayimshuflin.com:greg/gues-kucinako.git",
|
"author": "greg <greg.shuflin@protonmail.com>",
|
||||||
"author": "Greg Shuflin <greg.shuflin@protonmail.com>",
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"dependencies": {
|
||||||
"start": "parcel index.html",
|
"@babel/preset-react": "^7.0.0",
|
||||||
"build": "parcel build index.html --no-source-maps --detailed-report",
|
"parcel": "^1.12.3",
|
||||||
"prebuild": "yarn run typecheck",
|
"react": "^16.7.0",
|
||||||
"typecheck": "tsc --noEmit --jsx preserve",
|
"react-dom": "^16.7.0"
|
||||||
"lint": "eslint --ext .ts,.tsx,.js .",
|
|
||||||
"lint-fix": "eslint --ext .ts,.tsx,.js . --fix"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@parcel/core": "2.9.3",
|
"@babel/core": "^7.1.6",
|
||||||
"@parcel/transformer-image": "2.9.3",
|
"@babel/preset-env": "^7.1.6",
|
||||||
"@parcel/transformer-sass": "2.9.3",
|
"@babel/preset-react": "^7.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.31.0",
|
"parcel-bundler": "^1.11.0",
|
||||||
"@typescript-eslint/parser": "^4.31.0",
|
"sass": "^1.16.1"
|
||||||
"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"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"scripts": {
|
||||||
"dialog-polyfill": "^0.5.6",
|
"dev": "parcel index.html",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"build": "parcel build index.html",
|
||||||
"react": "^17.0.2",
|
"deploy": "sudo cp dist/* /srv/http-kucinako/ && sudo chown -R http:http /srv/http-kucinako"
|
||||||
"react-dom": "^17.0.2"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
61
saimiar_morphology.js
Normal file
61
saimiar_morphology.js
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
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 };
|
281
src/App.scss
281
src/App.scss
@ -1,281 +0,0 @@
|
|||||||
// Color variables
|
|
||||||
$background-color: #f0f0b8;
|
|
||||||
$primary-color: #b2a336;
|
|
||||||
$primary-color-dark: #a69530;
|
|
||||||
$text-color: #333;
|
|
||||||
$accent-color: #a63333;
|
|
||||||
$accent-color-dark: #6a3131;
|
|
||||||
$white: #fff;
|
|
||||||
|
|
||||||
body {
|
|
||||||
background-color: $background-color;
|
|
||||||
font-size: 18pt;
|
|
||||||
font-family: "Biwa";
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
font-family: "Biwa";
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
text-align: center;
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
div .container {
|
|
||||||
max-width: 62rem;
|
|
||||||
margin: auto;
|
|
||||||
padding: 0 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
div .results {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
div .textInput {
|
|
||||||
max-width: 100%;
|
|
||||||
margin: auto;
|
|
||||||
font-size: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 8px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
border: 1px solid $primary-color;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.searchDropdown {
|
|
||||||
font-size: 22px;
|
|
||||||
font-family: "Biwa";
|
|
||||||
padding: 10px 15px;
|
|
||||||
border: 2px solid $primary-color;
|
|
||||||
border-radius: 8px;
|
|
||||||
background-color: $white;
|
|
||||||
color: $text-color;
|
|
||||||
cursor: pointer;
|
|
||||||
appearance: none;
|
|
||||||
-webkit-appearance: none;
|
|
||||||
-moz-appearance: none;
|
|
||||||
$encoded-primary-color: str-slice("#{$primary-color}", 2);
|
|
||||||
background-image: url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23#{$encoded-primary-color}%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E");
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: right 15px top 50%;
|
|
||||||
background-size: 12px auto;
|
|
||||||
padding-right: 35px;
|
|
||||||
transition: border-color 0.3s, box-shadow 0.3s;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 300px;
|
|
||||||
margin: 10px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.searchDropdown:hover, .searchDropdown:focus {
|
|
||||||
border-color: $primary-color-dark;
|
|
||||||
box-shadow: 0 0 0 3px rgba($primary-color, 0.25);
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.searchButton {
|
|
||||||
padding: 10px 15px;
|
|
||||||
margin: 10px 5px;
|
|
||||||
font-size: 22px;
|
|
||||||
font-family: "Biwa";
|
|
||||||
background-color: $primary-color;
|
|
||||||
color: $white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.3s, transform 0.1s;
|
|
||||||
min-width: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.searchButton:hover {
|
|
||||||
background-color: $primary-color-dark;
|
|
||||||
}
|
|
||||||
|
|
||||||
.searchButton:active {
|
|
||||||
transform: translateY(1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.searchResult {
|
|
||||||
border-bottom: 1px solid $primary-color;
|
|
||||||
padding: 10px 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.searchResultHeader {
|
|
||||||
padding-bottom: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.additionalNotes {
|
|
||||||
color: $accent-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.semField {
|
|
||||||
font-variant: small-caps;
|
|
||||||
color: $accent-color-dark;
|
|
||||||
}
|
|
||||||
|
|
||||||
.saimiarNounMorpho {
|
|
||||||
color: $accent-color;
|
|
||||||
i {
|
|
||||||
color: $accent-color-dark;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.passwordDialog {
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: 1px solid $primary-color;
|
|
||||||
background-color: $background-color;
|
|
||||||
width: 90%;
|
|
||||||
max-width: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.passwordDialog h3 {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.passwordDialog input {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialogButtons {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.passwordDialog button {
|
|
||||||
margin: 5px;
|
|
||||||
padding: 8px 15px;
|
|
||||||
background-color: $primary-color;
|
|
||||||
color: $white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
min-width: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.passwordDialog button:hover {
|
|
||||||
background-color: $primary-color-dark;
|
|
||||||
}
|
|
||||||
|
|
||||||
.searchControls {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttonGroup {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: center;
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdownContainer {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdownLabel {
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
color: $accent-color-dark;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive styles for mobile devices */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
body {
|
|
||||||
font-size: 16pt;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 1.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
div .textInput {
|
|
||||||
max-width: 100%;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.searchDropdown {
|
|
||||||
font-size: 18px;
|
|
||||||
padding: 12px 15px;
|
|
||||||
margin: 10px auto;
|
|
||||||
width: 100%;
|
|
||||||
max-width: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search button {
|
|
||||||
width: 100%;
|
|
||||||
margin: 5px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.searchControls {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttonGroup {
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdownContainer {
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdownLabel {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.searchButton {
|
|
||||||
font-size: 18px;
|
|
||||||
padding: 12px 15px;
|
|
||||||
margin: 5px 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
body {
|
|
||||||
font-size: 14pt;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
padding: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
div .container {
|
|
||||||
padding: 0 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.searchDropdown {
|
|
||||||
font-size: 16px;
|
|
||||||
padding: 10px 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.searchButton {
|
|
||||||
font-size: 16px;
|
|
||||||
padding: 10px 12px;
|
|
||||||
}
|
|
||||||
}
|
|
238
src/App.tsx
238
src/App.tsx
@ -1,238 +0,0 @@
|
|||||||
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">
|
|
||||||
<h3>Enter Password</h3>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
placeholder="enter password"
|
|
||||||
value={password}
|
|
||||||
onChange={ (evt) => setPasswordStr(evt.target.value) }
|
|
||||||
/>
|
|
||||||
<div className="dialogButtons">
|
|
||||||
<button onClick={save}>Save</button>
|
|
||||||
<button onClick={cancel}>Cancel</button>
|
|
||||||
</div>
|
|
||||||
</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) => {
|
|
||||||
// First convert any shorthand notation
|
|
||||||
let processedSearchTerm = direction === SearchDirection.ToEnglish ?
|
|
||||||
convertSearchBoxShorthand(searchBoxInput, conlang) :
|
|
||||||
searchBoxInput;
|
|
||||||
|
|
||||||
// Then normalize to lowercase
|
|
||||||
processedSearchTerm = processedSearchTerm.toLowerCase();
|
|
||||||
|
|
||||||
if (processedSearchTerm === "") {
|
|
||||||
setSearchResults(null);
|
|
||||||
setSearchTerm(null);
|
|
||||||
setDirection(null);
|
|
||||||
} else {
|
|
||||||
searchEntry(processedSearchTerm, conlang, direction, (json) => {
|
|
||||||
setSearchResults(json);
|
|
||||||
setSearchTerm(processedSearchTerm);
|
|
||||||
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 = (
|
|
||||||
<div className="dropdownContainer">
|
|
||||||
<label htmlFor="languageSelect" className="dropdownLabel">Select Language:</label>
|
|
||||||
<select
|
|
||||||
id="languageSelect"
|
|
||||||
className="searchDropdown"
|
|
||||||
value={conlang}
|
|
||||||
onChange={handleLangChange}
|
|
||||||
aria-label="Select language"
|
|
||||||
>
|
|
||||||
{conlangs.map((conlang) => (
|
|
||||||
<option value={conlang} key={conlang}>
|
|
||||||
{renderConlang(conlang)}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
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);
|
|
||||||
}}
|
|
||||||
placeholder="Enter search term (case-insensitive)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="searchControls">
|
|
||||||
{ langSelectDropdown }
|
|
||||||
<div className="buttonGroup">
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Results
|
|
||||||
searchResults={ searchResults }
|
|
||||||
searchTerm={ searchTerm }
|
|
||||||
conlang={ conlang }
|
|
||||||
direction={ direction }
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default App;
|
|
163
src/Entries.tsx
163
src/Entries.tsx
@ -1,163 +0,0 @@
|
|||||||
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",
|
|
||||||
flexWrap: "wrap",
|
|
||||||
};
|
|
||||||
|
|
||||||
const controlStyle = {
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
flexDirection: "row",
|
|
||||||
minWidth: "20%",
|
|
||||||
marginTop: "10px",
|
|
||||||
};
|
|
||||||
|
|
||||||
const save = () => {
|
|
||||||
updateEntry(props.conlang, props.id, english);
|
|
||||||
};
|
|
||||||
|
|
||||||
const engTranslation = editing ?
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={ english }
|
|
||||||
onChange={ (evt) => setEnglish(evt.target.value) }
|
|
||||||
style={{ width: "100%", marginTop: "5px" }}
|
|
||||||
/> : 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 = (
|
|
||||||
<table className="saimiarNounMorpho">
|
|
||||||
<tr>
|
|
||||||
<td>Abs: <i>{decl.abs}</i></td><td>Erg: <i>{decl.erg}</i></td><td>Adp: <i>{decl.adp}</i></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>All: <i>{decl.all}</i></td><td>Loc: <i>{decl.loc}</i></td><td>Ell: <i>{decl.ell}</i></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Inst: <i>{decl.inst}</i></td><td>Rel: <i>{decl.rel}</i></td><td></td>
|
|
||||||
</tr>
|
|
||||||
</table>);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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};
|
|
@ -1,67 +0,0 @@
|
|||||||
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};
|
|
@ -1,110 +0,0 @@
|
|||||||
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
54
src/types.ts
@ -1,54 +0,0 @@
|
|||||||
/* 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};
|
|
@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"lib": ["ES2020", "dom"],
|
|
||||||
"jsx": "react",
|
|
||||||
"esModuleInterop": true
|
|
||||||
},
|
|
||||||
"include": ["src/**/*"]
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user