commit f5b4556120e93542ed5a4f8f8b8bf5f0f84b7a1c Author: Kailash Nadh Date: Sun Nov 30 14:11:42 2025 +0530 First commit. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1f937da --- /dev/null +++ b/LICENSE @@ -0,0 +1,7 @@ +Copyright 2019, Kailash Nadh + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..572f78d --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +# dictmaker-alar +This repository is the [dictmaker](https://dict.press) site theme for the Kannada-English dictionary site [Alar.ink](https://alar.ink) + +This can be cloned and used as a theme for building dictionary site themes for dictpress. + +Licensed under the MIT License. \ No newline at end of file diff --git a/base.html b/base.html new file mode 100644 index 0000000..b77d5fd --- /dev/null +++ b/base.html @@ -0,0 +1,110 @@ +{{- define "header" -}} + + + + + + + {{- block "meta" . -}} + + {{- if eq .Data.PageType "/" }} {{- .L.T "global.siteName" -}} + {{- else if eq .Data.PageType "glossary" }}{{- .L.T "public.glossaryTitle" -}} + {{- else if eq .Data.PageType "search" }}{{- .L.Ts "public.searchTitle" "query" .Data.Query.Query -}} + {{- else if ne .Data.Title "" }}{{ .Data.Title }} + {{- end -}} + + + {{- end -}} + + + + + + + + +
+
+
+
+ + + +
+
+{{ end}} + +{{ define "footer" }} + +
+ + +
+ +
+
+

{{ .L.T "public.submitTitle" }}

+ +

+ + +

+
+
+ + + + + +{{ end }} diff --git a/glossary-words.html b/glossary-words.html new file mode 100644 index 0000000..2a15a4f --- /dev/null +++ b/glossary-words.html @@ -0,0 +1,13 @@ +{{ define "glossary-words" }} +{{ $g := .Data.Glossary }} +{{ if not $g.Words }} +

{{ $.L.T "public.noResultsTitle" }}

+

{{ $.L.T "public.noResults" }}

+{{ else }} + +{{ end }} +{{ end }} \ No newline at end of file diff --git a/glossary.html b/glossary.html new file mode 100644 index 0000000..58d9526 --- /dev/null +++ b/glossary.html @@ -0,0 +1,22 @@ +{{ define "glossary" }} +{{ template "header" . }} + +
+ {{ if not .Data.Initials }} +

{{ .L.T "public.glossaryTitle" }}

+

{{ .L.T "public.noResults" }}

+ {{ else }} + + + + {{ template "glossary-words" . }} + + {{ end}} +
+ +{{ template "footer" . }} +{{ end }} \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..8454013 --- /dev/null +++ b/index.html @@ -0,0 +1,4 @@ +{{ define "index" }} +{{ template "header" . }} +{{ template "footer" . }} +{{ end }} \ No newline at end of file diff --git a/lang.json b/lang.json new file mode 100644 index 0000000..f477cb4 --- /dev/null +++ b/lang.json @@ -0,0 +1,36 @@ +{ + "_.code": "en", + "_.name": "English", + "global.btnClose": "Close", + "global.btnDelete": "Delete", + "global.btnSearch": "Search", + "global.siteName": "Dictionary", + "public.errorMessage": "An error occurred. Please try later.", + "public.errorTitle": "Error", + "public.about": "About", + "public.glossary": "{lang} glossary", + "public.glossaryTitle": "Glossary of words", + "public.mainTitle": "Dictionary website", + "public.noResults": "No results where found for the query.", + "public.noResultsTitle": "No results", + "public.searchTitle": "{query} English meaning - Alar", + "public.similarTitle": "Similar words", + "public.playAudio": "Play audio", + "public.subTitle": "Kannada-English dictionary", + "public.submitEntry": "Suggest new entry", + "public.submitEntryTitle": "Suggest new entry", + "public.submitTitle": "Comments and suggestions", + "public.suggestAddDefBtn": "Add another definition", + "public.suggestContent": "Content", + "public.suggestDefLang": "Definition language", + "public.suggestDefsTitle": "Definitions", + "public.suggestEdit": "Suggest edit for \"{word}\"", + "public.suggestEntryLang": "Entry language", + "public.suggestPhones": "Phonetic notations (pronunciation)", + "public.suggestPhonesPlaceholder": "eg: pɛts, pɛt (commma separated)", + "public.suggestSubmitBtn": "Submit for review", + "public.suggestSubmitted": "Submitted for review successfully", + "public.suggestTitle": "Suggest a new entry", + "public.viewMore": "View {num} more", + "public.sourceTag": "Source of the entry" +} \ No newline at end of file diff --git a/message.html b/message.html new file mode 100644 index 0000000..b570716 --- /dev/null +++ b/message.html @@ -0,0 +1,10 @@ +{{ define "message" }} +{{ template "header" . }} + +
+

{{ .Data.Title }}

+

{{ .Data.Description }}

+
+ +{{ template "footer" . }} +{{ end }} \ No newline at end of file diff --git a/pages/about.html b/pages/about.html new file mode 100644 index 0000000..737f360 --- /dev/null +++ b/pages/about.html @@ -0,0 +1,73 @@ +{{ define "meta" }} + About V. Krishna's Alar (Kannada-English dictionary) + +{{ end }} + +{{ define "page-about" }} + +{{ template "header" . }} +
+

About

+

+ Alar is an authoritative Kannada-English dictionary corpus + created by V. Krishna. It contains over 150,000 Kannada words + with over 240,000 English definitions. It is released + as an open data corpus licensed under + the Open Database License (ODC-ODbL). +

+
+

V. Krishna

+

+ Photo of V. Krishna + V. Krishna started building his Kannada-English dictionary in the 1970s + as a hobby project. This incredible endeavour spanning four decades + has now evolved into an invaluable contribution to Kannada language. + In addition to authoring the colossal dictionary, he single-handedly digitised + his original manuscripts. In 2019, he open sourced the entire dictionary. + Read the full story here. +

+

+ He is a resident of Bengaluru and spends his time working on his dictionary + and other Kannada literature projects. He has recently started laying + foundations for a new English-Kannada dictionary. He can be reached at + vkrishna1411@yahoo.co.in. +

+ +

+ +
+ +

Data

+

+ The corpus is available on the Alar repository. + It is licensed under ODC-ODbL. +

+ +

Website

+

+ This website is published using dictpress, + and uses knphone, a Kannada phonetic + indexing algorithm, for search. Search suggestions and transliterations are powered by Varnam. + The website's source is available here. +

+

+ Technical feedback can be sent to kailash@nadh.in. +

+ +
+ +

Zerodha

+

In 2019, Zerodha collaborated with + V. Krishna to open source and publish his dictionary online and + awarded him a grant to support his work. +

+ +
+ +

Indic Archive

+

Since 2023, Alar is a project under Indic Digital Archive Foundation.

+
+{{ template "footer" . }} +{{ end }} diff --git a/results.html b/results.html new file mode 100644 index 0000000..2eec57a --- /dev/null +++ b/results.html @@ -0,0 +1,97 @@ +{{ define "results" }} +{{ $maxContentItems := .Consts.SiteMaxEntryContentItems }} +{{ $numResults := (min (len .Data.Results.Entries) 10) }} +
+
+
    + {{- range $k, $r := (mustSlice .Data.Results.Entries 0 $numResults) -}} +
  1. +
    +
    +
    +

    {{ $r.Content | join ", " }}

    + + {{- if $r.Meta.audio -}} + {{ $.L.T + {{- end -}} + + {{- if $.Consts.EnableSubmissions -}} + ✏️ + {{- end -}} +
    + + {{- if $r.Phones -}} + ♪ {{ $r.Phones | join "," }} + {{- end -}} + +
    + +
    + {{- if $r.Tags -}} + + {{- range $tag := $r.Tags -}} + {{ $tag }} + {{- end -}} + + {{- end -}} + Share screenshot +
    +
    + + {{- if $r.Relations -}} + {{- $lastType := "" -}} + {{- range $k, $d := $r.Relations -}} + {{- $types := ($d.RelationTypes | join ", ") -}} + + {{- if ne $lastType $types -}} + {{- if $lastType -}}
{{- end }} +
    + {{ if $d.RelationTypes }} +
  1. + {{- range $t := $d.RelationTypes -}} + + {{- $dType := index (index $.Langs $r.Lang).Types $t }} + {{- if $dType }}{{ $dType }} {{ end -}} + {{- $rType := index (index $.Langs $d.Lang).Types $t }} + {{- if $rType }}({{ $rType }}){{ end -}} + + {{- end -}} +
  2. + {{ end }} + {{- end -}} + +
  3. + {{ $d.Content | join ", " }} + {{ if or (gt $d.ContentLength $maxContentItems) $d.Meta.synonyms -}} + + {{- end -}} + {{- if $.Consts.EnableSubmissions }} + ✏️ + {{ end -}} + + + + +
  4. + {{ $lastType = $types }} + {{- end -}} +
+ {{ end }} + + + {{- end -}} + +
+ +
+{{ end }} diff --git a/search.html b/search.html new file mode 100644 index 0000000..ebc3147 --- /dev/null +++ b/search.html @@ -0,0 +1,16 @@ +{{ define "search" }} +{{ template "header" . }} + +
+ {{ if not .Data.Results.Entries }} +

{{ .L.T "public.noResultsTitle" }}

+

+ {{ .L.T "public.noResults" }} +

+ {{ else }} + {{ template "results" . }} + {{ end }} +
+ +{{ template "footer" . }} +{{ end }} diff --git a/static/alar.js b/static/alar.js new file mode 100644 index 0000000..55f8c92 --- /dev/null +++ b/static/alar.js @@ -0,0 +1,55 @@ +function hasKannadaChar(str) { + return /[\u0C80-\u0CFF]/.test(str); +} + +(function () { + const reMatchNonKannadaBlobs = new RegExp(/[^\u0C80-\u0CFF]+/g); + // In the results (definitions), if there are Kannada words, hyperlink + // them to search. + let defs = document.querySelectorAll(".defs li"); + if (!defs || defs.length === 0) { + return; + } + + for (let i = 0; i < defs.length; i++) { + // Go through each definition. Ignore the ASCII ones. + if (!hasKannadaChar(defs[i].innerText)) { + continue; + } + + // Split the word and iterate through it, turning non-ASCII words into + // hyperlinks; + const parts = defs[i].innerText.split(" "); + const s = document.createElement("span"); + + parts.forEach((v) => { + if (!hasKannadaChar(v)) { + // ASCII word. Append the text as-is. + s.appendChild(document.createTextNode(v)); + } else { + // Non-ASCII word. Turn into a link. + const a = document.createElement("a"); + + // Some Kannada words have non-Kannada characters around them, + // the hyperlink should be formed only for Kannada words, while retaining all the characters order + const kannadaWord = v.replace(reMatchNonKannadaBlobs, ""); + const nonKannadaWords = v.split(kannadaWord); + + nonKannadaWords[0] && s.appendChild(document.createTextNode(nonKannadaWords[0])); + + a.setAttribute("href", kannadaWord); + a.appendChild(document.createTextNode(kannadaWord)); + s.appendChild(a); + + nonKannadaWords[1] && s.appendChild(document.createTextNode(nonKannadaWords[1])); + } + + // Append a space. + s.appendChild(document.createTextNode(" ")); + }); + + defs[i].innerHTML = ""; + defs[i].appendChild(s); + } + +})(); \ No newline at end of file diff --git a/static/alar.svg b/static/alar.svg new file mode 100644 index 0000000..9814e93 --- /dev/null +++ b/static/alar.svg @@ -0,0 +1,130 @@ + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + Alar - V Krishna's Kannada - English dictionary + ಶ್ರೀ . ವಿ ಕ್ರಿಷ್ಣ ರವರ ಕನ್ನಡ - ಇಂಗ್ಲಿಷ್ ನಿಘಂಟು + ಅಲರ್ + + diff --git a/static/audio.svg b/static/audio.svg new file mode 100644 index 0000000..495ee0d --- /dev/null +++ b/static/audio.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/autocomp.js b/static/autocomp.js new file mode 100644 index 0000000..a4ba4f7 --- /dev/null +++ b/static/autocomp.js @@ -0,0 +1,137 @@ +function autocomp(el, options = {}) { + const opt = { + onQuery: null, onNavigate: null, onSelect: null, onRender: null, debounce: 100, autoSelect: true,...options + }; + + let box, cur = opt.autoSelect ? 0 : -1, items = [], val, req; + + // Disable browser's default autocomplete behaviour on the input. + el.autocomplete = "off"; + + // Attach all the events required for the interactions in one go. + ["input", "keydown", "blur"].forEach(k => el.addEventListener(k, handleEvent)); + + function handleEvent(e) { + if (e.type === "keydown" && !handleKeydown(e)) { + return; + } + + if (e.type === "blur") { + return destroy(); + } + + const newVal = e.target.value; + if (!newVal) { + destroy(); + val = null; + return; + } + + if (newVal === val && box) { + return; + } + val = newVal; + + // Clear (debounce) any existing pending requests and queue + // the next search request. + clearTimeout(req); + req = setTimeout(query, opt.debounce); + } + + function handleKeydown(e) { + if (!box) { + return e.keyCode === 38 || e.keyCode === 40 + } + + switch (e.keyCode) { + case 38: return navigate(-1, e); // Up arrow. + case 40: return navigate(1, e); // Down arrow + case 9: // Tab + case 13: // Enter + e.preventDefault(); + select(cur); + destroy(); + return; + case 27: // Escape. + destroy(); + return; + } + } + + async function query() { + if (!val) { + return; + } + + items = await opt.onQuery(val); + if (!items.length) { + return destroy(); + } + + if (!box) { + createBox(); + } + + renderResults(); + } + + function createBox() { + box = document.createElement("div"); + Object.assign(box.style, { + width: window.getComputedStyle(el).width, + position: "absolute", + left: `${el.offsetLeft}px`, + top: `${el.offsetTop + el.offsetHeight}px` + }); + + box.classList.add("autocomp"); + el.parentNode.insertBefore(box, el.nextSibling); + } + + function renderResults() { + box.innerHTML = ""; + items.forEach((item, idx) => { + const div = document.createElement("div"); + div.classList.add("autocomp-item"); + + // If there's a custom renderer callback, use it, else, simply insert the value/text as-is. + opt.onRender ? div.appendChild(opt.onRender(item)) : div.innerText = item; + if (idx === cur) { + div.classList.add("autocomp-sel"); + } + + div.addEventListener("mousedown", () => select(idx)); + box.appendChild(div); + }); + } + + function navigate(direction, e) { + e.preventDefault(); + + // Remove the previous item's highlight; + const prev = box.querySelector(`:nth-child(${cur + 1})`); + prev?.classList.remove("autocomp-sel"); + + // Increment the cursor and highlight the next item, cycled between [0, n]. + cur = (cur + direction + items.length) % items.length; + box.querySelector(`:nth-child(${cur + 1})`).classList.add("autocomp-sel"); + } + + function select(idx) { + if (!opt.onSelect) { + return; + } + + val = opt.onSelect(items[idx]); + el.value = val || items[idx]; + } + + function destroy() { + items = []; + cur = opt.autoSelect ? 0 : -1; + if (box) { + box.remove(); + box = null; + } + } +} diff --git a/static/down.svg b/static/down.svg new file mode 100644 index 0000000..580bcde --- /dev/null +++ b/static/down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/export.svg b/static/export.svg new file mode 100644 index 0000000..05edf4a --- /dev/null +++ b/static/export.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/static/favicon.png b/static/favicon.png new file mode 100644 index 0000000..cd5f1d7 Binary files /dev/null and b/static/favicon.png differ diff --git a/static/flexit.css b/static/flexit.css new file mode 100644 index 0000000..54f7029 --- /dev/null +++ b/static/flexit.css @@ -0,0 +1,135 @@ +.container { + position: relative; + width: 100%; + max-width: 960px; + margin: 0 auto; + box-sizing: border-box; +} +.row { + box-sizing: border-box; + display: flex; + flex: 0 1 auto; + flex-flow: row wrap; +} +.row.nogutter { + margin-left: 0; + margin-right: 0; +} +.row.nogutter > .columns { + padding-left: 0; + padding-right: 0; +} +.columns { + box-sizing: border-box; + flex: 0 1 auto; + min-width: 0; +} +.one { + flex-basis: 8.33333333%; +} +.two { + flex-basis: 16.66666667%; +} +.three { + flex-basis: 25%; +} +.four { + flex-basis: 33.3333333333%; +} +.five { + flex-basis: 41.66666667%; +} +.six { + flex-basis: 50%; +} +.seven { + flex-basis: 58.33333333%; +} +.eight { + flex-basis: 66.66666667%; +} +.nine { + flex-basis: 75%; +} +.ten { + flex-basis: 83.33333333%; +} +.eleven { + flex-basis: 91.66666667%; +} +.twelve { + flex-basis: 100%; +} +.col-offset-0 { + margin-left: 0; +} +.col-offset-1 { + margin-left: 8.33333333%; +} +.col-offset-2 { + margin-left: 16.66666667%; +} +.col-offset-3 { + margin-left: 25%; +} +.col-offset-4 { + margin-left: 33.33333333%; +} +.col-offset-5 { + margin-left: 41.66666667%; +} +.col-offset-6 { + margin-left: 50%; +} +.col-offset-7 { + margin-left: 58.33333333%; +} +.col-offset-8 { + margin-left: 66.66666667%; +} +.col-offset-9 { + margin-left: 75%; +} +.col-offset-10 { + margin-left: 83.33333333%; +} +.col-offset-11 { + margin-left: 91.66666667%; +} +.between { + justify-content: space-between; +} +.end { + justify-content: flex-end; +} +.around { + justify-content: space-around; +} +.row-align-center { + align-items: center; +} +.space-right { + margin-right: 10px; +} +.space-left { + margin-left: 10px; +} +.space-bottom { + margin-bottom: 10px; +} +.space-top { + margin-top: 10px; +} +@media (max-width: 980px) { + .columns { + flex: 1 1 auto; + } + .offset-0, + .offset-1, + .offset-2 { + margin: unset; + } + .container { + padding: 0 15px; + } +} diff --git a/static/idaf-logo.svg b/static/idaf-logo.svg new file mode 100644 index 0000000..091e344 --- /dev/null +++ b/static/idaf-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/logo.svg b/static/logo.svg new file mode 100644 index 0000000..b24ac12 --- /dev/null +++ b/static/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/main.js b/static/main.js new file mode 100644 index 0000000..7d7076a --- /dev/null +++ b/static/main.js @@ -0,0 +1,304 @@ +async function screenshotElement(element) { + const canvas = await html2canvas(element); + canvas.toBlob(blob => { + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'screenshot.png'; + a.click(); + URL.revokeObjectURL(url); + }); +} + +(() => { + const elForm = document.querySelector("form.search-form"); + const elQ = document.querySelector("#q"); + const elSelDict = document.querySelector("form.search-form select"); + const defaultLang = elSelDict.value; + + + // =================================== + // Helper functions. + + function selectDict(dict) { + // dict is in the format '$fromLang/$toLang'. + const langs = dict.split("/"); + Object.assign(localStorage, { dict, from_lang: langs[0], to_lang: langs[1] }); + elSelDict.value = dict; + elForm.setAttribute("action", `${_ROOT_URL}/dictionary/${dict}`); + } + + // Capture the form submit and send it as a canonical URL instead + // of the ?q query param. + function search(q) { + let val = q.trim(); + + const uri = elForm.getAttribute("action"); + document.location.href = `${uri}/${encodeURIComponent(val).replace(/%20/g, "+")}`; + } + + // =================================== + // Events + + // On ~ press, focus search input. + document.onkeydown = (function (e) { + if (e.key !== "`" && e.key !== "~") { + return; + } + + e.preventDefault(); + q.focus(); + q.select(); + }); + + // Bind to language change. + elSelDict.addEventListener("change", function (e) { + selectDict(e.target.value); + }); + + // Bind to form submit. + elForm.addEventListener("submit", function (e) { + e.preventDefault(); + search(elQ.value); + }); + + // "More" link next to entries/defs that expands an entry. + document.querySelectorAll(".more-toggle").forEach((el) => { + el.onclick = (e) => { + e.preventDefault(); + + let state = "block"; + if (el.dataset.open) { + delete (el.dataset.open); + state = "none"; + } else { + el.dataset.open = true; + state = "block"; + } + + const container = document.getElementById(el.dataset.id); + container.style.display = state; + + // fetch() content from the dataset data.guid from /api/entry/{guid} + // and populate the div.more innerHTML with the content. + if (state === "block" && !el.dataset.fetched) { + container.innerHTML = `
`; + + fetch(`${window._ROOT_URL}/api/dictionary/entries/${el.dataset.entryGuid}`) + .then((resp) => resp.json()) + .then((data) => { + const d = data.data; + container.innerHTML = ""; + + // If there's data.data.meta.synonyms, add them to the synonyms div. + if (d.meta.synonyms) { + const syns = document.createElement("div"); + syns.classList.add("synonyms"); + syns.innerHTML = d.meta.synonyms.map((s) => `${s}`).join(""); + container.appendChild(syns); + } + + // Each data.content[] word, add to target. + const more = document.createElement("div"); + more.classList.add("words"); + more.innerHTML = d.content.slice(window._MAX_CONTENT_ITEMS).map((c) => `${c}`).join(""); + container.appendChild(more); + + el.dataset.fetched = true; + }) + .catch((err) => { + console.error("Error fetching entry content:", err); + }); + } + }; + }); + + // =================================== + + // Select a language based on the page URL. + let dict = localStorage.dict || defaultLang; + const uri = /(dictionary)\/((.+?)\/(.+?))\//i.exec(document.location.href); + if (uri && uri.length == 5) { + dict = uri[2]; + } + + selectDict(dict); + elQ.focus(); + elQ.select(); +})(); + +// Screenshot sharing. +(() => { + document.querySelectorAll("a.export").forEach((el) => { + el.onclick = async (e) => { + e.preventDefault(); + const guid = el.dataset.guid; + const entryEl = document.querySelector(`.entry[data-guid='${guid}']`); + if (!entryEl) { + alert("Could not find entry to export"); + return; + } + + const title = entryEl.dataset.head; + // Make the filename by stripping spaces from the head word(s). + const filename = title.replace(/\s+/g, "_").toLowerCase(); + + try { + await shareDOM(entryEl, `${title} meaning`, `${localStorage.from_lang} to ${localStorage.to_lang} meaning\n\n${window.location.href}`, `${filename}.png`); + } catch (err) { + console.error("Error sharing entry:", err); + alert(`Error sharing entry: ${err?.message || err}`); + } + }; + }); +})(); + +// Play audio. +(() => { + document.querySelectorAll("a[data-audio]").forEach((el) => { + el.onclick = (e) => { + e.preventDefault(); + const audio = new Audio(el.getAttribute("href")); + audio.play().catch((err) => { + console.error("error playing audio:", err); + alert("error playing audio"); + }); + }; + }); +})(); + + +// Submission form. +(() => { + function filterTypes(e) { + // Filter the types select field with elements that are supported by the language. + const types = e.target.closest("fieldset").querySelector("select[name=relation_type]"); + types.querySelectorAll("option").forEach((o) => o.style.display = "none"); + types.querySelectorAll(`option[data-lang=${e.target.value}]`).forEach((o) => o.style.display = "block"); + types.selectedIndex = 1; + } + + + if (document.querySelector(".form-submit")) { + document.querySelectorAll("select[name=relation_lang]").forEach((e) => { + e.onchange = filterTypes; + }); + + // +definition button. + document.querySelector(".btn-add-relation").onclick = (e) => { + e.preventDefault(); + + if (document.querySelectorAll(".add-relations li").length >= 20) { + return false; + } + + // Clone and add a relation fieldset. + const d = document.querySelector(".add-relations li").cloneNode(true); + d.dataset.added = true + d.querySelector("select[name=relation_lang]").onchange = filterTypes; + document.querySelector(".add-relations").appendChild(d); + + // Remove definition link. + d.querySelector(".btn-remove-relation").onclick = (e) => { + e.preventDefault(); + d.remove(); + }; + }; + } +})(); + +// Edit form. +(() => { + document.querySelectorAll(".edit").forEach((o) => { + o.onclick = ((e) => { + e.preventDefault(); + const btn = e.target; + + // Form is already open. + if (btn.close) { + btn.close(); + return; + } + + const form = document.querySelector(".form-comments").cloneNode(true); + o.parentNode.appendChild(form); + form.style.display = "block"; + + const txt = form.querySelector("textarea"); + txt.focus(); + txt.onkeydown = (e) => { + if (e.key === "Escape" && txt.value === "") { + btn.close(); + } + }; + + btn.close = () => { + btn.close = null; + form.remove(); + }; + + // Handle form submission. + form.onsubmit = () => { + fetch(`${window._ROOT_URL}/api/submissions/comments`, { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + from_guid: btn.dataset.from, + to_guid: btn.dataset.to, + comments: txt.value + }) + }).catch((err) => { + alert(`Error submitting: ${err}`); + }); + + alert(form.dataset.success); + btn.close(); + }; + + form.querySelector("button.close").onclick = btn.close; + }); + }) +})(); + +// Autocomplete. +(() => { + if(!autocomp) { + return; + } + + const elForm = document.querySelector("form.search-form"); + const elQ = document.querySelector("#q"); + let debounce; + + autocomp(elQ, { + autoSelect: false, + onQuery: async (val) => { + const langCode = localStorage.from_lang; + clearTimeout(debounce); + return new Promise(resolve => { + debounce = setTimeout(async () => { + const response = await fetch(`${_ROOT_URL}/atl/${langCode}/${val.toLowerCase()}`); + const data = await response.json(); + + const a = data.greedy_tokenized.map(item => item.word).slice(0, 3).sort((a, b) => a.length - b.length); + const b = data.dictionary_suggestions.map(item => item.word).slice(0, 6).sort((a, b) => a.length - b.length); + + debounce = null; + resolve([...new Set(a.concat(b))]); + }, 50); + }); + }, + + onSelect: (val) => { + // autocomp search isn't complete. Use the user's input instead of autocomp selection. + if (val) { + elQ.value = val; + } + + elForm.dispatchEvent(new Event("submit", { cancelable: true })); + return elQ.value; + } + }); +})(); diff --git a/static/search.svg b/static/search.svg new file mode 100644 index 0000000..ca2a381 --- /dev/null +++ b/static/search.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/share.js b/static/share.js new file mode 100644 index 0000000..7591233 --- /dev/null +++ b/static/share.js @@ -0,0 +1,265 @@ +async function screenshotDOM(el, opts = {}) { + const { + scale = 2, + type = 'image/png', + quality = 0.92, + background = '#fff', + // if you want to *allow* splitting (and risk the bug), set to false + preventCapsuleSplit = true + } = opts; + + if (!(el instanceof Element)) throw new TypeError('Expected a DOM Element'); + + if (document.fonts && document.fonts.status !== 'loaded') { + try { await document.fonts.ready; } catch { } + } + + const { width, height } = (() => { + const r = el.getBoundingClientRect(); + return { width: Math.ceil(r.width), height: Math.ceil(r.height) }; + })(); + if (!width || !height) throw new Error('Element has zero width/height.'); + + const clone = el.cloneNode(true); + await inlineEverything(el, clone); + + if (background !== 'transparent') clone.style.background = background; + if (!clone.getAttribute('xmlns')) clone.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml'); + + const xhtml = new XMLSerializer().serializeToString(clone); + const svg = ` + ${xhtml} + `; + + const svgBlob = new Blob([svg], { type: 'image/svg+xml;charset=utf-8' }); + const url = URL.createObjectURL(svgBlob); + + try { + const img = await loadImage(url); + const canvas = document.createElement('canvas'); + canvas.width = Math.ceil(width * scale); + canvas.height = Math.ceil(height * scale); + const ctx = canvas.getContext('2d'); + if (background !== 'transparent' || type !== 'image/png') { + ctx.fillStyle = background === 'transparent' ? '#0000' : background; + ctx.fillRect(0, 0, canvas.width, canvas.height); + } + ctx.drawImage(img, 0, 0, canvas.width, canvas.height); + const blob = await new Promise(res => canvas.toBlob(res, type, quality)); + if (!blob) throw new Error('Canvas.toBlob returned null.'); + return blob; + } finally { + URL.revokeObjectURL(url); + } + + function loadImage(src) { + return new Promise((resolve, reject) => { + const img = new Image(); + img.crossOrigin = 'anonymous'; + img.decoding = 'async'; + img.onload = () => resolve(img); + img.onerror = reject; + img.src = src; + }); + } + + async function inlineEverything(srcNode, dstNode) { + copyComputedStyle(srcNode, dstNode); + + // Remove interactive states that may be captured on touch devices + if (dstNode instanceof Element) { + // Reset outline from :focus state + dstNode.style.setProperty('outline', 'none', 'important'); + // Reset any pointer-events to ensure no hover states + const cs = window.getComputedStyle(srcNode); + // Only override cursor if it's pointer (indicating interactivity) + if (cs.cursor === 'pointer') { + dstNode.style.setProperty('cursor', 'default', 'important'); + } + } + + if (srcNode instanceof HTMLTextAreaElement) { + dstNode.textContent = srcNode.value; + } else if (srcNode instanceof HTMLInputElement) { + dstNode.setAttribute('value', srcNode.value); + if ((srcNode.type === 'checkbox' || srcNode.type === 'radio') && srcNode.checked) { + dstNode.setAttribute('checked', ''); + } + } else if (srcNode instanceof HTMLSelectElement) { + const sel = Array.from(srcNode.options).filter(o => o.selected).map(o => o.value); + Array.from(dstNode.options).forEach(o => (o.selected = sel.includes(o.value))); + } + + if (srcNode instanceof HTMLCanvasElement) { + try { + const dataURL = srcNode.toDataURL(); + const img = document.createElement('img'); + img.src = dataURL; + copyBoxSizing(dstNode, img); + dstNode.replaceWith(img); + dstNode = img; + } catch { } + } + + // Handle img elements to ensure they render properly + if (srcNode instanceof HTMLImageElement && dstNode instanceof HTMLImageElement) { + // Convert image to data URL for inlining + if (srcNode.complete && srcNode.naturalWidth > 0) { + try { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + canvas.width = srcNode.naturalWidth; + canvas.height = srcNode.naturalHeight; + ctx.drawImage(srcNode, 0, 0); + dstNode.src = canvas.toDataURL('image/png'); + // Also set width/height to ensure proper rendering + const cs = window.getComputedStyle(srcNode); + dstNode.style.width = cs.width; + dstNode.style.height = cs.height; + dstNode.style.objectFit = cs.objectFit; + } catch (err) { + // If cross-origin or other error, keep original src + console.log('Could not inline image:', err); + dstNode.src = srcNode.src; + } + } else { + // Image not loaded or has no dimensions, use src as-is + dstNode.src = srcNode.src || srcNode.getAttribute('src') || ''; + } + // Remove alt text from being rendered + dstNode.removeAttribute('alt'); + } + + materializePseudo(srcNode, dstNode, '::before'); + materializePseudo(srcNode, dstNode, '::after'); + + const sKids = srcNode.childNodes; + const dKids = dstNode.childNodes; + for (let i = 0; i < sKids.length; i++) { + const s = sKids[i], d = dKids[i]; + if (s && d && s.nodeType === 1 && d.nodeType === 1) await inlineEverything(s, d); + } + + function copyComputedStyle(src, dst) { + const cs = window.getComputedStyle(src); + let cssText = ''; + for (const prop of cs) cssText += `${prop}:${cs.getPropertyValue(prop)};`; + dst.setAttribute('style', (dst.getAttribute('style') || '') + cssText); + + // Always keep transform origin consistent + if (cs.transformOrigin) dst.style.transformOrigin = cs.transformOrigin; + + // Keep each line painting its own decoration (helps some engines) + dst.style.setProperty('box-decoration-break', 'clone', 'important'); + dst.style.setProperty('-webkit-box-decoration-break', 'clone', 'important'); + + // --- CAPSULE FIX: prevent decorated inline from splitting across lines --- + if (preventCapsuleSplit) { + const hasBg = (cs.backgroundImage && cs.backgroundImage !== 'none') || + (cs.backgroundColor && cs.backgroundColor !== 'rgba(0, 0, 0, 0)' && cs.backgroundColor !== 'transparent'); + const hasRadius = ['borderTopLeftRadius', 'borderTopRightRadius', 'borderBottomRightRadius', 'borderBottomLeftRadius'] + .some(k => parseFloat(cs[k]) > 0); + const hasPad = ['paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft'] + .some(k => parseFloat(cs[k]) > 0); + const isInlineLevel = cs.display.startsWith('inline'); + + if (isInlineLevel && (hasBg || hasRadius || hasPad)) { + // Make it behave like a chip for capture: no internal wrapping + dst.style.setProperty('display', 'inline-block', 'important'); + dst.style.setProperty('white-space', 'nowrap', 'important'); + // In case original allowed breaking long words, emulate by clipping + if (cs.overflowWrap === 'break-word' || cs.wordBreak === 'break-all') { + dst.style.setProperty('max-width', cs.maxWidth && cs.maxWidth !== 'none' ? cs.maxWidth : '100%', 'important'); + dst.style.setProperty('overflow', 'hidden', 'important'); + dst.style.setProperty('text-overflow', 'ellipsis', 'important'); + } + } + } + } + + function copyBoxSizing(src, dst) { + const cs = window.getComputedStyle(src); + dst.style.width = cs.width; + dst.style.height = cs.height; + dst.style.display = cs.display; + dst.style.objectFit = 'contain'; + } + + function materializePseudo(src, dst, which) { + const ps = window.getComputedStyle(src, which); + if (!ps || ps.content === '' || ps.content === 'none') return; + const span = document.createElement('span'); + let cssText = ''; + for (const prop of ps) cssText += `${prop}:${ps.getPropertyValue(prop)};`; + span.setAttribute('style', cssText); + + const content = ps.content; + const quoted = /^(['"]).*\1$/.test(content); + if (quoted) span.textContent = content.slice(1, -1).replace(/\\n/g, '\n'); + else if (content.startsWith('attr(')) { + const attrName = content.slice(5, -1).trim(); + span.textContent = src.getAttribute(attrName) || ''; + } else span.textContent = ''; + + if (which === '::before') dst.insertBefore(span, dst.firstChild); + else dst.appendChild(span); + } + } +} + +async function shareDOM(target, title, text, filename) { + try { + // Remove any active/focus states before capturing + // This is especially important on touch devices (Android) + if (document.activeElement) { + document.activeElement.blur(); + } + + // Wait a brief moment for any :active states to clear + await new Promise(resolve => setTimeout(resolve, 100)); + + // Make a crisp image (PNG keeps transparency; use JPEG for smaller files) + const blob = await screenshotDOM(target, { + scale: Math.max(2, window.devicePixelRatio || 2), + type: 'image/png', + background: 'white' + }); + + const file = new File([blob], filename || 'share.png', { + type: blob.type, + lastModified: Date.now() + }); + + // Web Share API with files (Android Chrome, iOS/iPadOS Safari 16+). + if (navigator.canShare && navigator.canShare({ files: [file] })) { + await navigator.share({ title, text, files: [file] }); + return; + } + + // Copy image to clipboard (desktop Chrome/Edge, some Android) + if (navigator.clipboard && window.ClipboardItem) { + try { + await navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]); + alert('Screenshot copied to clipboard'); + return; + } catch (clipErr) { + // Clipboard write failed (common on Android), fall through to download + console.log('Clipboard write not allowed, downloading instead:', clipErr); + } + } + + // Fallback: prompt a download + const dlUrl = URL.createObjectURL(blob); + const a = Object.assign(document.createElement('a'), { + href: dlUrl, + download: 'share.png' + }); + document.body.appendChild(a); + a.click(); + a.remove(); + URL.revokeObjectURL(dlUrl); + } catch (err) { + console.error(err); + alert(`share failed: ${err?.message || err}`); + } finally { } +} diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..52c65a8 --- /dev/null +++ b/static/style.css @@ -0,0 +1,741 @@ +:root { + --primary: #111; + --secondary: #333; + --light: #666; + --lighter: #aaa; + --lightest: #eee; + --lightestest: #f4f4f4; + --white: #fff; + --blue: #36c; +} + +* { + box-sizing: border-box; +} + +body { + background: #fbfbfb; + font-family: "Helvetica Neue", "Segoe UI", Helvetica, sans-serif; + margin: 0; + color: var(--primary); +} + +html, +body { + font-size: 1em; + line-height: 1.3; + height: 100%; + min-width: 320px; +} + +:focus { + outline-style: solid; + outline-width: 2px; + outline-color: var(--blue); +} + +img { + max-width: 100%; +} + +hr { + height: 2px; + border: 0; + background: #ddd; + display: block; + margin: 30px 0; + clear: both; +} + +h1, +h2, +h3, +h4, +h5 { + margin: 10px 0 10px 0; +} + +h4, +h5 { + margin: 0 0 10px 0; + font-weight: normal; +} + +a { + color: var(--primary); +} + +a:hover { + color: var(--blue); +} + +input:not([type="radio"]), +select, +button, +textarea, +.button { + padding: 10px 15px; + font-family: "Helvetica Neue", "Segoe UI", Helvetica, sans-serif; + font-size: 1.2rem; + line-height: 1; + color: var(--primary); + border-radius: 5px; + border: 1px solid #ccc; +} + +select { + background-color: var(--white); + font-size: 0.875rem; +} + +label { + display: block; + color: var(--light); +} + +button, +.button { + background: var(--primary); + border-color: var(--primary); + color: var(--white); + width: auto; + cursor: pointer; + padding: 10px 20px; +} + +.button-outline { + background: var(--white); + color: var(--primary); +} + +.button-outline:hover { + background: var(--lighter); + color: var(--primary); +} + +button:hover, +.button:hover { + border-color: #444; + background: #444; +} + +.noul { + list-style-type: none; + margin: 0; + padding: 0; +} + +.text-right { + text-align: right; +} + +.box { + box-shadow: 2px 2px 1px #f6f6f6; + border: 1px solid #e6e6e6; + padding: 15px; +} + +.loader { + width: 16px; + padding: 3px; + aspect-ratio: 1; + border-radius: 50%; + background: #aaa; + --_m: + conic-gradient(#0000 10%, #000), + linear-gradient(#000 0 0) content-box; + -webkit-mask: var(--_m); + mask: var(--_m); + -webkit-mask-composite: source-out; + mask-composite: subtract; + animation: l3 1s infinite linear; +} + +@keyframes l3 { + to { + transform: rotate(1turn) + } +} + +/* Custom */ +.main { + margin: 30px auto 15px auto; + + display: flex; + flex-direction: column; +} + +.home .container { + max-width: 700px; + margin: auto; +} + +.logo a { + display: inline-block; +} + +.logo img { + height: auto; + max-height: 40px; +} + +.home .logo img { + max-width: 80%; +} + +.intro { + display: none; + color: #444; + margin: 30px 0 30px; + font-weight: normal; +} + +.search-form { + position: relative; +} + +.search-form>div { + display: flex; + align-items: stretch; +} + +.search-form .input-group { + display: flex; + flex-grow: 1; + align-self: center; +} + +.search-form input { + flex-grow: 1; + padding: 10px 15px; + border-width: 1px 0px; + border-radius: 0; +} + +.search-form select { + background-color: var(--white); + padding: 5px 15px; + border-radius: 30px 0 0 30px; + color: var(--light) +} + +.search-form select:focus { + z-index: 100; +} + +.search-form button { + line-height: 0; + padding: 7px 15px 7px 10px; + border-radius: 0 30px 30px 0; + z-index: 100; +} + +.search-form button img { + max-height: 32px; + min-width: 13px; +} + +.page { + margin: 30px 0 30px 0; +} + +.word { + background: var(--lightest); + padding: 0 5px; + border-radius: 30px; + margin: 0 5px 5px 0; + display: inline-block; + text-decoration: none; +} + + +.entries { + padding: 0; +} + +.entries .entry { + background-color: var(--white); + margin-bottom: 30px; + padding: 15px; + border-radius: 5px; + box-shadow: 2px 2px 1px #f6f6f6; + border: 1px solid #e6e6e6; + list-style-type: none; +} + +.entries .head { + position: relative; + margin-bottom: 10px; + display: flex; + align-items: baseline; + justify-content: space-between; +} + +.entries .title { + margin: 0; +} + +.entries .title-wrap { + display: flex; + align-items: center; + gap: 10px; +} + +.entries .audio { + display: inline-block; + vertical-align: middle; + cursor: pointer; + transition: ease-in-out 0.035s; +} +.entries .audio:hover { + transform: scale(1.1); +} + +.entries .pronun { + color: var(--light); +} + +.entries .defs { + padding: 0 0 0 30px; +} + +.entries .defs:not(:last-child) { + margin-bottom: 30px; +} + +.entries li::marker { + color: var(--lighter); +} + +.entries .defs li { + margin-bottom: 5px; + position: relative; +} + +.entries .defs li::marker { + font-size: 0.8em; +} + +.entries .defs .more { + display: none; +} + +.entries .defs .synonyms { + margin: 5px 0 10px 0; +} + +.entries .defs .synonyms a { + display: inline-block; + text-decoration: none; + color: var(--light); + font-size: 0.775rem; + margin-right: 8px; +} + +.entries .defs .synonyms a:hover { + color: var(--blue); +} + +.entries .more-toggle { + display: inline-block; + text-decoration: none; + transform: translate(0, 3px); + transition: transform 0.15s ease; + position: absolute; + margin: 0 5px; +} + +.entries .more-toggle::before { + width: 1em; + line-height: 0; + content: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='rgba%280,0,0,.5%29' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"); + transform-origin: .5em 50%; +} + +.entries .more-toggle[data-open] { + transform: rotate(-180deg) translate(0, 3px); +} + +.entries .defs .types { + list-style-type: none; + display: block; + margin: 0 0 10px -15px; + color: var(--light); + text-decoration: underline; + text-decoration-style: dashed; + text-decoration-color: var(--lighter); + font-size: 0.675rem; + font-weight: bold; +} + +.entries .defs .types span { + margin-right: 10px; +} + +.entry .edit { + padding: 3px; + display: none; + text-decoration: none; + z-index: 1000; + line-height: 1; + text-shadow: 0px 3px 3px #fff; + font-size: 0.8rem; +} + +.entries .defs li:hover .edit { + display: inline-block; +} + +.entry .head:hover .edit { + display: inline-block; +} + +.entries .tags { + display: inline-block; + background-color: var(--lightestest); + border: 1px solid var(--lightest); + border-radius: 3px; + font-size: 0.75rem; + line-height: 1; + padding: 2px 5px; + color: var(--lighter); +} +.entries .meta { + display: flex; + align-items: center; + gap: 10px; +} + +.related .word { + margin: 0 5px 8px 0; + padding: 5px 10px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: inline-block; +} + +.form-submit fieldset { + padding: 0; + margin: 0 0 20px 0; + border: 0; +} + +.form-submit select { + margin-bottom: 5px; +} + +.form-submit select, +.form-submit textarea { + width: 100%; +} + +.form-submit li { + margin-bottom: 20px; +} + +.form-submit li:first-child .btn-remove-relation { + display: none; +} + +.form-submit input, +.form-submit textarea { + width: 100%; +} + +.form-comments { + margin: 10px 0 45px 0; + display: none; + padding: 15px; + clear: both; +} + +.form-comments textarea { + width: 100%; + min-height: 200px; +} + +.form-comments h3 { + margin: 0 0 15px 0; +} + +.form-comments button { + margin: 0 10px 10px 0; + width: 100%; +} + +.glossary .index { + background: #f7f7f7; + margin-bottom: 15px; +} + +.glossary .index a { + text-decoration: none; + display: inline-block; + padding: 5px; +} + +.glossary .index .sel, +.glossary .index a:hover { + background: var(--primary); + color: var(--white); +} + +.glossary .words { + /* column-count: auto; + column-gap: 0; */ + column-count: 3; + column-gap: 40px; +} + +.glossary .words li { + border-bottom: 1px solid #eee; + page-break-inside: avoid; + margin-bottom: 10px; +} + +.glossary .words a { + text-decoration: none; +} + +.pagination { + margin-top: 30px; +} + +.pagination a { + display: inline-block; + text-decoration: none; +} + +.pagination .pg-page { + padding: 0 6px; +} + +.pagination .pg-selected { + font-weight: bold; + border-bottom: 2px solid var(--primary); +} + +.pagination.top { + margin-bottom: 30px; +} + +.pagination.bottom { + margin-top: 30px; +} + +/* Custom pages */ +.page.type textarea { + width: 100%; + min-height: 400px; +} + +.page { + background-color: #fff; + padding: 30px; +} + +.page li { + margin-bottom: 20px; +} + +.page a { + color: var(--blue); +} + +.page a:hover { + color: var(--secondary); +} + +.nav { + border-bottom: 1px solid #ddd; + padding: 30px 0; + text-align: center; + margin-bottom: 30px; +} + +.nav a { + text-decoration: none; + color: var(--light); + margin: 0 15px; +} + +.nav a:hover { + color: var(--blue); +} + +.header { + margin-bottom: 40px; +} + +.footer { + padding-bottom: 30px; + text-align: center; + font-size: 0.75rem; + color: var(--light); +} + +.footer a { + color: var(--light); + display: inline-block; + margin: 0 10px; + text-decoration: none; +} + +.footer a:hover { + color: var(--blue); +} + +.footer img { + max-height: 9px; +} + +.footer .slash { + margin: 0 10px; +} + +.footer .credit img { + height: 11px; + width: auto; +} + +.footer .credit:hover img { + opacity: 0.6; +} + + +.center { + text-align: center; +} + +/* Homepage */ +.home { + display: flex; + justify-content: center; + min-height: 100%; + padding: 15px 0; +} + +.home .header { + text-align: center; + margin-bottom: 40px; +} + +.home .intro { + display: block; +} + +.home .logo { + width: 100%; + flex: none; + margin-bottom: 15px; +} + +.home .logo img { + max-height: 100%; +} + +.home .search { + width: 100%; + flex: none; +} + +/* Autocomplete */ +.autocomp { + background: #f8f8f8; + border-radius: 0 0 5px 5px; + border: 1px solid #ccc; + border-top: 0; + box-shadow: 2px 2px 2px #eee; + text-align: left; + z-index: 100; +} + +.autocomp-item { + padding-bottom: 5px; + padding: 10px; + cursor: pointer; +} + +.autocomp-item:hover, +.autocomp-sel { + background: #f1f1f1; + font-weight: bold; +} + +@media screen and (max-width: 500px) { + .search-form select { + min-width: 0; + flex-shrink: 1; + } + + .search-form .input-group { + min-width: 0; + flex-shrink: 1; + } + + .search-form input { + min-width: 0; + } +} + +@media screen and (max-width: 720px) { + .logo { + text-align: center; + margin-bottom: 5px; + } + .logo img { + max-height: 42px; + } + + .main { + margin-top: 15px; + } + + .header { + margin-bottom: 0; + } + + .footer a, .nav a { + display: block; + line-height: 1.4rem; + } + + .footer a.icon { + display: inline-block; + } +} + +@media screen and (max-width: 320px) { + .search-form>div { + flex-wrap: wrap; + } + + .search-form select { + flex-basis: 100%; + border-radius: 30px; + padding: 10px 15px; + margin-bottom: 10px; + } + + .search-form .input-group { + flex-basis: 100%; + width: 100%; + min-width: 0; + } + + .search-form input { + border-width: 1px 0 1px 1px; + border-radius: 30px 0 0 30px; + padding: 10px 15px; + min-width: 0; + } + + .entries .entry { + margin-bottom: 15px; + } + + .entries .defs:not(:last-child) { + margin-bottom: 15px; + } +} diff --git a/static/thumb.png b/static/thumb.png new file mode 100644 index 0000000..058627b Binary files /dev/null and b/static/thumb.png differ diff --git a/static/up.svg b/static/up.svg new file mode 100644 index 0000000..bd278ae --- /dev/null +++ b/static/up.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/vkrishna.png b/static/vkrishna.png new file mode 100644 index 0000000..2e8be5b Binary files /dev/null and b/static/vkrishna.png differ diff --git a/static/zerodha-logo.svg b/static/zerodha-logo.svg new file mode 100644 index 0000000..5f59321 --- /dev/null +++ b/static/zerodha-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/submit-new.html b/submit-new.html new file mode 100644 index 0000000..5f446a2 --- /dev/null +++ b/submit-new.html @@ -0,0 +1,74 @@ +{{ define "submit-entry" }} +{{ template "header" . }} + +

{{ .L.T "public.submitEntryTitle" }}

+
+
+
+
+
+ + +
+
+
+ + +
+
+
+ + +
+ +
+

{{ .L.T "public.suggestDefsTitle" }}

+
    +
  1. +
    + + + + + +
    + {{ .L.T "global.btnDelete" }} +
    +
    +
    + + +
    +
  2. +
+

+ + {{ .L.T "public.suggestAddDefBtn" }} +

+ +
+

+ +

+
+
+ + + +{{ template "footer" . }} +{{ end }}