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