Skip to content

Commit 2c35dcc

Browse files
committed
add basic ui
1 parent fb5628f commit 2c35dcc

File tree

1 file changed

+271
-11
lines changed

1 file changed

+271
-11
lines changed

assets/index.html

Lines changed: 271 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,80 @@
77
body {
88
font-family: Arial, sans-serif;
99
margin-top: 50px;
10+
width: 1150px;
11+
margin: auto;
12+
min-height: 100vh;
13+
position: relative;
1014
}
1115

1216
header {
1317
display: flex;
1418
gap: 1em;
1519
}
20+
21+
.translation-section {
22+
display: flex;
23+
justify-content: space-between;
24+
align-content: center;
25+
}
26+
27+
.source, .target {
28+
width: 45%;
29+
display: flex;
30+
flex-direction: column;
31+
gap: 0.5em;
32+
border: 1px solid #ccc;
33+
padding: 1%;
34+
}
35+
36+
.source textarea, .target textarea {
37+
width: 98%;
38+
resize: none;
39+
}
40+
41+
.dropdown-area select {
42+
width: 200px;
43+
}
44+
45+
.swap-button-area {
46+
display: flex;
47+
align-items: center;
48+
justify-content: center;
49+
}
50+
51+
.text-area {
52+
height: 20em;
53+
}
54+
55+
.text-area textarea {
56+
min-height: 60%;
57+
height: 60%;
58+
}
59+
.text-area .transliterated-text {
60+
margin-top: 0.5em;
61+
font-style: italic;
62+
color: #555;
63+
max-height: 35%;
64+
overflow-y: auto;
65+
}
66+
67+
.actions {
68+
display: flex;
69+
justify-content: flex-end;
70+
gap: 2em;
71+
}
72+
73+
.actions button {
74+
height: 32px;
75+
}
76+
77+
footer {
78+
position: absolute;
79+
bottom: 10px;
80+
width: 60%;
81+
text-align: center;
82+
margin: 0 20%;
83+
}
1684
</style>
1785
</head>
1886
<body>
@@ -21,12 +89,92 @@ <h1>Glotter</h1>
2189
<h3 id="version"></h3>
2290
</header>
2391

24-
<section>
25-
<h2>Supported Languges</h2>
26-
<ol id="languages"></ol>
92+
<section class="translation-section">
93+
<div class="source">
94+
<div class="dropdown-area">
95+
<select id="select-source" name="select-source"></select>
96+
</div>
97+
<div class="text-area">
98+
<textarea id="source-text" name="source-text"></textarea>
99+
<div id="source-transliteration" class="transliterated-text"></div>
100+
</div>
101+
<div class="actions">
102+
<button id="paste-button">
103+
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"><path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h167q11-35 43-57.5t70-22.5q40 0 71.5 22.5T594-840h166q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H200Zm0-80h560v-560h-80v80q0 17-11.5 28.5T640-640H320q-17 0-28.5-11.5T280-680v-80h-80v560Zm280-560q17 0 28.5-11.5T520-800q0-17-11.5-28.5T480-840q-17 0-28.5 11.5T440-800q0 17 11.5 28.5T480-760Z"/>
104+
</svg>
105+
</button>
106+
<button id="translate-button">Translate</button>
107+
</div>
108+
</div>
109+
<div class="swap-button-area">
110+
<button id="swap-button">
111+
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"><path d="m233-280 76 76q12 12 11.5 28T308-148q-12 11-28 11.5T252-148L108-292q-6-6-8.5-13T97-320q0-8 2.5-15t8.5-13l144-144q11-11 27.5-11t28.5 11q12 12 12 28.5T308-435l-75 75h567q17 0 28.5 11.5T840-320q0 17-11.5 28.5T800-280H233Zm494-320H160q-17 0-28.5-11.5T120-640q0-17 11.5-28.5T160-680h567l-76-76q-12-12-11.5-28t12.5-28q12-11 28-11.5t28 11.5l144 144q6 6 8.5 13t2.5 15q0 8-2.5 15t-8.5 13L708-468q-11 11-27.5 11T652-468q-12-12-12-28.5t12-28.5l75-75Z"/>
112+
</svg>
113+
</button>
114+
</div>
115+
<div class="target">
116+
<div class="dropdown-area">
117+
<select id="select-target" name="select-target"></select>
118+
</div>
119+
<div class="text-area">
120+
<textarea id="target-text" name="target-text" disabled></textarea>
121+
<div id="target-transliteration" class="transliterated-text"></div>
122+
</div>
123+
<div class="actions">
124+
<button id="copy-button">
125+
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"><path d="M360-240q-33 0-56.5-23.5T280-320v-480q0-33 23.5-56.5T360-880h360q33 0 56.5 23.5T800-800v480q0 33-23.5 56.5T720-240H360Zm0-80h360v-480H360v480ZM200-80q-33 0-56.5-23.5T120-160v-520q0-17 11.5-28.5T160-720q17 0 28.5 11.5T200-680v520h400q17 0 28.5 11.5T640-120q0 17-11.5 28.5T600-80H200Zm160-240v-480 480Z"/>
126+
</svg>
127+
</button>
128+
</div>
129+
</div>
27130
</section>
131+
132+
<footer>
133+
<a href="https://github.com/terslanf/glotter">Glotter</a> by Ters. Licenced under <a href="https://www.gnu.org/licenses/agpl-3.0.en.html">AGPL 3.0</a>
134+
<br />
135+
<a href="/source-code">Click here</a> to get the source code.
136+
</footer>
28137

29138
<script>
139+
const nonLatinLangs = ["ar", "be", "bg", "bn", "el", "fa", "gu", "he", "hi", "ja", "kn", "ko", "ml", "mt", "ru", "sr", "ta", "te", "uk", "zh"];
140+
const debounce = (callback, delay) => {
141+
let timeout;
142+
return (...args) => {
143+
clearTimeout(timeout);
144+
timeout = setTimeout(() => {
145+
callback.apply(this, args);
146+
}, delay);
147+
};
148+
};
149+
150+
const transliterateText = async (text, lang) => {
151+
if (!nonLatinLangs.includes(lang)) {
152+
return '';
153+
}
154+
155+
try {
156+
const res = await fetch("/transliterate", {
157+
method: "POST",
158+
headers: {
159+
"Content-Type": "application/json"
160+
},
161+
body: JSON.stringify({
162+
lang: lang,
163+
text: text
164+
})
165+
});
166+
if (!res.ok) {
167+
throw new Error("Error during transliteration");
168+
}
169+
170+
const data = await res.json();
171+
return data.transliteration;
172+
} catch (err) {
173+
window.alert("Transliteration failed");
174+
return null;
175+
}
176+
};
177+
30178
window.addEventListener("load", async () => {
31179
fetch("/version")
32180
.then(async (res) => {
@@ -48,20 +196,132 @@ <h2>Supported Languges</h2>
48196
}
49197

50198
const languages = await res.json();
51-
const languagesList = document.getElementById('languages');
52-
const listFragment = document.createDocumentFragment();
53-
languages.forEach((lang) => {
54-
const listItem = document.createElement('li');
55-
listItem.innerText = lang.code + ' - ' + lang.name;
56-
listFragment.appendChild(listItem);
199+
const sourceLanguagesList = document.getElementById('select-source');
200+
const targetLanguagesList = document.getElementById('select-target');
201+
const sourceOptionsFragment = document.createDocumentFragment();
202+
const targetOptionsFragment = document.createDocumentFragment();
203+
languages.sort((a, b) => a.name.localeCompare(b.name)).forEach((lang) => {
204+
const sourceOptionItem = document.createElement('option');
205+
sourceOptionItem.value = lang.code;
206+
sourceOptionItem.innerText = lang.name;
207+
sourceOptionsFragment.appendChild(sourceOptionItem);
208+
209+
const targetOptionItem = document.createElement('option');
210+
targetOptionItem.value = lang.code;
211+
targetOptionItem.innerText = lang.name;
212+
targetOptionsFragment.appendChild(targetOptionItem);
57213
});
58-
languagesList.appendChild(listFragment);
214+
sourceLanguagesList.appendChild(sourceOptionsFragment);
215+
targetLanguagesList.appendChild(targetOptionsFragment);
216+
217+
// Set default selections
218+
sourceLanguagesList.value = 'en';
219+
targetLanguagesList.value = 'de';
59220
})
60221
.catch((err) => {
61222
window.alert("Could not fetch supported languages");
62223
});
63224
});
225+
226+
const swapButton = document.getElementById('swap-button');
227+
swapButton.addEventListener("click", () => {
228+
const sourceSelect = document.getElementById('select-source');
229+
const targetSelect = document.getElementById('select-target');
230+
const targetTransliterationDiv = document.getElementById('target-transliteration');
231+
const tempValue = sourceSelect.value;
232+
sourceSelect.value = targetSelect.value;
233+
targetSelect.value = tempValue;
234+
235+
const sourceTextArea = document.getElementById('source-text');
236+
const targetTextArea = document.getElementById('target-text');
237+
sourceTextArea.value = targetTextArea.value;
238+
targetTextArea.value = "";
239+
targetTransliterationDiv.innerText = "";
240+
241+
transliterateText(sourceTextArea.value, sourceSelect.value)
242+
.then((transliteration) => {
243+
const transliterationDiv = document.getElementById('source-transliteration');
244+
if (transliteration) {
245+
transliterationDiv.innerText = transliteration;
246+
} else {
247+
transliterationDiv.innerText = '';
248+
}
249+
});
250+
});
251+
252+
const pasteButton = document.getElementById('paste-button');
253+
pasteButton.addEventListener("click", async () => {
254+
const sourceTextArea = document.getElementById('source-text');
255+
try {
256+
const text = await navigator.clipboard.readText();
257+
sourceTextArea.value = text;
258+
} catch (err) {
259+
window.alert("Could not read from clipboard");
260+
}
261+
});
262+
263+
const copyButton = document.getElementById('copy-button');
264+
copyButton.addEventListener("click", async () => {
265+
const targetTextArea = document.getElementById('target-text');
266+
try {
267+
await navigator.clipboard.writeText(targetTextArea.value);
268+
} catch (err) {
269+
window.alert("Could not write to clipboard");
270+
}
271+
});
272+
273+
const translateButton = document.getElementById('translate-button');
274+
translateButton.addEventListener("click", () => {
275+
const sourceText = document.getElementById('source-text').value;
276+
const sourceLang = document.getElementById('select-source').value;
277+
const targetLang = document.getElementById('select-target').value;
278+
fetch("/translate", {
279+
method: "POST",
280+
headers: {
281+
"Content-Type": "application/json"
282+
},
283+
body: JSON.stringify({
284+
from: sourceLang,
285+
to: targetLang,
286+
text: sourceText
287+
})
288+
})
289+
.then(async (res) => {
290+
if (!res.ok) {
291+
throw new Error("Error during translation");
292+
}
293+
294+
const data = await res.json();
295+
document.getElementById('target-text').value = data.translation;
296+
297+
transliterateText(data.translation, targetLang)
298+
.then((transliteration) => {
299+
const transliterationDiv = document.getElementById('target-transliteration');
300+
if (transliteration) {
301+
transliterationDiv.innerText = transliteration;
302+
} else {
303+
transliterationDiv.innerText = '';
304+
}
305+
});
306+
})
307+
.catch((err) => {
308+
window.alert("Translation failed");
309+
});
310+
});
311+
312+
const sourceTextArea = document.getElementById('source-text');
313+
sourceTextArea.addEventListener("input", debounce(() => {
314+
const sourceLang = document.getElementById('select-source').value;
315+
transliterateText(sourceTextArea.value, sourceLang)
316+
.then((transliteration) => {
317+
const transliterationDiv = document.getElementById('source-transliteration');
318+
if (transliteration) {
319+
transliterationDiv.innerText = transliteration;
320+
} else {
321+
transliterationDiv.innerText = '';
322+
}
323+
});
324+
}, 300));
64325
</script>
65326
</body>
66327
</html>
67-

0 commit comments

Comments
 (0)