Widget Components
A collection of reusable Vue.js components for web widgets.
Motivation
To reuse widget components across all web widget projects.
Requirements
The consuming project must be a Vue.js project with the following:
- Inside
/static/img/
if using Nuxt.js or/public/img
, require images:-
menu-icon.png
if using HeaderBar and there is a menu page. -
profile-picture.png
andprofile-form-pen-icon.png
if using ProfileForm -
white-cross.png
if using ShareOverlay -
profile-picture.png
if using LeaderboardBody or LeaderboardFooter - 6 icons (
leaderboard-star-0
toleaderboard-star-5
) if using LeaderboardBody or LeaderboardFooter - 7 images (
streak-0
tostreak-30
) if using LeaderboardBody or LeaderboardFooter -
info-icon.png
if using LeaderboardBody and need to display additional information about points
-
- vue-social-sharing and Font Awesome 5 if using ShareOverlay
- vuebar.css in global scope if using GenericLayout
.vb > .vb-dragger {
z-index: 5;
width: 12px;
right: 0;
}
.vb > .vb-dragger > .vb-dragger-styler {
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
-webkit-transform: rotate3d(0,0,0,0);
transform: rotate3d(0,0,0,0);
-webkit-transition:
background-color 100ms ease-out,
margin 100ms ease-out,
height 100ms ease-out;
transition:
background-color 100ms ease-out,
margin 100ms ease-out,
height 100ms ease-out;
background-color: rgba(150, 150, 150,.1);
margin: 5px 5px 5px 0;
border-radius: 20px;
height: calc(100% - 10px);
display: block;
}
.vb.vb-scrolling-phantom > .vb-dragger > .vb-dragger-styler {
background-color: rgba(150, 150, 150,.3);
}
.vb > .vb-dragger:hover > .vb-dragger-styler {
background-color: rgba(150, 150, 150,.5);
margin: 0px;
height: 100%;
}
.vb.vb-dragging > .vb-dragger > .vb-dragger-styler {
background-color: rgba(150, 150, 150,.5);
margin: 0px;
height: 100%;
}
.vb.vb-dragging-phantom > .vb-dragger > .vb-dragger-styler {
background-color: rgba(150, 150, 150,.5);
}
- leaderboard.css in global scope if using LeaderboardBody or LeaderboardFooter
.lb-table {
width: 100%;
border-collapse: collapse;
}
.lb-footer .lb-table {
margin: auto 0;
}
.lb-head {
text-transform: uppercase;
text-align: center;
color: #97A0A5;
background-color: #F1F5F7;
font-size: 80%;
}
.lb-head-row {
height: 2.4rem;
}
.lb-point-info-icon {
cursor: pointer;
height: 1rem;
margin-bottom: 0.25rem;
}
.lb-body-row {
border-bottom: 1px solid #dee2e6;
color: #232F43;
}
.lb-pundit-row {
background: linear-gradient(to right, #FFA548 , #FAD961);
}
.lb-pundit-row .lb-body-cell {
color: #fff;
}
.lb-rank-cell, .lb-streak-cell, .lb-btn-cell {
width: 4rem;
text-align: center;
}
.lb-rank-cell-label {
height: 2rem;
width: 2rem;
padding-top: 0.25rem;
margin-bottom: 0;
font-size: 1rem;
font-weight: 700;
}
.lb-rank-cell-p {
font-size: 1rem;
font-weight: 700;
}
.lb-name-cell {
width: 13rem;
display: flex;
}
.lb-profile-picture {
border-radius: 50%;
background-color: #fff;
border: 1px solid #dee2e6;
margin: 0.25rem 0;
height: 3.7rem;
width: 3.7rem;
}
.lb-star-icon {
position: absolute;
min-height: 1.6rem;
min-width: 1.6rem;
right: -0.8rem;
bottom: -0.8rem;
border-radius: 50%;
border: 0.1rem solid white;
}
.lb-name-cell-p-container {
margin: auto 0;
max-width: 14rem;
}
.lb-name-cell-p {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 1.2rem;
font-weight: 700;
margin: auto 0 auto 1.5rem;
}
.lb-name-cell-p-top-player {
font-size: 1rem;
}
.lb-point-cell {
width: 5rem;
text-align: center;
}
.lb-point-cell, .lb-streak-cell {
font-size: 1.2rem;
font-weight: 500;
}
.lb-streak-cell-flag {
width: 2.2rem;
}
.lb-streak-cell-flag-p {
position: absolute;
width: 100%;
bottom: 1.25rem;
}
.lb-btn-cell-btn {
background-color: #299934;
cursor: pointer;
color: #fff;
border-radius: 0;
padding: 0.25rem 0.5rem;
border: 1px solid transparent;
}
.lb-footer {
height: 5rem;
background-color: #232F43;
color: #fff;
display: flex;
border-radius: 0.8rem 0.8rem 0 0;
}
.lb-footer .lb-profile-picture {
margin-top: auto;
margin-bottom: auto;
}
.lb-footer .lb-btn-cell-btn {
background-color: #fff;
color: #4a4a4a;
border-radius: 0.25rem;
}
Getting Started
npm i @incrowd/widget-components
or yarn add @incrowd/widget-components
then in main.js
or a JavaScript file that serve as the entry point of the application:
import '@incrowd/widget-components'
It will automatically register all widget components.
Usage
GenericLayout
<generic-layout
fullBg="/path/to/background/image"
fullBgColor="colour"
bodyBg="/path/to/background/image"
:hideAlert="false">
<div slot="header">HEADER</div>
<div slot="subHeader">SUBHEADER</div>
<div slot="body">BODY</div>
<div slot="footer">FOOTER</div>
</generic-layout>
Props
Prop | Type | Default |
---|---|---|
fullBg | String | |
fullBgColor | String | |
bodyBg | String | |
hideAlert | Boolean | false |
Slots
header
and subHeader
slots will always be fixed at the top of the widget.
body
slot uses vuebar for scrollbar hence the CSS requirement to style it. body
will fill up any space left inside the widget between headers and footer.
footer
slot will always be fixed at the bottom of the widget.
Alert
An alert box is included as part of the GenericLayout
. To use it, it is required to have Vuex store set up and the store must contain the following default state and mutation:
export const state = () => ({
...
alert: null,
...
})
export const mutations = {
...
setAlert (state, alert) {
state.alert = alert
},
...
}
To show an alert, e.g. call store.commit('setAlert', {msg: 'Something went wrong', type: 'error'})
. The alert box will then have class of alert-${type}
. By default text in alert box is white in colour, to style certain type
of alert, add CSS in global scope, for example:
.alert-error {
background-color: #252525;
}
.alert-success {
background-color: #299934;
}
By default, the text for dismissing the alert is OK
, to replace it, call alert with btnText
as part of the object: store.commit('setAlert', {msg: 'Something went wrong', type: 'error', btnText: 'Dismiss'})
The alert will not be shown if hideAlert
prop is set to true
.
HeaderBar
<generic-layout>
<header-bar slot="header"
height="3"
headerBg="/path/to/background/image"
bgColor="colour"
title="title"
:isTitleWhite="true"
:hasShadow="false"
:hasMenu="false">
<div slot="left">LEFT</div>
<div slot="mid">MID</div>
<div slot="right">RIGHT</div>
</header-bar>
</generic-layout>
Props
Prop | Type | Default |
---|---|---|
height* | String | 5 |
headerBg | String | |
bgColor | String | #002672 |
hasBgShadow | Boolean | true |
title | String | |
isTitleWhite | Boolean | true |
titleClasses** | Array | |
hasShadow*** | Boolean | false |
hasMenu | Boolean | false |
midZIndex | String | 0 |
* height
in rem
** Array of CSS classes apply to title of header bar
*** If hasShadow
is true
, class box-shadow
will be applied, style it by adding CSS in global scope, for example:
.box-shadow {
box-shadow: 0 1px 5px 0 rgba(0,0,0,0.2);
}
Slots
Elements inside left
slot will be aligned to the left and elements inside right
slot will be aligned to the right.
If hasMenu
is true
, left
slot will display menu-icon.png
be default hence the requirement of having the image and clicking on it will navigate to /menu
page.
By default, text in mid
slot is centred and will display title
using <h3>
with class header-bar-title
.
ProfileForm
<template>
...
<profile-form
:profilePicture.sync="profilePicture"
:screenName.sync="screenName"
:firstName.sync="firstName"
:lastName.sync="lastName"
labelColor="#0054A5"
penIconBgColor="#299934"/>
...
</template>
<script>
export default {
data: () => ({
profilePicture: null,
screenName: null,
firstName: null,
lastName: null
})
}
</script>
Props
Prop | Type | Required | Default |
---|---|---|---|
profilePicture | Any | Yes | |
screenName | Any | Yes | |
firstName | Any | Yes | |
lastName | Any | Yes | |
labelColor | String | No | #707070 |
penIconBgColor | String | No | #000 |
The profile form consist of four inputs: An image upload and three text fields for the names. The parent component must have data
set up as shown above and pass them as props
into ProfileForm
with .sync
. When the values change inside ProfileForm
, the corresponding value in data
in parent will be updated as well.
Alert
store.commit('setAlert', {msg: 'Image too big', type: 'error'})
will be called if profile image size is greater than 8388607
.
Style
The three input fields for name have class form-control
, add CSS in global scope to style them.
Images
profile-picture.png
will be used as the placeholder image and profile-form-pen-icon.png
is the pen icon next to the profile image.
ShareOverlay
<template>
...
<share-overlay
:showShare.sync="showShare"
title="Share Title"
:platforms="platforms"
:trackShare="trackShare"
globalShareUrl="URL"/>
...
</template>
<script>
export default {
methods: {
trackShare (platform) {...}
},
data: () => ({
showShare: false,
platforms: [
...
{
network: 'facebook',
url: 'SHARE_URL',
title: 'TITLE',
description: 'DESCRIPTION',
quote: 'QUOTE'
}
...
]
})
}
</script>
Props
Prop | Type | Required |
---|---|---|
showShare | Boolean | Yes |
title | String | No |
platforms | Array | Yes |
trackShare | Function | No |
globalShareUrl | String | No |
.sync
is needed for showShare
to enable ShareOverlay
to update its value, there will be white-cross.png
that will set showShare
to false when clicked, effectively dismissing the overlay.
title
is the title of the overlay displayed above the platform icons.
platforms
is an array of objects, each object contains information about the social platform as shown above.
trackShare
will be called when a share dialog is opened.
If globalShareUrl
exists, it will be used instead of url
within each platform.
vue-social-sharing
vue-social-sharing is used, please read the documentation and construct an appropriate platforms
array. Please install NPM package vue-social-sharing
then in main.js
or a JavaScript file that serve as the entry point of the application, add:
import Vue from 'vue'
import SocialSharing from 'vue-social-sharing'
Vue.use(SocialSharing)
Currently ShareOverlay
only support the following networks: facebook
, twitter
, linkedin
, googleplus
, whatsapp
, sms
, email
, and only title
, description
, and quote
are supported for share text.
Font Awesome 5
Font Awesome 5 icons are required. Inside the template of the component, it uses tag name fa
for icons: <fa :icon="p.icon"/>
. In a Nuxt.js project, please install NPM packages nuxt-fontawesome
, @fortawesome/free-solid-svg-icons
and @fortawesome/free-brands-svg-icons
, then in nuxt.config.js
add:
modules: ['nuxt-fontawesome'],
fontawesome: {
component: 'fa',
imports: [
{set: '@fortawesome/free-solid-svg-icons'},
{set: '@fortawesome/free-brands-svg-icons', icons: ['faFacebookSquare', 'faTwitterSquare', 'faGooglePlusSquare', 'faLinkedin', 'faWhatsappSquare]}
]
}
LeaderboardBody
<template>
...
<leaderboard-body
:rankings="rankings"
:handleScroll="handleScroll"
:pointInfo="pointInfo"
:btnObj="btnObj"
type="round"
typeValue="1"/>
...
</template>
<script>
import _ from 'lodash'
export default {
computed: {
btnObj () {
return {
key: 'KEY',
text: 'BUTTON_TEXT',
action: () => { ... }
}
}
},
methods: {
handleScroll: _.throttle(function (body) {
// Your logic
}, 1000)
},
data: () => ({
rankings: {...},
pointInfo: 'POINT_INFO'
})
}
</script>
Props
Prop | Type | Required | Default |
---|---|---|---|
rankings | Any | Yes | |
handleScroll | Function | No | |
pointInfo | String | No | |
btnObj | Object | No | |
type | String | No | 'season' |
typeValue | String | No |
rankings
should be an object containing the leaderboard and user objects. The leaderboard object should then contains the users array which will be used to render the leaderboard.
handleScroll
is a function that will run when user scroll on the leaderboard. It is recommended to use lodash throttle
to limit calls per second.
pointInfo
is a string for displaying additional information about the points. If pointInfo
exist, info-icon.png
will be displayed next to points header and clicking on it will call store.commit('setAlert', {msg: pointInfo, type: 'info'})
. Please read above for more information about alert, and add CSS class alert-info
in global scope to style.
If btnObj
exist, an extra column of buttons will be added to the leaderboard. The key
controls whether a user should have a button or not, a button will be shown if user[key]
exist. btnObj.text
is the text displayed on the button and btnObj.action
will be called when the button is clicked.
type
can be season
, month
, or round
.
typeValue
for season
world normally be year such as 2018
, round
will be a number and so as month
, e.g. 8
will be used for August.
Style
Add leaderboard.css
in global scope and edit it when needed.
Streak
There is a column for streak and the streak images will be used depending on user's streak.
LeaderboardFooter
<leaderboard-footer :rankings="rankings" :btnObj="btnObj"/>
Props
Prop | Type | Required | Default |
---|---|---|---|
rankings | Any | Yes | |
btnObj | Object | No | |
type | String | No | 'season' |
typeValue | String | No |
Same as LeaderboardBody
except button won't check for key
.
Style
Add leaderboard.css
in global scope and edit it when needed.