Modern and responsive Wikipedia UI concept. Use the search input on the top left corner to find and display articles.
Inspired by: Aurélien Salomon's https://dribbble.com/shots/1508672-Wikipedia-concept
| //- Add tags/description to CodePen | |
| #app | |
| ma-header | |
| ma-article | |
| //--- Vue component definitions ---// | |
| script#ma-header(type='text/x-template') | |
| div | |
| header.row.header | |
| .row__col.row__col--sm | |
| form.row.row--no-wrap(v-on:submit.prevent='findArticle') | |
| button.header__icon(@click.prevent='store.commit("toggleMenu")' type='button') | |
| i.fas.fa-bars.fa-lg | |
| input.header__text-input(v-model='search') | |
| button.header__icon(type='submit') | |
| i.fas.fa-search | |
| .row__col.row__col--lg | |
| .row.row--right | |
| button.header__icon( | |
| v-for='icon in icons' | |
| @click.prevent='setMode(icon.mode)' | |
| :class='{ "header__icon--active": mode === icon.mode }' | |
| ) | |
| i(:class='`${icon.type} fa-${icon.name} fa-fw`') | |
| .row__col.row__col--md | |
| .row.row--right | |
| button.header__icon | |
| i.far.fa-user | |
| .dropdown(@click.prevent='toggleDropdown') | |
| button.header__icon | |
| i.fas.fa-caret-down | |
| ul.dropdown__body.list(v-show='showDropdown') | |
| li | |
| a(href='#').list__link.list__link #[i.fas.fa-cog.fa-fw] Settings | |
| li | |
| a(href='#').list__link #[i.fas.fa-sign-out-alt.fa-fw] Logout | |
| .alert(v-show='mode === "edit"') | |
| i.fas.fa-bell | |
| | The article content below is editable now! | |
| script#ma-article(type='text/x-template') | |
| div | |
| .loader(v-if='store.state.isLoading' key='loading') | |
| i.fas.fa-sun.fa-7x.fa-spin | |
| .error-page(v-else-if='store.state.isArticleNotFound') | |
| h1 #[i.fas.fa-exclamation-triangle] 404 Article Not Found | |
| p Please try searching for something else. | |
| template(v-else) | |
| article.row.row--main | |
| aside.row__col.row__col--sm(v-show='store.state.showMenu') | |
| ma-logo | |
| ma-toc | |
| section.row__col.row__col--lg.article-content( | |
| v-html='store.state.article.content' | |
| :contenteditable='store.state.isEditingEnabled' | |
| ) | |
| aside.row__col.row__col--md( | |
| v-show='store.state.article.infobox' | |
| v-html='store.state.article.infobox' | |
| :contenteditable='store.state.isEditingEnabled' | |
| ) | |
| script#ma-logo(type='text/x-template') | |
| .logo | |
| a(href='#') | |
| img.logo__image(src='https://upload.wikimedia.org/wikipedia/commons/b/b3/Wikipedia-logo-v2-en.svg') | |
| script#ma-toc(type='text/x-template') | |
| .list | |
| .list__title Contents | |
| ul | |
| li.list__item(v-for='heading in store.state.article.headings') | |
| a.list__link(:href='"#" + heading.id') {{ heading.title }} | |
| ul | |
| li.list__item(v-for='heading in heading.children') | |
| a.list__link.list__link--secondary(:href='"#" + heading.id') {{ heading.title }} |
| //--- Helpers ---// | |
| function constructTableOfContents(doc) { | |
| const headings = [] | |
| doc.querySelectorAll('h2, h3').forEach(e => { | |
| const heading = { | |
| title: e.innerText, | |
| children: [], | |
| } | |
| heading.id = heading.title.replace(/\s+/g, '_') | |
| if (e.nodeName === 'H2') { | |
| headings.push(heading) | |
| } else { | |
| headings[headings.length - 1].children.push(heading) | |
| } | |
| }) | |
| return headings | |
| } | |
| // get article from Wikipedia API and 'clean' it | |
| async function fetchArticle(title) { | |
| const url = `https://en.wikipedia.org/w/api.php?action=parse&prop=text&page=${title}&format=json&disabletoc&disableeditsection&origin=*` | |
| const json = await (await fetch(url)).json() | |
| const articleTitle = json.parse.title | |
| const html = json.parse.text['*'] | |
| const doc = new DOMParser().parseFromString(html, 'text/html') | |
| const infobox = doc.getElementsByClassName('infobox')[0] | |
| // strip out unneeded meta html elements | |
| const elementsToRemove = [...doc.querySelectorAll('.navbox, .ambox, .sistersitebox, .mw-empty-elt')] | |
| elementsToRemove.push(infobox) | |
| elementsToRemove.forEach(e => { if (e) e.parentElement.removeChild(e) }) | |
| // make infobox responsive | |
| if (infobox) infobox.removeAttribute('style') | |
| return { | |
| headings: constructTableOfContents(doc), | |
| content: `<h1>${articleTitle}</h1>${doc.body.innerHTML}`, | |
| infobox: infobox ? infobox.outerHTML : null, | |
| } | |
| } | |
| // display wiki links inside app | |
| function handleWikiLinks() { | |
| document.querySelectorAll('a[href^="/wiki"]').forEach(link => { | |
| function clickHandler(event) { | |
| event.preventDefault() | |
| let href = event.target.href | |
| if (!href || href.indexOf('/wiki/File:') !== -1) return | |
| href = href.substring(href.indexOf('/wiki/') + 6) | |
| const hashIndex = href.indexOf('#') | |
| if (hashIndex !== -1) href = href.substring(0, hashIndex) | |
| store.commit('setArticle', href) | |
| } | |
| link.addEventListener('click', clickHandler) | |
| }) | |
| } | |
| //--- Vue Components ---// | |
| const maLogo = { | |
| template: '#ma-logo', | |
| } | |
| const maToc = { | |
| template: '#ma-toc', | |
| } | |
| const maHeader = { | |
| template: '#ma-header', | |
| data() { | |
| return { | |
| search: '', | |
| mode: 'view', | |
| icons: [ | |
| { mode: 'history', type: 'fas', name: 'history' }, | |
| { mode: 'comments', type: 'far', name: 'comment-alt' }, | |
| { mode: 'edit', type: 'fas', name: 'edit' }, | |
| { mode: 'view', type: 'far', name: 'file' }, | |
| ], | |
| showDropdown: false, | |
| } | |
| }, | |
| methods: { | |
| findArticle() { | |
| store.commit('setArticle', this.search) | |
| this.search = '' | |
| }, | |
| setMode(mode) { | |
| this.mode = mode | |
| store.state.isEditingEnabled = mode === 'edit' | |
| }, | |
| toggleDropdown() { | |
| this.showDropdown = !this.showDropdown | |
| }, | |
| }, | |
| } | |
| const maArticle = { | |
| template: '#ma-article', | |
| updated() { | |
| handleWikiLinks() | |
| }, | |
| components: { | |
| maLogo, | |
| maToc, | |
| }, | |
| } | |
| //--- Vuex Store ---// | |
| const store = new Vuex.Store({ | |
| state: { | |
| article: {}, | |
| isLoading: false, | |
| isArticleNotFound: false, | |
| showMenu: true, | |
| isEditingEnabled: false, | |
| }, | |
| mutations: { | |
| setArticle(state, title) { | |
| state.isLoading = true | |
| state.isArticleNotFound = false | |
| fetchArticle(title) | |
| .then(article => state.article = article) | |
| .catch(() => state.isArticleNotFound = true) | |
| .finally(() => state.isLoading = false) | |
| }, | |
| toggleMenu(state) { | |
| state.showMenu = !state.showMenu | |
| }, | |
| enableEditing(state) { | |
| state.isEditingEnabled = true | |
| }, | |
| disableEditing(state) { | |
| state.isEditingEnabled = false | |
| }, | |
| }, | |
| }) | |
| //--- Vue Instance ---// | |
| const vue = new Vue({ | |
| el: '#app', | |
| created() { | |
| // get example Wikipedia article | |
| store.commit('setArticle', 'Martinique') | |
| }, | |
| components: { | |
| maHeader, | |
| maArticle, | |
| }, | |
| store, | |
| }) |
| <script src="https://use.fontawesome.com/releases/v5.0.13/js/all.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/vuex/3.0.1/vuex.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/6.26.0/polyfill.min.js"></script> |
| //--- Variables ---// | |
| $c-white: #fff | |
| $c-black: #000 | |
| $c-grey-lighter: #eff1f2 | |
| $c-grey-light: #eae8e8 | |
| $c-grey: #888 | |
| $c-blue-light: #f5f8f9 | |
| $c-blue: #77a1d4 | |
| $c-blue-grey: #7d859d | |
| $c-blue-green: #0682c0 | |
| $p-w-md: 3em | |
| $p-w-lg: 7em | |
| $p-xs: .5em 1.5em | |
| $p-sm: .5em 2em | |
| $p-md: 1em $p-w-md | |
| $p-lg: 2em $p-w-lg | |
| $f-serif: 'Lora', 'Georgia', 'Times', serif | |
| $border: $c-grey-light 1px solid | |
| //--- Importing Google Fonts ---// | |
| @import url('https://fonts.googleapis.com/css?family=Lora') | |
| //--- Common Styles ---// | |
| *, *:before, *:after | |
| box-sizing: border-box | |
| ::selection | |
| background-color: $c-grey | |
| color: $c-black | |
| body | |
| font-size: 16px | |
| line-height: 1.7 | |
| h1, h2, h3 | |
| font-family: $f-serif | |
| font-weight: normal | |
| margin-top: 1.2em | |
| h1 | |
| font-size: 3em | |
| h2 | |
| font-size: 2em | |
| margin-top: 2.4em | |
| &:before | |
| content: '' | |
| margin-top: -1em | |
| border-top: $border | |
| width: 100% | |
| position: absolute | |
| left: 0 | |
| a | |
| color: $c-blue-green | |
| text-decoration: none | |
| &:hover | |
| text-decoration: underline | |
| img | |
| max-width: 100% | |
| height: auto | |
| ul | |
| list-style-type: none | |
| padding: 0 | |
| margin: 0 | |
| table | |
| display: block | |
| overflow-x: auto | |
| border-collapse: collapse | |
| th, td | |
| border: $border | |
| padding: $p-xs | |
| th | |
| background-color: $c-blue-light | |
| //--- Wikipedia Classes ---// | |
| // column on the right | |
| .infobox | |
| display: table | |
| width: 100% | |
| font-size: .8em | |
| border-bottom: $border | |
| th | |
| text-align: left | |
| td | |
| background-color: $c-blue-light | |
| th, td | |
| border-right: none | |
| border-left: none | |
| table | |
| display: table | |
| th, td | |
| border: none | |
| padding: 3px 1em | |
| // parent rows | |
| .mergedtoprow | |
| th, td | |
| border-bottom: none | |
| // children rows | |
| .mergedrow | |
| th, td | |
| border: none | |
| // notes in the begining of sections | |
| .hatnote | |
| padding: $p-sm | |
| display: inline-block | |
| background-color: $c-grey-lighter | |
| color: $c-blue-grey | |
| margin-bottom: 1em | |
| border-radius: 2em | |
| // floated boxes in article | |
| .tleft, .floatleft | |
| float: left | |
| clear: left | |
| margin: 0 1.2em 1.2em | |
| .tright, .floatright | |
| float: right | |
| clear: right | |
| margin: 0 1.2em 1.2em | |
| .tleft | |
| margin-left: -1 * $p-w-lg | |
| .tright | |
| margin-right: -1 * $p-w-lg | |
| // captions of floated boxes | |
| .thumbcaption | |
| font-size: .8em | |
| padding-right: .5em | |
| //--- BEM Components ---// | |
| .row | |
| display: flex | |
| flex-wrap: wrap | |
| &__col | |
| &--sm | |
| flex-grow: 1 | |
| &--md | |
| flex-grow: 6 | |
| &--lg | |
| flex-grow: 10 | |
| &--main | |
| & .row__col | |
| overflow-x: auto | |
| &--sm | |
| flex-basis: 200px | |
| &--md | |
| flex-basis: 250px | |
| &--lg | |
| flex-basis: 500px | |
| &--right | |
| justify-content: flex-end | |
| &--no-wrap | |
| flex-wrap: nowrap | |
| .header | |
| background: linear-gradient(to bottom right, $c-blue, $c-blue-grey) | |
| &__icon, &__text-input | |
| color: $c-white | |
| background-color: transparent | |
| outline: none | |
| border: none | |
| opacity: .6 | |
| &__icon | |
| padding: .8em .7em | |
| cursor: pointer | |
| &:hover | |
| opacity: 1 | |
| &--active | |
| opacity: 1 | |
| padding-bottom: .3em | |
| &:after | |
| content: '' | |
| display: block | |
| width: 0 | |
| border-right: .5em solid transparent | |
| border-left: .5em solid transparent | |
| border-bottom: .5em solid $c-white | |
| position: relative | |
| top: .5em | |
| &__text-input | |
| font-size: .8em | |
| border-bottom: $border | |
| margin: 1em 0 1em 1em | |
| padding: 0 | |
| width: 100% | |
| &:focus | |
| opacity: 1 | |
| .dropdown | |
| &__body | |
| position: absolute | |
| right: 0 | |
| margin-top: .2em | |
| .list | |
| &__title, &__link | |
| padding: $p-md | |
| background-color: $c-blue-light | |
| border-bottom: $border | |
| font-weight: bold | |
| &__title | |
| color: $c-grey | |
| text-align: center | |
| &__link | |
| display: block | |
| color: $c-black | |
| font-size: .9em | |
| &:hover | |
| text-decoration: none | |
| background-color: $c-grey-lighter | |
| &--secondary | |
| font-weight: unset | |
| padding: $p-sm | |
| padding-left: 4em | |
| border-bottom-width: 0 | |
| &__item:last-child &__link--secondary | |
| border-bottom-width: 1px | |
| .alert | |
| padding: $p-sm | |
| text-align: center | |
| border-bottom: $border | |
| .loader | |
| padding: 5em 1em 1em | |
| text-align: center | |
| color: $c-grey | |
| .error-page | |
| padding: $p-sm | |
| .logo | |
| padding: $p-sm | |
| border-bottom: $border | |
| text-align: center | |
| &__image | |
| width: 100% | |
| max-width: 200px | |
| .article-content | |
| padding: $p-lg | |
| border-right: $border | |
| border-left: $border | |
| position: relative | |
| z-index: 1 | |
| transition: box-shadow .2s ease-out | |
| &:hover | |
| box-shadow: 0 0 50px -20px | |
| ul | |
| list-style-type: disc | |
| padding-left: 2.5em | |
| margin: 1em 0 | |
| //--- Media Queries ---// | |
| @media screen and (max-width: 1220px) | |
| .tleft | |
| margin-left: -1 * $p-w-md | |
| .tright | |
| margin-right: -1 * $p-w-md | |
| .article-content | |
| padding: $p-md |
Modern and responsive Wikipedia UI concept. Use the search input on the top left corner to find and display articles.
Inspired by: Aurélien Salomon's https://dribbble.com/shots/1508672-Wikipedia-concept