diff --git a/src/App.vue b/src/App.vue index bf1fde0a457c4a5bf45222170399994b91f8319f..42155f8be07861bfb391fda0ac56f8c2456bc973 100644 --- a/src/App.vue +++ b/src/App.vue @@ -5,51 +5,7 @@ <p class="about-link"><a href="#">OM ORDBØKENE</a></p> <p class="sub-title"><a href="/">Bokmålsordboka | Nynorskordboka – rett norsk</a></p> </header> - <main :class="(article.error || article.lemmas.length || search_results.length || waiting) ? '' : 'welcome '"> - <div class="search_container"> - <div class="lang_select_container"> - <v-radio-group row v-model="lang"> - <template v-slot:label> - <span>VIS TREFF I</span> - </template> - <v-radio value="bob,nob" color="secondary"> - <template v-slot:label> - <span>begge<span class="verbose"> ordbøkene</span></span> - </template> - </v-radio> - <v-radio value="bob" color="secondary"> - <template v-slot:label> - <span>bokmål</span> - </template> - </v-radio> - <v-radio value="nob" color="secondary"> - <template v-slot:label> - <span>nynorsk</span> - </template> - </v-radio> - </v-radio-group> - </div> - <Autocomplete @submit="select_result" :endpoint="api_pref"> - </Autocomplete> - </div> - <div id="spinner"> - <v-progress-circular indeterminate color="secondary" size="120" v-show="waiting"></v-progress-circular> - </div> - <SearchResults :hits="search_results" :lang="lang" @article-click="article_link_click" v-show="! waiting" /> - <div id="single_article_container"> - <Article :key="article_key" :article="article" @article-click="article_link_click" /> - </div> - <div class="welcome" v-show="! (article.error || article.lemmas.length || search_results.length || waiting)"> - <div class="monthly"> - <div> - <Article :article="monthly_bm" @article-click="article_link_click" /> - </div> - <div> - <Article :article="monthly_nn" @article-click="article_link_click" /> - </div> - </div> - </div> - </main> + <router-view></router-view> <footer> <div> <img id="srlogo" src="./assets/Sprakradet_logo_neg.png" alt=""> @@ -173,7 +129,7 @@ function navigate_to_word(self, word) { } export default { - name: 'app', + name: 'App', data: function() { return { article_key: 0, diff --git a/src/Root.vue b/src/Root.vue new file mode 100644 index 0000000000000000000000000000000000000000..083049bd4e4f703469821ab82edfba3c2bf5c064 --- /dev/null +++ b/src/Root.vue @@ -0,0 +1,3 @@ +<template> + <router-view/> +</template> diff --git a/src/components/About.vue b/src/components/About.vue new file mode 100644 index 0000000000000000000000000000000000000000..03abe6024775aff6a3ed9d0f0585a20dc850779e --- /dev/null +++ b/src/components/About.vue @@ -0,0 +1,3 @@ +<template id=""> + <h1>Blæh</h1> +</template> diff --git a/src/components/DictionaryView.vue b/src/components/DictionaryView.vue new file mode 100644 index 0000000000000000000000000000000000000000..363e315532c2612dfa68c92fff7e5b83a8c40b4b --- /dev/null +++ b/src/components/DictionaryView.vue @@ -0,0 +1,442 @@ +<template> + <main :class="(article.error || article.lemmas.length || search_results.length || waiting) ? '' : 'welcome '"> + <div class="search_container"> + <div class="lang_select_container"> + <v-radio-group row v-model="lang"> + <template v-slot:label> + <span>VIS TREFF I</span> + </template> + <v-radio value="bob,nob" color="secondary"> + <template v-slot:label> + <span>begge<span class="verbose"> ordbøkene</span></span> + </template> + </v-radio> + <v-radio value="bob" color="secondary"> + <template v-slot:label> + <span>bokmål</span> + </template> + </v-radio> + <v-radio value="nob" color="secondary"> + <template v-slot:label> + <span>nynorsk</span> + </template> + </v-radio> + </v-radio-group> + </div> + <Autocomplete @submit="select_result" :endpoint="api_pref"> + </Autocomplete> + </div> + <div id="spinner"> + <v-progress-circular indeterminate color="secondary" size="120" v-show="waiting"></v-progress-circular> + </div> + <SearchResults :hits="search_results" :lang="lang" @article-click="article_link_click" v-show="! waiting" /> + <div id="single_article_container"> + <Article :key="article_key" :article="article" @article-click="article_link_click" /> + </div> + <div class="welcome" v-show="! (article.error || article.lemmas.length || search_results.length || waiting)"> + <div class="monthly"> + <div> + <Article :article="monthly_bm" @article-click="article_link_click" /> + </div> + <div> + <Article :article="monthly_nn" @article-click="article_link_click" /> + </div> + </div> + </div> + </main> +</template> + +<script> +import axios from "axios" +import entities from '../utils/entities.js' +import Article from './Article.vue' +import SearchResults from './SearchResults.vue' +import Autocomplete from './Autocomplete.vue' + +var api_endpoint = 'https://beta.ordbok.uib.no/api/dict' + +axios.interceptors.request.use(function (config) { + config.headers["x-api-key"] = "ZkYiyRVXxH86ijsvhx3cH4SY5Iik2ijI3BKVJGMm" + return config; + }, function (error) { + return Promise.reject(error); + }); + +function navigate_to_article(self, source) { + axios.get(api_endpoint + '/' + self.$route.params.lang + '/article/' + self.$route.params.id) + .then(function(response){ + self.article = Object.assign(response.data, {'dictionary': self.$route.params.lang}) + self.search_results = [] + }) + .catch(function(error){ + if (error.response && error.response.status == 404) { + self.article = { + lemmas: [], + error: "Vi har ingen artikkel med id " + self.$route.params.id + } + } else { + self.article = { + lemmas: [], + error: "Noe gikk galt..." + } + console.log(error) + } + }) + .then(function(response){ + self.waiting_for_articles = false + history.replaceState({article: self.article, search_results: [], lang: self.lang}, '') + if (source) { + self.$plausible.trackEvent('internal link incoming', {props: {origin: source}}) + } + }) +} + +function navigate_to_search(self, query) { + axios.get(self.api_pref + 'search?q=' + query) + .then(function(response){ + self.search_results = response.data + if (! self.search_results.length) { + self.article = { + lemmas: [], + error: "Vi fant ingen resultater for '" + decodeURIComponent(query) + "'. (Søkeforlag kommer i en senere oppatering av Ordbøkene)" + } + } + }) + .catch(function(error){ + if (error.response && error.response.status == 400) { + self.article = { + lemmas: [], + error: "Søkeuttrykket inneholder feil" + } + } else if (error.response) { + self.article = { + lemmas: [], + error: "Noe gikk galt på serversiden" + } + } else { + self.article = { + lemmas: [], + error: "Nettverksproblemer, prøv igjen" + } + } + }) + .then(function(_){ + self.waiting_for_articles = false + history.replaceState({article: self.article, search_results: self.search_results, lang: self.lang}, '') + }) +} + +function navigate_to_word(self, word) { + axios.get(self.api_pref + 'suggest?q=' + word) + .then(function(response){ + self.search_results = response.data.filter(result => result.match.length == word.length) + if (! self.search_results.length) { + self.article = { + lemmas: [], + error: "Ordet '" + decodeURIComponent(word) + "' finnes ikke i ordbøkene" + } + } + }) + .catch(function(error){ + if (error.response) { + self.article = { + lemmas: [], + error: "Noe gikk galt på serversiden" + } + } else { + self.article = { + lemmas: [], + error: "Nettverksproblemer, prøv igjen" + } + } + }) + .then(function(_){ + self.waiting_for_articles = false + history.replaceState({article: self.article, search_results: self.search_results, lang: self.lang}, '') + }) +} + +export default { + name: 'DictionaryView', + data: function() { + return { + article_key: 0, + search_results: [], + lang: 'bob,nob', + waiting_for_articles: true, + waiting_for_metadata: true, + article: {lemmas: [], body:{pronunciation: [], definitions: [], etymology: []}}, + monthly_bm: {lemmas: [], body:{pronunciation: [], definitions: [], etymology: []}}, + monthly_nn: {lemmas: [], body:{pronunciation: [], definitions: [], etymology: []}} + } + }, + computed: { + waiting: function() { + return (this.waiting_for_articles || this.waiting_for_metadata) && this.$route.name != 'root' + }, + api_pref: function() { + return api_endpoint + '/' + this.lang + '/article/' + } + }, + components: { + Article, + Autocomplete, + SearchResults + }, + methods: { + select_result: function(event) { + if(event.articles){ + this.$router.push('/' + this.lang + '/w/' + event.word) + this.search_results = event.articles + this.article = {lemmas: [], body:{pronunciation: [], definitions: [], etymology: []}} + history.replaceState({article: this.article, search_results: this.search_results, lang: this.lang}, '') + this.$plausible.trackEvent('dropdown selection', {props: {query: event.label, match: event.match}}) + }else{ + this.waiting_for_articles = true + this.article = {lemmas: [], body:{pronunciation: [], definitions: [], etymology: []}} + this.$router.push(`/${this.lang}/search/${event.q}`) + navigate_to_search(this, event.q) + this.$plausible.trackEvent('dropdown selection', {props: {query: event.label, match: '<fritekstsøk>'}}) + } + }, + article_link_click: function(item) { + if (this.article.article_id == item.article_id){ + this.article_key++ + history.replaceState({article: this.article, search_results: this.search_results, lang: this.lang}, '') + }else{ + this.article = {lemmas: [], body:{pronunciation: [], definitions: [], etymology: []}} + this.waiting_for_articles = true + navigate_to_article(this, item.source) + } + } + }, + mounted: function(){ + let self = this + this.lang = 'bob,nob' + + Promise.all([ + axios.get(api_endpoint + '/bob').then(function(response){ + let concepts = response.data.concepts + entities.bob = concepts + }), + axios.get(api_endpoint + '/nob').then(function(response){ + let concepts = response.data.concepts + entities.nob = concepts + }) + ]).then(function(_) { + self.waiting_for_metadata = false + if(self.$route.name == 'word') { + self.lang = self.$route.params.lang + navigate_to_word(self, self.$route.params.word) + } + else if(self.$route.name == 'lookup'){ + navigate_to_article(self, self.$route.params.id) + } + else if (self.$route.name == 'search') { + self.lang = self.$route.params.lang + navigate_to_search(self, self.$route.params.query) + } + else { + self.lang = self.$route.params.lang || 'bob,nob' + self.waiting_for_articles = false + history.replaceState({article: self.article, search_results: self.search_results, lang: self.lang}, '') + } + + // words of the month + axios.get(api_endpoint + '/bob/article/5607').then(function(response){ + self.monthly_bm = Object.assign(response.data, {dictionary: 'bob'}) + }) + + axios.get(api_endpoint + '/nob/article/78569').then(function(response){ + self.monthly_nn = Object.assign(response.data, {dictionary: 'nob'}) + }) + }).catch(function(_){ + self.article = { + lemmas: [], + error: "Et nettverksproblem hindret lasting av siden. Prøv å laste siden på nytt" + } + self.waiting_for_metadata = false + self.waiting_for_articles = false + }) + }, + watch: { + $route() { + this.$plausible.trackEvent('language', {props: {code: this.$route.params.lang}}) + } + }, + created: function() { + let self = this + window.onpopstate = function (event) { + if (event.state) { + self.article = event.state.article + self.search_results = event.state.search_results + self.lang = event.state.lang + } + } + + } +} +</script> + +<style> + @import url('https://fonts.googleapis.com/css2?family=Inria+Serif:ital,wght@0,400;0,700;1,400;1,700&display=swap'); + @import url('https://fonts.googleapis.com/css2?family=Inria+Serif:ital,wght@0,400;0,700;1,400;1,700&family=Noto+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap'); + +#app { + font-family: 'Noto Sans', Helvetica, Arial, sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + color: #2c3e50; + display: flex; + flex-direction: column; + height: 100%; +} + +html, body { + height: 100% +} + +body { + margin: 0px; + +} + +h1 { + font-family: Inria Serif; + font-size: 36px; + color: var(--v-primary-base); +} + + +header > h1 > a { + color: var(--v-secondary-base) !important; + font-size: 40px; + margin: 0px; + text-decoration: none; +} + +p.about-link { + text-align: right; + margin: 0px; + float: right; +} + +header > p > a { + color: var(--v-tertiary-base) !important; + text-decoration: none; +} + +span.beta { + color: #BBBBBB; +} + +p.about-link > a{ + text-decoration: none; + border-bottom: solid var(--v-secondary-base) 4px; + font-size: 12px; + color: var(--v-tertiary-base); +} + +main { + padding-bottom: 20px; + flex: 1 0 auto; + background-color: var(--v-tertiary-base); +} + +main.welcome { + background-image: url('../assets/books.jpg'); + background-repeat: no-repeat; + background-attachment: fixed; +} + +header, #search_results, #spinner, #single_article_container, footer, div.welcome, div.search_container { + padding-left: calc((100vw - 1000px) / 2); + padding-right: calc((100vw - 1000px) / 2); +} + +@media (max-width: 500px) { + .verbose { + display: none; + } +} + +#spinner { + padding-top: 40px; +} + +header { + padding-top: 20px; + padding-bottom: 20px; + background-color: var(--v-primary-base); +} + +div.monthly { + padding: 20px; + border-radius: 10px; + display: flex; + width: 100%; +} + +div.monthly > div { + flex: 50%; +} + +div.monthly article.bob .dict-label::before { + content: "månedens "; +} + +div.monthly article.nob .dict-label::before { + content: "månadens "; +} + +.sub-title { + font-size: 20px; + margin: 0px; +} + +footer { + font-size: smaller; + display: table; + flex-direction: row; + background-color: var(--v-primary-base); + color: #ffffff; +} + +.search_container { + max-width: 1400px; + padding-top: 50px; +} + +.v-label span { + color: var(--v-primary-base); +} + +li.suggestion { + font-weight: bold; + padding-left: 20px; + padding-top: 5px; + padding-bottom: 5px; + border: 0px; + background-image: none; +} + +footer > div { + display: table-cell; + vertical-align: middle; + padding: 10px; +} + +#srlogo { + height: 20px; +} + +#uiblogo { + height: 60px; +} + +::selection { + background: var(--v-secondary-base); + color: white; +} + +</style> diff --git a/src/main.js b/src/main.js index d91dda067e037b0b14d2c28805e67eff629d9452..6744e108942d7e6bf3bb030cb8b8c20de167660d 100644 --- a/src/main.js +++ b/src/main.js @@ -1,5 +1,8 @@ import Vue from 'vue' +import Root from './Root.vue' import App from './App.vue' +import About from './components/About.vue' +import DictionaryView from './components/DictionaryView.vue' import VueRouter from 'vue-router' import { VuePlausible } from 'vue-plausible' import vuetify from './plugins/vuetify'; @@ -17,28 +20,44 @@ const router = new VueRouter({ base: __dirname, routes: [ { - name: 'root', - path: '/:lang?', - component: App }, // No props, no nothing - { - name: 'word', - path: '/:lang/w/:word' - }, - { - name: 'lookup', - path: '/:lang/:id(\\d+)/:lemma?', - component: App, - props: true }, // Pass route.params to props - { - name: 'search', - path: '/:lang/search/:query', + path: '/', component: App, - props: true} + children: [ + { + path: 'om', + name: 'about', + component: About + }, + { + path: '', + component: DictionaryView, + children: [ + { + path: ':lang', + children: [ + { + name: 'word', + path: 'w/:word' + }, + { + name: 'lookup', + path: ':id(\\d+)/:lemma?' + }, + { + name: 'search', + path: 'search/:query' + } + ] + } + ] + } + ] + } ] }) new Vue({ router, vuetify, - render: h => h(App) + render: h => h(Root ) }).$mount('#app') diff --git a/vue.config.js b/vue.config.js index 2ae460b7a5a079f11a2e4db80dfa46d12b5a101d..650ea49f0f51e5f12e062b0332cd204fbe2e7d65 100644 --- a/vue.config.js +++ b/vue.config.js @@ -1,4 +1,5 @@ module.exports = { + runtimeCompiler: true, transpileDependencies: [ 'vuetify' ]