npm package: @incrowd/widget-components
Description: A collection of reusable Vue.js components for web widgets.
Latest version: 1.10.2

# Widget Components

A collection of reusable Vue.js components for web widgets.

- [GenericLayout](#genericlayout)
- [HeaderBar](#headerbar)
- [ProfileForm](#profileform)
- [ShareOverlay](#shareoverlay)
- [LeaderboardBody](#leaderboardbody)
- [LeaderboardFooter](#leaderboardfooter)

## 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](https://nuxtjs.org/) or `/public/img`, require images:
  - `menu-icon.png` if using [HeaderBar](#headerbar) and there is a menu page.
  - `profile-picture.png` and `profile-form-pen-icon.png` if using [ProfileForm](#profileform)
  - `white-cross.png` if using [ShareOverlay](#shareoverlay)
  - `profile-picture.png` if using [LeaderboardBody](#leaderboardbody) or [LeaderboardFooter](#leaderboardfooter)
  - 6 icons (`leaderboard-star-0` to `leaderboard-star-5`) if using [LeaderboardBody](#leaderboardbody) or [LeaderboardFooter](#leaderboardfooter)
  - 7 images (`streak-0` to `streak-30`) if using [LeaderboardBody](#leaderboardbody) or [LeaderboardFooter](#leaderboardfooter)
  - `info-icon.png` if using [LeaderboardBody](#leaderboardbody) and need to display additional information about points
- [vue-social-sharing](https://nicolasbeauvais.github.io/vue-social-sharing/) and [Font Awesome 5](https://fontawesome.com/) if using [ShareOverlay](#shareoverlay)
- vuebar.css in global scope if using [GenericLayout](#genericlayout)

```css
.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](#leaderboardbody) or [LeaderboardFooter](#leaderboardfooter)

```css
.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

```html
<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](http://github.serafin.io/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:

```javascript
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:

```css
.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

```html
<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:

```css
.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

```html
<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

```html
<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](https://nicolasbeauvais.github.io/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:

```javascript
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](https://fontawesome.com/) 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:

```javascript
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

```html
<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

```html
<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.