WIP: ktb-static #5

Draft
niko wants to merge 9 commits from ktb-static into main
5 changed files with 556 additions and 192 deletions
Showing only changes of commit da7d75641d - Show all commits

View file

@ -1,28 +1,20 @@
# Viossa.net # Viossa.net kotoba-tumam
bråtula viossa.net måde! We're here to build an informational website about Viossa. bråtula viossa.net måde! We're here to build an informational website about Viossa.
## The Stack ## The Stack
**What will we be using to build this site?** **What will we be using to build this site?**
### Core ### Backend
- [Node.js](https://nodejs.org/)
- [TypeScript](https://www.typescriptlang.org/) - [TypeScript](https://www.typescriptlang.org/)
- [pnpm](https://pnpm.io/) - [pnpm](https://pnpm.io/)
- [Turborepo](https://turborepo.com/) - [Turborepo](https://turborepo.com/)
### Frontend ### Frontend
- [Vue 3](https://vuejs.org/) - The ktb frontend is purely coded in HTML, CSS, and browser JS.
- [Vite](https://vite.dev/) - [AlpineJS](https://alpinejs.dev/)
Additionally, we will be following [**atomic design principles**](https://bradfrost.com/blog/post/atomic-web-design/) to organize the components of the project.
### Styling
- [Bulma](https://bulma.io/)
- [Tailwind CSS](https://tailwindcss.com/)
- [Sass](https://sass-lang.com/)
### Backend
- [Node.js](https://nodejs.org/)
### Linting ### Linting
- [Prettier](https://prettier.io/) - [Prettier](https://prettier.io/)
@ -46,15 +38,12 @@ Additionally, we will be following [**atomic design principles**](https://bradfr
This project uses Turborepo for task management/caching. Install Turborepo globally on your machine to allow for executing turbo commands more easily: `pnpm i -g turbo` (this is needed to continue with the instructions below) This project uses Turborepo for task management/caching. Install Turborepo globally on your machine to allow for executing turbo commands more easily: `pnpm i -g turbo` (this is needed to continue with the instructions below)
### Frontend (Viossa.net) ### Frontend (Viossa.net)
1. Ensure you're in the root directory of the project (`ViossaDotNet`) 1. The frontend no longer requires a compilation step. Yippee!
1. Move into the app's directory: `cd apps/vdn-static`
1. To run the site, use `turbo dev`. This will set up watchers to build all libraries used by the frontend, as well as hot-refreshing the site as changes are made to it.
1. To view the website running locally, visit http://localhost:1224/ in your browser!
### Backend (Viossa DB) ### Backend (Viossa DB)
1. Ensure you're in the root directory of the project (`ViossaDotNet`) 1. Ensure you're in the root directory of the project (`ViossaDotNet`)
1. Move into the app's directory: `cd apps/vdb-backend` 1. Move into the app's directory: `cd apps/vdb-backend`
1. To run the site, use `turbo start`. This will build all of the app's dependencies and then start the application. 1. To run the API, use `turbo start`. This will build all of the app's dependencies and then start the application.
1. **NOTE:** Backend apps are not watched/hot-refreshed like frontend apps! If you make changes, you must kill the app and re-run it to apply changes. 1. **NOTE:** Backend apps are not watched/hot-refreshed like frontend apps! If you make changes, you must kill the app and re-run it to apply changes.
1. To view a sample response from the backend API, visit http://localhost:1225/sample in your browser! 1. To view a sample response from the backend API, visit http://localhost:1225/sample in your browser!

View file

@ -1,4 +1,5 @@
// script.js let HOST = 'http://localhost:1225'
document.addEventListener("alpine:init", () => { document.addEventListener("alpine:init", () => {
Alpine.store("dictionary", { Alpine.store("dictionary", {
search_results: { search_results: {
@ -83,7 +84,7 @@ document.addEventListener("alpine:init", () => {
async fetch_all_lects() { async fetch_all_lects() {
try { try {
const res = await axios.get("http://localhost:1225/lects"); const res = await axios.get(`${HOST}/lects`);
return res.data; return res.data;
} catch (e) { } catch (e) {
console.error(e); console.error(e);
@ -92,7 +93,7 @@ document.addEventListener("alpine:init", () => {
async post_lect(lect_name) { async post_lect(lect_name) {
try { try {
const res = await axios.post("http://localhost:1225/lect", { const res = await axios.post(`${HOST}/lect`, {
lect_name, lect_name,
}); });
return res.data.lect; return res.data.lect;
@ -103,7 +104,7 @@ document.addEventListener("alpine:init", () => {
async fetch_all_terms(search_term) { async fetch_all_terms(search_term) {
try { try {
const res = await axios.get("http://localhost:1225/search", { const res = await axios.get(`${HOST}/search`, {
params: { search_term: search_term }, params: { search_term: search_term },
}); });
this.search_results = res.data; this.search_results = res.data;
@ -115,7 +116,7 @@ document.addEventListener("alpine:init", () => {
async fetch_one_lemma_detail(lemma_name) { async fetch_one_lemma_detail(lemma_name) {
try { try {
const res = await axios.get( const res = await axios.get(
"http://localhost:1225/lemma-detail", `${HOST}/lemma-detail`,
{ params: { lemma_name } }, { params: { lemma_name } },
); );
return res.data.lemma_detail; return res.data.lemma_detail;
@ -127,7 +128,7 @@ document.addEventListener("alpine:init", () => {
async put_definition(definition_id, definition_text, lemma_name) { async put_definition(definition_id, definition_text, lemma_name) {
try { try {
const res = await axios.put( const res = await axios.put(
"http://localhost:1225/definition", `${HOST}/definition`,
{ definition_id, definition_text, lemma_name }, { definition_id, definition_text, lemma_name },
); );
return res.data.lemma_detail; return res.data.lemma_detail;
@ -142,7 +143,7 @@ document.addEventListener("alpine:init", () => {
) { ) {
try { try {
const res = await axios.delete( const res = await axios.delete(
`http://localhost:1225/definition/${definition_id}`, `${HOST}/definition/${definition_id}`,
); );
} catch (e) { } catch (e) {
console.error(e); console.error(e);
@ -152,7 +153,7 @@ document.addEventListener("alpine:init", () => {
async put_example(example_id, example_text, lemma_name) { async put_example(example_id, example_text, lemma_name) {
try { try {
const res = await axios.put("http://localhost:1225/example", { const res = await axios.put(`${HOST}/example`, {
example_id, example_id,
example_text, example_text,
lemma_name, lemma_name,
@ -169,7 +170,7 @@ document.addEventListener("alpine:init", () => {
) { ) {
try { try {
const res = await axios.delete( const res = await axios.delete(
`http://localhost:1225/example/${example_id}`, `${HOST}/example/${example_id}`,
); );
} catch (e) { } catch (e) {
console.error(e); console.error(e);
@ -179,7 +180,7 @@ document.addEventListener("alpine:init", () => {
async put_word_form_text(word_form_id, word_form_text) { async put_word_form_text(word_form_id, word_form_text) {
try { try {
const res = await axios.put("http://localhost:1225/word-form", { const res = await axios.put(`${HOST}/word-form`, {
word_form_id, word_form_id,
word_form_text, word_form_text,
}); });
@ -198,7 +199,7 @@ document.addEventListener("alpine:init", () => {
try { try {
const res = await axios.post( const res = await axios.post(
"http://localhost:1225/word-form", `${HOST}/word-form`,
{ lemma_name, lect_name, word_form_text }, { lemma_name, lect_name, word_form_text },
); );
return res.data.lemma_detail; return res.data.lemma_detail;
@ -207,13 +208,26 @@ document.addEventListener("alpine:init", () => {
} }
}, },
async post_lemma(lect_name, word_form_text){
/* Accepts a new word form for a lemma and the corresponding lect, initializing with the provided form. */
try {
const res = await axios.post(
`${HOST}/lemma`,
{ lect_name, word_form_text },
);
return res.data.lemma_detail;
} catch (e) {
console.error(e);
}
},
async delete_word_form(word_form_id, word_form_text) { async delete_word_form(word_form_id, word_form_text) {
if ( if (
window.confirm(`Du keshite kofal ${word_form_text}.\nPravda?`) window.confirm(`Du keshite kofal ${word_form_text}.\nPravda?`)
) { ) {
try { try {
const res = await axios.delete( const res = await axios.delete(
`http://localhost:1225/word-form/${word_form_id}`, `${HOST}/word-form/${word_form_id}`,
); );
return res.data.lemma_detail; return res.data.lemma_detail;
} catch (e) { } catch (e) {

View file

@ -21,9 +21,48 @@
lects: [], lects: [],
current_result_page: 0, current_result_page: 0,
results_per_page: 30, results_per_page: 30,
new_lemma_form_open: false,
md(text){ md(text){
return $store.dictionary.md(text); return $store.dictionary.md(text);
}, },
async put_word_form_text(word_form_id, word_form_text){
$store.dictionary.put_word_form_text(word_form_id, word_form_text).then(()=>{
this.load_detail();
});
},
async post_new_word_form(lemma_name, lect_name, word_form_text){
$store.dictionary.post_new_word_form(lemma_name, lect_name, word_form_text).then(()=>{
this.load_detail();
});
},
async delete_word_form(word_form_id, word_form_text){
$store.dictionary.delete_word_form(word_form_id, word_form_text).then(()=>{
this.load_detail();
});
},
async put_definition(definition_id, definition_text){
$store.dictionary.put_definition(definition_id, definition_text, this.lemma_detail?.lemma_name).then(()=>{
this.load_detail();
});
},
async delete_definition(definition_id, definition_text){
$store.dictionary.delete_definition(definition_id, definition_text).then(()=>{
this.load_detail();
});
},
async put_example(example_id, example_text){
$store.dictionary.put_example(example_id, example_text, this.lemma_detail?.lemma_name).then(()=>{
this.load_detail();
});
},
async post_lemma(lect_name, word_form_text){
$store.dictionary.post_lemma(lect_name, word_form_text).then(lemma=>console.info(JSON.stringify(lemma)));
},
async delete_example(example_id, example_text){
$store.dictionary.delete_example(example_id, example_text).then(()=>{
this.load_detail();
});
},
}" }"
x-init="$store.dictionary.fetch_all_lects().then((lects_response)=>{ x-init="$store.dictionary.fetch_all_lects().then((lects_response)=>{
if(!!lects_response.lects) { if(!!lects_response.lects) {
@ -32,7 +71,6 @@
});"> });">
<aside class="sidebar"> <aside class="sidebar">
<div class="preferences-pane"> <div class="preferences-pane">
<button <button
@click="showPrefs = !showPrefs" @click="showPrefs = !showPrefs"
class="prefs-toggle is-fullwidth"> class="prefs-toggle is-fullwidth">
@ -83,39 +121,53 @@
</aside> </aside>
<main class="main-content"> <main class="main-content">
<div class="search-bar"> <div class="top-bar sticky top">
<button <div class="search-bar">
@click="$store.dictionary.fetch_all_terms(search_term)"> <button
Zuha @click="$store.dictionary.fetch_all_terms(search_term)">
</button> Zuha
<input </button>
type="text" <input
x-model="search_term" type="text"
@keydown.enter="$store.dictionary.fetch_all_terms(search_term)" x-model="search_term"
placeholder="Tasta ko(tel) her..." /> @keydown.enter="$store.dictionary.fetch_all_terms(search_term)"
</div> placeholder="Tasta ko(tel) her..." />
<div class="flex-row" <button
x-data="{ class="hollow primary"
page_count: 1, @click="new_lemma_form_open = true">
item_count: 0 +
}" </button>
x-effect="current_result_page = Math.min(current_result_page, page_count-1); page_count = Math.ceil($store.dictionary.search_results.terms/results_per_page); item_count = $store.dictionary.search_results.terms;"> </div>
<button <div
class="hollow primary" class="flex-row"
x-show="item_count > results_per_page && current_result_page > 0" x-data="{
@click="current_result_page-=1"><strong></strong></button> page_count: 1,
<div x-text="`${current_result_page + 1} / ${page_count}`"></div> item_count: 0
<button }"
class="hollow primary" x-effect="current_result_page = Math.min(current_result_page, page_count-1); page_count = Math.ceil($store.dictionary.search_results.terms/results_per_page); item_count = $store.dictionary.search_results.terms;">
x-show="item_count > results_per_page && current_result_page+1 < page_count" <button
@click="current_result_page+=1"><strong></strong></button> class="hollow primary"
<select x-model="results_per_page"> :disabled="!(item_count > results_per_page && current_result_page > 0)"
<option value="15">15</option> @click="current_result_page-=1">
<option value="30">30</option> <strong></strong>
<option value="60">60</option> </button>
<option value="120">120</option> <div
</select> x-text="`${current_result_page + 1} / ${page_count}`"></div>
<button
class="hollow primary"
:disabled="!(item_count > results_per_page && current_result_page+1 < page_count)"
@click="current_result_page+=1">
<strong></strong>
</button>
<select x-model="results_per_page">
<option value="15">15</option>
<option value="30">30</option>
<option value="60">60</option>
<option value="120">120</option>
</select>
</div>
</div> </div>
<div class="cards-container"> <div class="cards-container">
<template <template
x-for="item in $store.dictionary.filter_results(search_term).slice(current_result_page*results_per_page, Math.min((current_result_page+1)*results_per_page, $store.dictionary.search_results.terms))" x-for="item in $store.dictionary.filter_results(search_term).slice(current_result_page*results_per_page, Math.min((current_result_page+1)*results_per_page, $store.dictionary.search_results.terms))"
@ -132,41 +184,7 @@
example_text: '', example_text: '',
definition_text: '', definition_text: '',
lemma_detail: null, lemma_detail: null,
async put_word_form_text(word_form_id, word_form_text){
$store.dictionary.put_word_form_text(word_form_id, word_form_text).then(()=>{
this.load_detail();
});
},
async post_new_word_form(lemma_name, lect_name, word_form_text){
$store.dictionary.post_new_word_form(lemma_name, lect_name, word_form_text).then(()=>{
this.load_detail();
});
},
async delete_word_form(word_form_id, word_form_text){
$store.dictionary.delete_word_form(word_form_id, word_form_text).then(()=>{
this.load_detail();
});
},
async put_definition(definition_id, definition_text){
$store.dictionary.put_definition(definition_id, definition_text, this.lemma_detail?.lemma_name).then(()=>{
this.load_detail();
});
},
async delete_definition(definition_id, definition_text){
$store.dictionary.delete_definition(definition_id, definition_text).then(()=>{
this.load_detail();
});
},
async put_example(example_id, example_text){
$store.dictionary.put_example(example_id, example_text, this.lemma_detail?.lemma_name).then(()=>{
this.load_detail();
});
},
async delete_example(example_id, example_text){
$store.dictionary.delete_example(example_id, example_text).then(()=>{
this.load_detail();
});
},
async toggle_expand() { async toggle_expand() {
if(!this.lemma_detail){ if(!this.lemma_detail){
/* If there isn't a lemma detail loaded yet, get it then expand. */ /* If there isn't a lemma detail loaded yet, get it then expand. */
@ -209,80 +227,157 @@
<h4>Trofal</h4> <h4>Trofal</h4>
<table class="is-fullwidth"> <table class="is-fullwidth">
<tbody> <tbody>
<template <template
x-for="wf2 in (lemma_detail?.word_forms ?? [])"> x-for="wf2 in (lemma_detail?.word_forms ?? [])">
<tr x-data="{ <tr
x-data="{
editing:false, editing:false,
edited_text: wf2.word_form edited_text: wf2.word_form
}"> }">
<td style="width: 8em;" <td
x-text="wf2 ? wf2.lect.name : '(empty)'"></td> style="width: 8em"
<td> x-text="wf2 ? wf2.lect.name : '(empty)'"></td>
<span x-text="wf2 ? wf2.word_form : '(empty)'" <td>
@click="editing = true" <span
x-show="!editing"></span> x-text="wf2 ? wf2.word_form : '(empty)'"
@click="editing = true"
<input x-show="!editing"></span>
type="text"
class="is-fullwidth" <input
@click.outside="editing = false" type="text"
x-show="editing" class="is-fullwidth"
x-model="edited_text"> @click.outside="editing = false"
</td> x-show="editing"
<td style="width: 5em;"> x-model="edited_text" />
<div class="float-right"> </td>
<button class="tiny hollow primary" x-show="!editing" title="kawari" <td style="width: 5em">
@click="editing = true" >~</button> <div class="float-right">
<button class="tiny hollow delete" x-show="!editing" title="keshite" <button
@click="await delete_word_form(wf2.word_form_id, wf2.word_form)" >x</button> class="tiny hollow primary"
<button class="tiny edit" x-show="editing" title="jame" x-show="!editing"
@click="editing = false">/</button> title="kawari"
<button class="tiny" x-show="editing" title="antaa" @click="editing = true">
@click="await put_word_form_text(wf2.word_form_id, edited_text); editing = false" >></button> ~
</div> </button>
</td> <button
</tr> class="tiny hollow delete"
</template> x-show="!editing"
title="keshite"
@click="await delete_word_form(wf2.word_form_id, wf2.word_form)">
×
</button>
<button
class="tiny edit"
x-show="editing"
title="jame"
@click="editing = false">
/
</button>
<button
class="tiny"
x-show="editing"
title="antaa"
@click="await put_word_form_text(wf2.word_form_id, edited_text); editing = false">
>
</button>
</div>
</td>
</tr>
</template>
</tbody> </tbody>
<!-- Add new wf --> <!-- Add new wf -->
<tfoot> <tfoot>
<tr x-data="{ <tr
x-data="{
adding_word_form:false, adding_word_form:false,
new_word_form_text:'', new_word_form_text:'',
selected_lect:'', selected_lect:'',
adding_lect:false, adding_lect:false,
new_lect_name:'' new_lect_name:''
}"> }">
<td
<td colspan="3" x-show="!adding_word_form"> colspan="3"
<button class="is-fullwidth hollow primary" x-show="!adding_word_form">
@click="adding_word_form = !adding_word_form">+</button> <button
class="is-fullwidth hollow primary"
@click="adding_word_form = !adding_word_form">
+
</button>
</td> </td>
<td colspan="3" x-show="adding_word_form" style="width: 12em;"> <td
<div style="display: flex; flex-direction: row; gap: 2em; justify-content: space-between;"> colspan="3"
x-show="adding_word_form"
style="width: 12em">
<div
style="
display: flex;
flex-direction: row;
gap: 2em;
justify-content: space-between;
">
<div> <div>
<select name="vilgovor" x-model="selected_lect" x-show="!adding_lect"> <select
<option value="" selected disabled hidden>Sentaku...</option> name="vilgovor"
<template x-for="lect in lects"> x-model="selected_lect"
<option :value="lect.name" x-text="lect.name"></option> x-show="!adding_lect">
<option
value=""
selected
disabled
hidden>
Sentaku...
</option>
<template
x-for="lect in lects">
<option
:value="lect.name"
x-text="lect.name"></option>
</template> </template>
</select> </select>
<input type="text" x-show="adding_lect" x-model="new_lect_name"> <input
<button class="float-right tiny primary" x-show="!adding_lect" @click="adding_lect = true" title="neo govor">+</button> type="text"
<button class="float-right tiny edit" x-show="adding_lect" title="jame" @click="adding_lect = false">/</button> x-show="adding_lect"
x-model="new_lect_name" />
<button
class="float-right tiny primary"
x-show="!adding_lect"
@click="adding_lect = true"
title="neo govor">
+
</button>
<button
class="float-right tiny edit"
x-show="adding_lect"
title="jame"
@click="adding_lect = false">
/
</button>
</div> </div>
<div> <div>
<input type="text" x-model="new_word_form_text"> <input
type="text"
x-model="new_word_form_text" />
</div> </div>
<div class="float-right"> <div class="float-right">
<button class="tiny edit" x-show="adding_word_form" title="jame" @click="adding_word_form = false">/</button> <button
<button class="tiny" x-show="adding_word_form" title="antaa" class="tiny edit"
@click="await post_new_word_form(item.lemma_name, adding_lect ? new_lect_name : selected_lect, new_word_form_text); adding_word_form = false">></button> x-show="adding_word_form"
title="jame"
@click="adding_word_form = false">
/
</button>
<button
class="tiny"
x-show="adding_word_form"
title="antaa"
:disabled="!(selected_lect || new_lect_name)"
@click="await post_new_word_form(item.lemma_name, adding_lect ? new_lect_name : selected_lect, new_word_form_text); adding_word_form = false">
>
</button>
</div> </div>
</div> </div>
</td> </td>
</tr> </tr>
</tfoot> </tfoot>
@ -290,23 +385,51 @@
<h4>Imi</h4> <h4>Imi</h4>
<ul> <ul>
<template x-for="def in (lemma_detail?.definitions ?? [])"> <template
x-for="def in (lemma_detail?.definitions ?? [])">
<li <li
x-data="{ x-data="{
editing: false, editing: false,
edited_text: def.definition_text.toString() edited_text: def.definition_text.toString()
}" @click.outside="editing = false"> }"
<span x-show="!editing" x-html="md(def.definition_text)" @click="editing = true"></span> @click.outside="editing = false">
<input type="text" x-show="editing" x-model="edited_text"> <span
x-show="!editing"
x-html="md(def.definition_text)"
@click="editing = true"></span>
<input
type="text"
x-show="editing"
x-model="edited_text" />
<div class="float-right"> <div class="float-right">
<button class="tiny hollow primary" title="kawari" x-show="!editing" <button
@click="editing = true">~</button> class="tiny hollow primary"
<button class="tiny edit" title="jame" x-show="editing" title="kawari"
@click="editing = false">/</button> x-show="!editing"
<button class="tiny primary" title="antaa" x-show="editing" @click="editing = true">
@click="await put_definition(def.definition_id, edited_text); editing = false">></button> ~
<button class="tiny hollow delete" x-show="!editing" title="keshite" </button>
@click="await delete_definition(def.definition_id, def.definition_text)" >x</button> <button
class="tiny edit"
title="jame"
x-show="editing"
@click="editing = false">
/
</button>
<button
class="tiny primary"
title="antaa"
x-show="editing"
@click="await put_definition(def.definition_id, edited_text); editing = false">
>
</button>
<button
class="tiny hollow delete"
x-show="!editing"
title="keshite"
@click="await delete_definition(def.definition_id, def.definition_text)">
×
</button>
</div> </div>
</li> </li>
</template> </template>
@ -334,7 +457,7 @@
Popoczta Popoczta
</button> </button>
</div> </div>
<button <button
class="is-fullwidth hollow primary" class="is-fullwidth hollow primary"
x-show="!adding_definition" x-show="!adding_definition"
@ -344,23 +467,52 @@
<h4>Tatoeba</h4> <h4>Tatoeba</h4>
<ul> <ul>
<template x-for="ex in (lemma_detail?.examples ?? [])"> <template
<li x-data="{ x-for="ex in (lemma_detail?.examples ?? [])">
<li
x-data="{
editing:false, editing:false,
edited_text: ex.example_text.toString() edited_text: ex.example_text.toString()
}" }"
@click.outside="editing = false"> @click.outside="editing = false">
<span x-show="!editing" x-html="md(ex.example_text)" @click="editing = true"></span> <span
<input type="text" x-show="editing" x-model="edited_text"> x-show="!editing"
x-html="md(ex.example_text)"
@click="editing = true"></span>
<input
type="text"
x-show="editing"
x-model="edited_text" />
<div class="float-right"> <div class="float-right">
<button class="tiny hollow primary" x-show="!editing" <button
@click="editing = true" title="kawari">~</button> class="tiny hollow primary"
<button class="tiny edit" x-show="editing" x-show="!editing"
@click="editing = false" title="jame">/</button> @click="editing = true"
<button class="tiny" x-show="editing" type="button" title="kawari">
@click="await put_example(ex.example_id, edited_text); editing = false" title="antaa">></button> ~
<button class="tiny hollow delete" x-show="!editing" title="keshite" </button>
@click="await delete_example(ex.example_id, ex.example_text)">x</button> <button
class="tiny edit"
x-show="editing"
@click="editing = false"
title="jame">
/
</button>
<button
class="tiny"
x-show="editing"
type="button"
@click="await put_example(ex.example_id, edited_text); editing = false"
title="antaa">
>
</button>
<button
class="tiny hollow delete"
x-show="!editing"
title="keshite"
@click="await delete_example(ex.example_id, ex.example_text)">
×
</button>
</div> </div>
</li> </li>
</template> </template>
@ -397,6 +549,89 @@
</template> </template>
</div> </div>
</main> </main>
<div
x-show="new_lemma_form_open"
class="modal"
x-data="{
adding_word_form:false,
adding_lect:false,
new_word_form_text:'',
selected_lect:'',
new_lect_name:''
}"
@click.self="new_lemma_form_open = false;">
<div class="card">
<h3>Neo kotoba...</h3>
<div class="flex-row">
<div>
<label>
<div>Viljena govor</div>
<div>
<select
name="vilgovor"
x-model="selected_lect"
x-show="!adding_lect">
<option
value=""
selected
disabled
hidden>
Sentaku...
</option>
<template x-for="lect in lects">
<option
:value="lect.name"
x-text="lect.name"></option>
</template>
</select>
<input
type="text"
x-show="adding_lect"
x-model="new_lect_name" />
<button
class="float-right tiny primary"
x-show="!adding_lect"
@click="adding_lect = true"
title="neo govor">
+
</button>
<button
class="float-right tiny edit"
x-show="adding_lect"
title="jame"
@click="adding_lect = false">
/
</button>
</div>
</label>
</div>
<label>
<div>Kakufal</div>
<div>
<input
type="text"
x-model="new_word_form_text" />
</div>
</label>
</div>
<div style="margin-top: var(--space-sm)">
<button
class="edit"
@click="new_lemma_form_open = false">
/
</button>
<button
type="button"
@click="post_lemma(new_lect_name?new_lect_name:selected_lect, new_word_form_text)
.then(()=>{
new_lemma_form_open = false;
})">
Popoczta
</button>
</div>
</div>
</div>
</div> </div>
</body> </body>
</html> </html>

View file

@ -102,6 +102,49 @@ tfoot {
margin: var(--space-xs); margin: var(--space-xs);
} }
.relative {
position: relative;
}
.fixed {
position: fixed;
}
.absolute {
position: absolute;
}
.sticky {
position: sticky;
top: 0;
}
.top {
z-index: 1;
}
.modal {
z-index: 2;
position: absolute;
background-color: #000000aa;
height: 100%;
width: 100%;
}
.modal .card {
max-width: 480px;
margin: auto;
margin-top: 4rem;
}
.top-bar {
padding: var(--space-sm);
border: 1px solid var(--color-text-secondary);
border-radius: var(--radius-lg);
background-color: var(--color-card-bg);
}
button.tiny { button.tiny {
min-width: 2em; min-width: 2em;
max-width: 2em; max-width: 2em;
@ -111,8 +154,10 @@ button.tiny {
} }
button:disabled{ button:disabled{
background-color: #555; background-color: #aaa;
color: #fff; color: #fff;
border-color: #999;
cursor: not-allowed;
} }
.flex-row { .flex-row {
@ -130,7 +175,7 @@ button:disabled{
} }
.edit:hover { .edit:hover {
background-color: #bb0; background-color: rgb(187, 159, 0);
} }
.delete { .delete {
@ -262,10 +307,15 @@ button:disabled{
} }
/* Search Bar */ /* Search Bar */
.search-bar {
margin: var(--space-sm);
}
.search-bar input { .search-bar input {
display: inline-block; display: inline-block;
width: 80%; width: 16rem;
padding: var(--space-sm) var(--space-md); padding: var(--space-xs) var(--space-md);
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
font-size: 1rem; font-size: 1rem;

View file

@ -27,18 +27,14 @@ function initExpress() {
const app = express(); const app = express();
const PORT = 1225; const PORT = 1225;
const lect_repository = appDataSource.getRepository(Lect);
const word_form_repository = appDataSource.getRepository(WordForm);
const lemma_repository = appDataSource.getRepository(Lemma);
app.use(cors(), express.json()); app.use(cors(), express.json());
app.get("/sample", (_req, res) => { app.get("/sample", (_req, res) => {
res.status(200).send(SAMPLE); res.status(200).send(SAMPLE);
}); });
app.get("/search", async (req, res) => { app.get("/search", async (_req, res) => {
const search_term = req.query.search_term?.toString(); const search_term = _req.query.search_term?.toString();
let word_forms: WordForm[]; let word_forms: WordForm[];
let lemmas: Lemma[]; let lemmas: Lemma[];
@ -64,8 +60,8 @@ function initExpress() {
res.status(200).send({ terms: lemmas.length, results: lemmas }); res.status(200).send({ terms: lemmas.length, results: lemmas });
}); });
app.get("/lect", async (req, res) => { app.get("/lect", async (_req, res) => {
const name = req.query.name?.toString(); const name = _req.query.name?.toString();
if (!name) { if (!name) {
return void res.sendStatus(400); return void res.sendStatus(400);
@ -79,8 +75,8 @@ function initExpress() {
res.status(200).send({ lect }); res.status(200).send({ lect });
}); });
app.post("/lect", (req, res) => { app.post("/lect", (_req, res) => {
const lect_name = req.query.lect_name?.toString(); const lect_name = _req.query.lect_name?.toString();
if (!lect_name) { if (!lect_name) {
return void res.sendStatus(400); return void res.sendStatus(400);
@ -120,7 +116,74 @@ function initExpress() {
res.status(200).send({ lemma_detail }); res.status(200).send({ lemma_detail });
}); });
/* Definitions */ app.post("/lemma", async (_req, res) => {
const lect_name = _req.body.lect_name ?? null;
const word_form_text = _req.body.word_form_text?.toString();
if(!word_form_text || /^\s*$/.test(word_form_text) || !lect_name){
console.error(`Error: ${JSON.stringify(_req.body)}`);
return void res.status(400).send();
}
let word_form = new WordForm();
let lemma = new Lemma();
let lect;
try{
lect = await Lect.findOne({where:{name:lect_name}});
} catch {
console.info(`Lect ${lect_name} not found.`);
}
if(!lect) {
lect = new Lect();
lect.name = lect_name;
await lect
.save()
.then((l)=>{console.info(`Created lect: ${JSON.stringify(l)}`)});
}
lemma.lemma_name = word_form_text;
word_form.word_form = word_form_text;
word_form.lemma = lemma;
word_form.lect = lect;
lemma.word_forms = [word_form];
Lemma.save(lemma);
let lemma_detail = await Lemma.findOne({
where: {
lemma_name: word_form.lemma.lemma_name
},
relations: {
word_forms: {
lect: true
},
examples: true,
definitions: true,
media: true,
parts_of_speech: true
}
});
res.status(200).send({lemma_detail});
});
app.get("/definition/:definition_id", async (_req, res) =>{
const definition_id:number = parseInt(_req.params.definition_id);
if(!definition_id){
console.error(`Error: Could not find word form ${JSON.stringify({definition_id})}`);
return void res.status(400).send();
}
let definition = await Definition.findOne({where:{definition_id}}).then();
res.status(200).send({definition});
});
app.put("/definition", async (_req, res) => { app.put("/definition", async (_req, res) => {
const definition_text = _req.body.definition_text?.toString(); const definition_text = _req.body.definition_text?.toString();
@ -171,7 +234,7 @@ function initExpress() {
const definition_id:number = parseInt(_req.params.definition_id); const definition_id:number = parseInt(_req.params.definition_id);
if(!definition_id){ if(!definition_id){
console.error(`Error: Could not find word form ${JSON.stringify({definition_id:definition_id})}`); console.error(`Error: Could not find word form ${JSON.stringify({definition_id})}`);
return void res.status(400).send(); return void res.status(400).send();
} }
@ -182,6 +245,19 @@ function initExpress() {
/* examples */ /* examples */
app.get("/example/:example_id", async (_req, res) =>{
const example_id:number = parseInt(_req.params.example_id);
if(!example_id){
console.error(`Error: Could not find word form ${JSON.stringify({example_id})}`);
return void res.status(400).send();
}
let example = await Example.findOne({where:{example_id}}).then();
res.status(200).send({definition: example});
});
app.put("/example", async (_req, res) => { app.put("/example", async (_req, res) => {
const example_text = _req.body.example_text?.toString(); const example_text = _req.body.example_text?.toString();
const example_id = _req.body.example_id ?? null; const example_id = _req.body.example_id ?? null;