diff --git a/runtime/dapp-frontend-vue/resources/i18n/en/Medals.json b/runtime/dapp-frontend-vue/resources/i18n/en/Medals.json new file mode 100644 index 00000000..02c524cf --- /dev/null +++ b/runtime/dapp-frontend-vue/resources/i18n/en/Medals.json @@ -0,0 +1,15 @@ +{ + "medals": { + "badge_title": "My Badge", + "badge_description": "Keep recording your exercise efforts", + "medal_condition": "Condition:", + "medal_activities": "Eligible Activities:", + "medal_board": " Medal Board", + "referrals_title": "Referral", + "referrals_description": "Invite your friends to join and earn more medal!", + "distance_title": "Distance", + "distance_description": "Start workout and reach the target distance!", + "month_title": "Breast Cancer Month", + "month_description": "workout during the Breast Cancer Month to get special medals!" + } +} \ No newline at end of file diff --git a/runtime/dapp-frontend-vue/resources/i18n/index.ts b/runtime/dapp-frontend-vue/resources/i18n/index.ts index cf226872..23f881b0 100644 --- a/runtime/dapp-frontend-vue/resources/i18n/index.ts +++ b/runtime/dapp-frontend-vue/resources/i18n/index.ts @@ -15,6 +15,7 @@ import dashboardTranslations from "./en/Dashboard.json"; import settingsTranslations from "./en/Settings.json"; import activitiesTranslations from "./en/Activities.json"; import legalTranslations from "./en/Legal.json"; +import medalsTranslations from "./en/Medals.json"; // bundle all .JSON together export default { @@ -26,4 +27,5 @@ export default { ...settingsTranslations, ...activitiesTranslations, ...legalTranslations, + ...medalsTranslations, }; diff --git a/runtime/dapp-frontend-vue/src/App.scss b/runtime/dapp-frontend-vue/src/App.scss index f4ae2778..e9692b6d 100644 --- a/runtime/dapp-frontend-vue/src/App.scss +++ b/runtime/dapp-frontend-vue/src/App.scss @@ -15,6 +15,15 @@ color: #2c3e50; } +@font-face { + font-family: "Joystix Monospace"; + src: url("fonts/JoystixMonospace.ttf") format("truetype"); + font-weight: normal; + font-style: normal; + font-display: swap; + letter-spacing: -0.05em; +} + nav { padding: 30px; diff --git a/runtime/dapp-frontend-vue/src/App.ts b/runtime/dapp-frontend-vue/src/App.ts index f2b38b1b..5a1002e6 100644 --- a/runtime/dapp-frontend-vue/src/App.ts +++ b/runtime/dapp-frontend-vue/src/App.ts @@ -57,7 +57,11 @@ export default class App extends MetaView { text: "Dashboard", icon: "icons/menu-dashboard.svg", }, - { path: "#1", text: "Rewards", icon: "icons/menu-rewards.svg" }, + { + path: { name: "app.medals" }, + text: "Rewards", + icon: "icons/menu-rewards.svg", + }, { path: { name: "app.settings" }, text: "Settings", diff --git a/runtime/dapp-frontend-vue/src/_vars.scss b/runtime/dapp-frontend-vue/src/_vars.scss index bd2bf087..71bc45ea 100644 --- a/runtime/dapp-frontend-vue/src/_vars.scss +++ b/runtime/dapp-frontend-vue/src/_vars.scss @@ -13,6 +13,7 @@ $white: #ffffff; $black: #000000; $base-grey: #f6f6f6; $base-grey-lighter: #f7f7f7; +$base-grey-darker: #F9F8F8; $grey-darker: #d7dbe8; $system-grey-60: #7e7b93; $button-text-dark: #09090a; @@ -42,6 +43,9 @@ $trendline-positive: #3d7773; $trendline-negative: #f8503e; $tooltip-dark: #181818; $tooltip-grey-light: #f5f5f5; +$blue-1: #6EE7EE; + +$notifications-border-grey: #D9D9D9; // overlays $modal-overlay: rgba(19, 30, 25, 0.7); diff --git a/runtime/dapp-frontend-vue/src/assets/dhealth-notifications-icon.svg b/runtime/dapp-frontend-vue/src/assets/dhealth-notifications-icon.svg new file mode 100644 index 00000000..4af604bc --- /dev/null +++ b/runtime/dapp-frontend-vue/src/assets/dhealth-notifications-icon.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/runtime/dapp-frontend-vue/src/assets/medals/10.svg b/runtime/dapp-frontend-vue/src/assets/medals/10.svg new file mode 100644 index 00000000..022714ad --- /dev/null +++ b/runtime/dapp-frontend-vue/src/assets/medals/10.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/runtime/dapp-frontend-vue/src/assets/medals/100.svg b/runtime/dapp-frontend-vue/src/assets/medals/100.svg new file mode 100644 index 00000000..e149f813 --- /dev/null +++ b/runtime/dapp-frontend-vue/src/assets/medals/100.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/runtime/dapp-frontend-vue/src/assets/medals/50.svg b/runtime/dapp-frontend-vue/src/assets/medals/50.svg new file mode 100644 index 00000000..eb4f58ec --- /dev/null +++ b/runtime/dapp-frontend-vue/src/assets/medals/50.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/runtime/dapp-frontend-vue/src/assets/notifications-new.svg b/runtime/dapp-frontend-vue/src/assets/notifications-new.svg new file mode 100644 index 00000000..158f179c --- /dev/null +++ b/runtime/dapp-frontend-vue/src/assets/notifications-new.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/runtime/dapp-frontend-vue/src/assets/notifications.svg b/runtime/dapp-frontend-vue/src/assets/notifications.svg new file mode 100644 index 00000000..0f536c99 --- /dev/null +++ b/runtime/dapp-frontend-vue/src/assets/notifications.svg @@ -0,0 +1,3 @@ + + + diff --git a/runtime/dapp-frontend-vue/src/assets/rewards-tip.svg b/runtime/dapp-frontend-vue/src/assets/rewards-tip.svg new file mode 100644 index 00000000..e1457368 --- /dev/null +++ b/runtime/dapp-frontend-vue/src/assets/rewards-tip.svg @@ -0,0 +1,3 @@ + + + diff --git a/runtime/dapp-frontend-vue/src/assets/share-rewards.svg b/runtime/dapp-frontend-vue/src/assets/share-rewards.svg new file mode 100644 index 00000000..696427df --- /dev/null +++ b/runtime/dapp-frontend-vue/src/assets/share-rewards.svg @@ -0,0 +1,3 @@ + + + diff --git a/runtime/dapp-frontend-vue/src/components/AppHeader/AppHeader.ts b/runtime/dapp-frontend-vue/src/components/AppHeader/AppHeader.ts index 3e2a6b54..e3d45650 100644 --- a/runtime/dapp-frontend-vue/src/components/AppHeader/AppHeader.ts +++ b/runtime/dapp-frontend-vue/src/components/AppHeader/AppHeader.ts @@ -22,6 +22,9 @@ import MobileNavigationButton from "../MobileNavigationButton/MobileNavigationBu import Dropdown from "../Dropdown/Dropdown.vue"; import UserBalance from "../UserBalance/UserBalance.vue"; import UiButton from "../UiButton/UiButton.vue"; +import Notifications from "../Notifications/Notifications.vue"; + +import { Notification } from "../Notifications/Notifications"; // style resource import "./AppHeader.scss"; @@ -40,6 +43,7 @@ export interface HeaderLink { Dropdown, UserBalance, UiButton, + Notifications, }, computed: { ...mapGetters({ @@ -138,6 +142,15 @@ export default class AppHeader extends MetaView { ]; } + /** + * This computed defines *temporary* items + * for the notifications component. + * @todo remove once implement notifications on backend + * + * @access public + */ + public tempNotifications: Notification[] | never[] = []; + /** * Watcher that sets overflowY hidden, * to prevent scrolling of body when mobile menu opened @@ -160,4 +173,53 @@ export default class AppHeader extends MetaView { this.isMenuOpen = false; } } + + public handleNotificationView(notification: Notification) { + this.tempNotifications = this.tempNotifications.map( + (notificationItem: Notification) => ({ + ...notificationItem, + viewed: + notification.id === notificationItem.id + ? false + : notificationItem.viewed, + }) + ); + } + + public mounted() { + this.tempNotifications = [ + { + createdAt: "2h", + title: "Congratulations!", + description: "You have completed 10KM!", + icon: "dhealth-notifications-icon.svg", + viewed: true, + id: 0, + medal: { + image: "medals/10.svg", + condition: "Finish your first 10KM in one go to get!", + received: true, + relatedActivities: "Running, Walking, Swimming, Cycling", + assetId: process.env.VUE_APP_ASSETS_BOOST5_IDENTIFIER as string, + }, + }, + { + createdAt: "1d", + title: "Breast Cancer Month", + description: + "Get an energy boost with Breast Cancer Month special event!", + icon: "dhealth-notifications-icon.svg", + viewed: true, + id: 1, + }, + { + createdAt: "1d", + title: "ELEVATE", + description: "Welcome to your Notification Inbox!", + icon: "dhealth-notifications-icon.svg", + viewed: false, + id: 3, + }, + ]; + } } diff --git a/runtime/dapp-frontend-vue/src/components/AppHeader/AppHeader.vue b/runtime/dapp-frontend-vue/src/components/AppHeader/AppHeader.vue index c1fb47e9..ba719f42 100644 --- a/runtime/dapp-frontend-vue/src/components/AppHeader/AppHeader.vue +++ b/runtime/dapp-frontend-vue/src/components/AppHeader/AppHeader.vue @@ -121,6 +121,10 @@
+
diff --git a/runtime/dapp-frontend-vue/src/components/Notifications/Notifications.scss b/runtime/dapp-frontend-vue/src/components/Notifications/Notifications.scss new file mode 100644 index 00000000..eebb778b --- /dev/null +++ b/runtime/dapp-frontend-vue/src/components/Notifications/Notifications.scss @@ -0,0 +1,72 @@ +/** + * This file is part of dHealth dApps Framework shared under LGPL-3.0 + * Copyright (C) 2022-present dHealth Network, All rights reserved. + * + * @package dHealth dApps Framework + * @subpackage Vue Frontend + * @author dHealth Network + * @license LGPL-3.0 + */ + + @import "../../vars.scss"; + + .dapp-notifications { + cursor: pointer; + display: inline-block; + margin-right: 10px; + position: relative; + + &__list { + position: absolute; + top: 100%; + right: 0; + width: 326px; + background-color: $base-grey-balance; + padding: 34px 25px 24px 25px; + box-shadow: 3px 3px 0px $black; + border: 1px solid $black; + border-radius: 8px; + text-align: left; + + .notification-item { + padding-bottom: 11px; + margin-bottom: 8px; + border-bottom: 1px solid $notifications-border-grey; + + &:last-child { + border-bottom: none; + padding-bottom: 0; + margin-bottom: 0; + } + + h3 { + font-size: 13px; + font-weight: 700; + font-size: 13px; + line-height: 130%; + } + + p { + font-weight: 400; + font-size: 11px; + line-height: 130%; + } + + .icon-wrapper { + flex: 1 0 44px; + } + + .state { + text-align: center; + + .unread-circle { + width: 6px; + height: 6px; + display: inline-block; + border-radius: 50%; + background-color: $grey-system-dark; + } + } + } + } + } \ No newline at end of file diff --git a/runtime/dapp-frontend-vue/src/components/Notifications/Notifications.ts b/runtime/dapp-frontend-vue/src/components/Notifications/Notifications.ts new file mode 100644 index 00000000..e163e091 --- /dev/null +++ b/runtime/dapp-frontend-vue/src/components/Notifications/Notifications.ts @@ -0,0 +1,91 @@ +/** + * This file is part of dHealth dApps Framework shared under LGPL-3.0 + * Copyright (C) 2022-present dHealth Network, All rights reserved. + * + * @package dHealth dApps Framework + * @subpackage Vue Frontend + * @author dHealth Network + * @license LGPL-3.0 + */ +// external dependencies +import { Component, Prop } from "vue-property-decorator"; +import InlineSvg from "vue-inline-svg"; + +// internal dependencies +import { MetaView } from "@/views/MetaView"; +import { MedalItem } from "../RewardsList/RewardsList"; + +// style resource +import "./Notifications.scss"; + +export interface Notification { + createdAt: string | number; + title: string; + description: string; + icon: string; + viewed: boolean; + medal?: MedalItem; + id: number | string; +} + +/* + * @class Notifications + * @description This class implements a Vue component to display + * latest notifications received by user + * + * @since v0.3.0 + */ +@Component({ + components: { + InlineSvg, + }, + methods: {}, + computed: {}, +}) +export default class Notifications extends MetaView { + /** + * Prop which defines items for available notifications. + * + * @access protected + * @var {Notification[]} + */ + @Prop({ default: () => [] }) protected items?: Notification[]; + + isOpen = false; + + /** + * This computed checks if list contains + * item with "viewed: false" prop. + * + * @access protected + * @returns boolean + */ + public get unreadExist(): boolean { + const unread = this.items?.filter( + (notification: Notification) => notification.viewed === true + ); + return !!unread && unread.length > 0; + } + + public viewNotification(medalNotification: Notification) { + this.$emit("notification-viewed", medalNotification); + if (medalNotification.medal) { + const relatedMedal = medalNotification.medal; + this.$root.$emit("modal", { + type: "medal", + overlayColor: "rgba(0, 0, 0, 0.2)", + width: 720, + modalBg: "#FFFFFF", + medal: relatedMedal.image, + condition: relatedMedal.condition, + activities: relatedMedal.relatedActivities, + }); + } + } + + hideNotifications() { + if (this.isOpen) { + this.isOpen = false; + } + } +} diff --git a/runtime/dapp-frontend-vue/src/components/Notifications/Notifications.vue b/runtime/dapp-frontend-vue/src/components/Notifications/Notifications.vue new file mode 100644 index 00000000..98611c12 --- /dev/null +++ b/runtime/dapp-frontend-vue/src/components/Notifications/Notifications.vue @@ -0,0 +1,58 @@ + + + + diff --git a/runtime/dapp-frontend-vue/src/components/RewardsList/RewardsList.scss b/runtime/dapp-frontend-vue/src/components/RewardsList/RewardsList.scss new file mode 100644 index 00000000..ff6c420e --- /dev/null +++ b/runtime/dapp-frontend-vue/src/components/RewardsList/RewardsList.scss @@ -0,0 +1,113 @@ +/** + * This file is part of dHealth dApps Framework shared under LGPL-3.0 + * Copyright (C) 2022-present dHealth Network, All rights reserved. + * + * @package dHealth dApps Framework + * @subpackage Vue Frontend + * @author dHealth Network + * @license LGPL-3.0 + */ + + @import "../../vars.scss"; + + .dapp-rewards-list { + padding: 46px 106px; + box-shadow: 5px 5px 0px $black; + border-radius: 32px; + background-color: $base-grey-darker; + text-align: left; + + &__title, &__description { + color: $black; + } + + &__title { + font-weight: 600; + font-size: 42px; + line-height: 140%; + margin-bottom: 20px; + } + + &__description { + font-weight: 500; + font-size: 30px; + line-height: 140%; + } + + &__share { + font-weight: 500; + font-size: 30px; + line-height: 140%; + cursor: pointer; + + img { + display: inline-block; + } + } + + &__items { + display: flex; + flex-wrap: wrap; + justify-content: space-around; + + .medal { + flex: 1 0 21%; + text-align: center; + margin-bottom: 58px; + cursor: pointer; + position: relative; + + &:hover { + .medal__tip { + display: block; + } + } + + .medal__content { + opacity: 0.5; + } + + &.received { + .medal__content { + opacity: 1; + } + + &:hover { + .medal__tip { + display: none; + } + } + } + + + &__tip { + position: absolute; + width: 100%; + top: -115px; + background-color: $blue-1; + border-radius: 9px; + padding: 10px 28px; + color: $login-dark; + font-weight: 500; + font-size: 23.5311px; + line-height: 140%; + display: none; + opacity: 1; + + .triangle { + position: absolute; + display: inline-block; + bottom: -30px; + left: 50%; + transform: translateX(-50%); + } + } + + img { + display: inline-block; + width: 201px; + height: 218px; + } + } + } + } \ No newline at end of file diff --git a/runtime/dapp-frontend-vue/src/components/RewardsList/RewardsList.ts b/runtime/dapp-frontend-vue/src/components/RewardsList/RewardsList.ts new file mode 100644 index 00000000..3e00d88e --- /dev/null +++ b/runtime/dapp-frontend-vue/src/components/RewardsList/RewardsList.ts @@ -0,0 +1,109 @@ +/** + * This file is part of dHealth dApps Framework shared under LGPL-3.0 + * Copyright (C) 2022-present dHealth Network, All rights reserved. + * + * @package dHealth dApps Framework + * @subpackage Vue Frontend + * @author dHealth Network + * @license LGPL-3.0 + */ +// external dependencies +import { Component, Prop } from "vue-property-decorator"; +import InlineSvg from "vue-inline-svg"; + +// internal dependencies +import { MetaView } from "@/views/MetaView"; +import Card from "@/components/Card/Card.vue"; + +// style resource +import "./RewardsList.scss"; + +export interface MedalItem { + image: string; + condition: string; + relatedActivities?: string; + received: boolean; + assetId: string; +} + +/* + * @class RewardsList + * @description This class implements a Vue component to display + * basic list of available medals, based on props value + * + * @since v0.3.0 + */ +@Component({ + components: { + Card, + InlineSvg, + }, + methods: { + onMedalClick: () => ({}), + }, +}) +export default class RewardsList extends MetaView { + /** + * Prop which defines title of current rewards list, + * example: "Referral". + * Defaults to "". + * + * @access protected + * @var {string} + */ + @Prop({ default: "" }) protected title?: string; + + /** + * Prop which defines description of current rewards list, + * example: "Keep recording your exercise efforts". + * Defaults to "". + * + * @access protected + * @var {string} + */ + @Prop({ default: "" }) protected description?: string; + + /** + * Prop which defines list of the rewards and their state + * + * @access protected + * @var {MedalItem[]} + */ + @Prop({ default: () => [] }) protected medals?: MedalItem[]; + + /** + * This method opens "share" popup + * + * @access protected + * @returns void + */ + public handleShare() { + // display a custom modal popup + this.$root.$emit("modal", { + type: "share", + overlayColor: "rgba(0, 0, 0, .50)", + width: 518, + modalBg: "#FFFFFF", + }); + } + + /** + * This method handles opening of medal popup with data received in argument + * + * @access protected + * @returns void + */ + public onMedalClick(medal: MedalItem) { + if (medal.received) { + this.$root.$emit("modal", { + type: "medal", + overlayColor: "rgba(0, 0, 0, 0.2)", + width: 720, + modalBg: "#FFFFFF", + medal: medal.image, + condition: medal.condition, + activities: medal.relatedActivities, + }); + } + } +} diff --git a/runtime/dapp-frontend-vue/src/components/RewardsList/RewardsList.vue b/runtime/dapp-frontend-vue/src/components/RewardsList/RewardsList.vue new file mode 100644 index 00000000..fe518b7b --- /dev/null +++ b/runtime/dapp-frontend-vue/src/components/RewardsList/RewardsList.vue @@ -0,0 +1,43 @@ + + + + diff --git a/runtime/dapp-frontend-vue/src/components/Stats/Stats.scss b/runtime/dapp-frontend-vue/src/components/Stats/Stats.scss index 90ef12e1..a8ef76a6 100644 --- a/runtime/dapp-frontend-vue/src/components/Stats/Stats.scss +++ b/runtime/dapp-frontend-vue/src/components/Stats/Stats.scss @@ -28,21 +28,21 @@ text-align: center; .amount-big { - font-family: "Dotrice"; + font-family: "Joystix Monospace"; font-size: 72px; line-height: 100%; text-align: left; @include devices(laptop) { - font-size: 54px; + font-size: 46px; } .dark { - color: $button-text-dark; + color: $black; } .light { - color: $stats-teal-main; + color: $black; } } diff --git a/runtime/dapp-frontend-vue/src/components/UiPopup/UiPopup.scss b/runtime/dapp-frontend-vue/src/components/UiPopup/UiPopup.scss index db06d478..644a0787 100644 --- a/runtime/dapp-frontend-vue/src/components/UiPopup/UiPopup.scss +++ b/runtime/dapp-frontend-vue/src/components/UiPopup/UiPopup.scss @@ -164,5 +164,48 @@ } } } + + &__medal { + padding: 130px 56px 66px 56px; + border-radius: 41px; + position: relative; + border: 2px solid $black; + + .medal-image { + position: absolute; + top: -18%; + left: 50%; + transform: translateX(-50%); + width: 235px; + height: auto; + } + + .medal-details { + background-color: $stats-teal-light; + padding: 23px 52px; + border-radius: 20px; + + .details-item { + margin-bottom: 21px; + + .title { + font-weight: 700; + font-size: 35.7795px; + line-height: 140%; + } + + .value { + font-weight: 400; + font-size: 30.6681px; + line-height: 130%; + padding: 0 52px; + } + + &:last-child { + margin-bottom: 0; + } + } + } + } } } diff --git a/runtime/dapp-frontend-vue/src/components/UiPopup/UiPopup.ts b/runtime/dapp-frontend-vue/src/components/UiPopup/UiPopup.ts index 53fb71ad..fc243fdf 100644 --- a/runtime/dapp-frontend-vue/src/components/UiPopup/UiPopup.ts +++ b/runtime/dapp-frontend-vue/src/components/UiPopup/UiPopup.ts @@ -25,7 +25,7 @@ export interface ModalConfig { keepOnBgClick?: boolean; overlayColor?: string; modalBg?: string; - type: "form" | "notification" | "share"; + type: "form" | "notification" | "share" | "medal"; title?: string; width: number; fields?: any[]; @@ -33,6 +33,9 @@ export interface ModalConfig { description?: string; illustration?: string; shareNetworks?: SocialPlatformDTO[]; + condition?: string; + activities?: string; + medal?: string; } // styles source diff --git a/runtime/dapp-frontend-vue/src/components/UiPopup/UiPopup.vue b/runtime/dapp-frontend-vue/src/components/UiPopup/UiPopup.vue index 4f18529e..7f531b2e 100644 --- a/runtime/dapp-frontend-vue/src/components/UiPopup/UiPopup.vue +++ b/runtime/dapp-frontend-vue/src/components/UiPopup/UiPopup.vue @@ -117,6 +117,37 @@ :val="refCode" />
+ + +
+ + +
+
+

+

+

+
+

+

+

+
+
diff --git a/runtime/dapp-frontend-vue/src/components/index.ts b/runtime/dapp-frontend-vue/src/components/index.ts index a9dfcdd6..0b50283c 100644 --- a/runtime/dapp-frontend-vue/src/components/index.ts +++ b/runtime/dapp-frontend-vue/src/components/index.ts @@ -35,6 +35,8 @@ import TopActivities from "./TopActivities/TopActivities.vue"; import UiButton from "./UiButton/UiButton.vue"; import UiPopup from "./UiPopup/UiPopup.vue"; import UserBalance from "./UserBalance/UserBalance.vue"; +import RewardsList from "./RewardsList/RewardsList.vue"; +import Notifications from "./Notifications/Notifications.vue"; // scoped export of application-level components export const AppComponents = { @@ -62,6 +64,8 @@ export const AppComponents = { UiButton, UiPopup, UserBalance, + RewardsList, + Notifications, }; // scoped export of library-level components diff --git a/runtime/dapp-frontend-vue/src/fonts/JoystixMonospace.ttf b/runtime/dapp-frontend-vue/src/fonts/JoystixMonospace.ttf new file mode 100644 index 00000000..5fd36a54 Binary files /dev/null and b/runtime/dapp-frontend-vue/src/fonts/JoystixMonospace.ttf differ diff --git a/runtime/dapp-frontend-vue/src/models/AssetDTO.ts b/runtime/dapp-frontend-vue/src/models/AssetDTO.ts new file mode 100644 index 00000000..54b1efb6 --- /dev/null +++ b/runtime/dapp-frontend-vue/src/models/AssetDTO.ts @@ -0,0 +1,72 @@ +/** + * This file is part of dHealth dApps Framework shared under LGPL-3.0 + * Copyright (C) 2022-present dHealth Network, All rights reserved. + * + * @package dHealth dApps Framework + * @subpackage Vue Frontend + * @author dHealth Network + * @license LGPL-3.0 + */ +/** + * @interface AssetDTO + * @description This interface defines the fields that are returned + * by the backend runtime for individual entries using the endpoint + * `/assets`. + * endpoint. + *

+ * @example Using the `AssetDTO` interface + * ```typescript + * const entry = { + * data: [{ + * transactionHash: "4288A7ACF51A04AEFFBAA3DC96BCB96F20BA95671C19C3EE9E0443BC0FB79A61", + * userAddress: "NDAPPH6ZGD4D6LBWFLGFZUT2KQ5OLBLU32K3HNY", + creationBlock: 123456, + assetId: "39E0C49FA322A459", + amount: 123 + * }], + pagination: {} + * ``` + *

+ * #### Properties + * + * @param {string} data Contains array of asset entries. + * @param {string} pagination Contains pagination data. + * + * @since v0.5.0 + */ +export interface AssetDTO { + data: AssetEntry[]; + pagination: any; +} + +/** + * @interface AssetEntry + * @description This interface defines the fields that are included + * into `data` field in `/assets` endpoint response. + * endpoint. + *

+ * @example Using the `AssetDTO` interface + * ```typescript + * const entry = { + * transactionHash: "4288A7ACF51A04AEFFBAA3DC96BCB96F20BA95671C19C3EE9E0443BC0FB79A61", + * userAddress: "NDAPPH6ZGD4D6LBWFLGFZUT2KQ5OLBLU32K3HNY", + creationBlock: 123456, + assetId: "39E0C49FA322A459", + amount: 123 + } + * ``` + *

+ * #### Properties + * + * @param {string} data Contains array of asset entries. + * @param {string} pagination Contains pagination data. + * + * @since v0.5.0 + */ +export interface AssetEntry { + transactionHash: string; + userAddress: string; + creationBlock: number; + assetId: string; + amount: string; +} diff --git a/runtime/dapp-frontend-vue/src/router.ts b/runtime/dapp-frontend-vue/src/router.ts index 71fdffa4..79e66ebd 100644 --- a/runtime/dapp-frontend-vue/src/router.ts +++ b/runtime/dapp-frontend-vue/src/router.ts @@ -117,6 +117,16 @@ export const createRouter = ($store: any): VueRouter => { }, component: () => import("./views/Dashboard/Dashboard.vue"), }, + + { + path: "/medals", + name: "app.medals", + meta: { + layout: "app/default", + middleware: [auth], + }, + component: () => import("./views/Medals/Medals.vue"), + }, { path: "/settings", name: "app.settings", diff --git a/runtime/dapp-frontend-vue/src/services/RewardsService.ts b/runtime/dapp-frontend-vue/src/services/RewardsService.ts new file mode 100644 index 00000000..2f6a50b2 --- /dev/null +++ b/runtime/dapp-frontend-vue/src/services/RewardsService.ts @@ -0,0 +1,56 @@ +/** + * This file is part of dHealth dApps Framework shared under LGPL-3.0 + * Copyright (C) 2022-present dHealth Network, All rights reserved. + * + * @package dHealth dApps Framework + * @subpackage Vue Frontend + * @author dHealth Network + * @license LGPL-3.0 + */ + +// internal dependencies +import { BackendService } from "./BackendService"; +import { HttpRequestHandler } from "../kernel/remote/HttpRequestHandler"; +import { AssetDTO } from "../models/AssetDTO"; + +/** + * @class RewardsService + * @description This class handles backend requests for the `/assets` + * namespace and endpoints related to *social platforms*. + * + * @since v0.5.0 + */ +export class RewardsService extends BackendService { + /** + * This property sets the request handler used for the implemented + * requests. This handler forwards the execution of the request to + * `axios`. + * + * @access protected + * @returns {HttpRequestHandler} + */ + protected get handler(): HttpRequestHandler { + return new HttpRequestHandler(); + } + + /** + * This method fetches the social platform configuration objects + * from the backend runtime using the `/social/platforms` API. + * + * @access public + * @async + * @returns {Promise} An array of social platform configuration objects. + */ + public async getAssetsByAddress(address: string): Promise { + // fetch from backend runtime + const response = await this.handler.call( + this.getUrl(`assets/${address}`), + "GET", + undefined, // no-body + { withCredentials: true, credentials: "include" } // no-headers + ); + + // return the list of identifiers + return response.data; + } +} diff --git a/runtime/dapp-frontend-vue/src/state/store/RewardsModule.ts b/runtime/dapp-frontend-vue/src/state/store/RewardsModule.ts new file mode 100644 index 00000000..e982e1a4 --- /dev/null +++ b/runtime/dapp-frontend-vue/src/state/store/RewardsModule.ts @@ -0,0 +1,80 @@ +/** + * This file is part of dHealth dApps Framework shared under LGPL-3.0 + * Copyright (C) 2022-present dHealth Network, All rights reserved. + * + * @package dHealth dApps Framework + * @subpackage Vuex Store + * @author dHealth Network + * @license LGPL-3.0 + */ + +// external dependencies +import Vue from "vue"; +import { ActionContext } from "vuex"; + +// internal dependencies +import { RootState } from "./Store"; +import { AwaitLock } from "../AwaitLock"; +import { RewardsService } from "../../services/RewardsService"; +import { AssetEntry } from "@/models/AssetDTO"; + +// creates an "async"-lock for state of pending initialization +// this will be kept *locally* to this store module implementation +const Lock = AwaitLock.create(); + +export interface AssetsModuleState { + initialized: boolean; + userAssets: AssetEntry[]; +} + +/** + * + */ +export type AssetsModuleContext = ActionContext; + +export const AssetsModule = { + // this store module is namespaced, meaning the + // module name must be included when calling a + // mutation, getter or action, i.e. "integrations/getIntegrations". + namespaced: true, + state: (): AssetsModuleState => ({ + initialized: false, + userAssets: [], + }), + + getters: { + isLoading: (state: AssetsModuleState): boolean => !state.initialized, + getAssets: (state: AssetsModuleState): AssetEntry[] => state.userAssets, + }, + + mutations: { + /** + * + */ + setInitialized: (state: AssetsModuleState, payload: boolean): boolean => + (state.initialized = payload), + + setAssets: (state: AssetsModuleState, payload: AssetEntry[]) => + (state.userAssets = payload), + }, + + actions: { + /** + * + */ + async fetchRewards( + context: AssetsModuleContext, + address: string + ): Promise { + const service = new RewardsService(); + const response = await service.getAssetsByAddress(address); + const assets = response.data; + + console.log({ response }); + + context.commit("setAssets", assets); + + return assets; + }, + }, +}; diff --git a/runtime/dapp-frontend-vue/src/state/store/Store.ts b/runtime/dapp-frontend-vue/src/state/store/Store.ts index 3137b97b..25605d55 100644 --- a/runtime/dapp-frontend-vue/src/state/store/Store.ts +++ b/runtime/dapp-frontend-vue/src/state/store/Store.ts @@ -18,6 +18,7 @@ import { OAuthModule } from "./OAuthModule"; import { LeaderboardModule } from "./LeaderboardModule"; import { StatisticsModule } from "./StatisticsModule"; import { ActivitiesModule } from "./ActivitiesModule"; +import { AssetsModule } from "./RewardsModule"; /** * @todo missing interface documentation @@ -58,6 +59,7 @@ export const createStore = () => { leaderboard: LeaderboardModule, statistics: StatisticsModule, activities: ActivitiesModule, + assets: AssetsModule, }, state: (): RootState => ({ initialized: false, diff --git a/runtime/dapp-frontend-vue/src/views/Medals/Medals.scss b/runtime/dapp-frontend-vue/src/views/Medals/Medals.scss new file mode 100644 index 00000000..53bb5363 --- /dev/null +++ b/runtime/dapp-frontend-vue/src/views/Medals/Medals.scss @@ -0,0 +1,34 @@ +/** + * This file is part of dHealth dApps Framework shared under LGPL-3.0 + * Copyright (C) 2022-present dHealth Network, All rights reserved. + * + * @package dHealth dApps Framework + * @subpackage Vue Frontend + * @author dHealth Network + * @license LGPL-3.0 + */ + + @import "../../vars.scss"; + + .dapp-medals { + .boards-title { + display: inline-block; + background-color: $accent-yellow; + color: $black; + font-weight: 600; + font-size: 42px; + line-height: 140%; + margin-top: 120px; + margin-bottom: 50px; + border-radius: 8px; + padding: 4px 54px; + } + + .dapp-rewards-list { + margin-bottom: 105px; + + &:first-child { + margin-bottom: 0; + } + } + } \ No newline at end of file diff --git a/runtime/dapp-frontend-vue/src/views/Medals/Medals.ts b/runtime/dapp-frontend-vue/src/views/Medals/Medals.ts new file mode 100644 index 00000000..8762381c --- /dev/null +++ b/runtime/dapp-frontend-vue/src/views/Medals/Medals.ts @@ -0,0 +1,149 @@ +/** + * This file is part of dHealth dApps Framework shared under LGPL-3.0 + * Copyright (C) 2022-present dHealth Network, All rights reserved. + * + * @package dHealth dApps Framework + * @subpackage Vue Frontend + * @author dHealth Network + * @license LGPL-3.0 + */ +// external dependencies +import { Component } from "vue-property-decorator"; +import { mapGetters } from "vuex"; + +// internal dependencies +import { MetaView } from "@/views/MetaView"; +import RewardsList from "@/components/RewardsList/RewardsList.vue"; +import { MedalItem } from "@/components/RewardsList/RewardsList"; +import { AssetEntry } from "@/models/AssetDTO"; + +// style resource +import "./Medals.scss"; + +/* + * @class Medals + * @description This class implements a Vue component to display + * rewards page wit + * + * @since v0.3.0 + */ +@Component({ + components: { + RewardsList, + }, + computed: { + ...mapGetters({ + currentUserAddress: "auth/getCurrentUserAddress", + availableAssets: "assets/getAssets", + }), + }, +}) +export default class Medals extends MetaView { + /** + * This property contains the authenticated user's dHealth Account + * Address. This field is populated using the Vuex Store after a + * successful request to the backend API's `/me` endpoint. + *

+ * The `!`-operator tells TypeScript that this value is required + * and the *public* access permits the Vuex Store to mutate this + * value when it is necessary. + * + * @access public + * @var {string} + */ + public currentUserAddress!: string; + + /** + * This property represents vuex getter + * for the assets, available for user. + *

+ * It's value received from + * GET /assets/:address endpoint. + * + * @access public + * @var {AssetEntry[]} + */ + public availableAssets!: AssetEntry[]; + + /** + * This computed represents all available + * medals for the user, initially marked as + * *not received* + * + * @access protected + * @returns MedalItem[] + */ + public get knownMedals(): MedalItem[] { + return [ + { + image: "medals/10.svg", + condition: "Invite 5 users to receive medal!", + received: false, + relatedActivities: "Running, Walking, Swimming, Cycling", + assetId: process.env.VUE_APP_ASSETS_BOOST5_IDENTIFIER as string, + }, + { + image: "medals/50.svg", + condition: "Invite 10 users.", + received: false, + relatedActivities: "Running, Walking, Swimming, Cycling", + assetId: process.env.VUE_APP_ASSETS_BOOST10_IDENTIFIER as string, + }, + { + image: "medals/10.svg", + condition: "Invite 15 users", + received: false, + relatedActivities: "Running, Walking, Swimming, Cycling", + assetId: process.env.VUE_APP_ASSETS_BOOST15_IDENTIFIER as string, + }, + { + image: "medals/100.svg", + condition: "Complete one more workout to get.", + received: false, + relatedActivities: "Running, Walking, Swimming, Cycling", + assetId: "fakeIdToShowNotReceivedMedals1", + }, + { + image: "medals/50.svg", + condition: "Complete 50 kilometers to get.", + received: false, + relatedActivities: "Running, Walking, Swimming, Cycling", + assetId: "fakeIdToShowNotReceivedMedals2", + }, + { + image: "medals/100.svg", + condition: "Complete one more workout to get.", + received: false, + relatedActivities: "Running, Walking, Swimming, Cycling", + assetId: "fakeIdToShowNotReceivedMedals3", + }, + { + image: "medals/10.svg", + condition: "Finish your first 10KM in one go to get!", + received: false, + relatedActivities: "Running, Walking, Swimming, Cycling", + assetId: "fakeIdToShowNotReceivedMedals4", + }, + ]; + } + + /** + * This computed returns medals with + * *received* value based on available assets. + * + * @access protected + * @returns MedalItem[] + */ + public get validatedMedals(): MedalItem[] { + return this.knownMedals.map((medal: MedalItem) => ({ + ...medal, + received: !!this.availableAssets.find( + (asset: AssetEntry) => asset.assetId === medal.assetId + ), + })); + } + + public async mounted() { + await this.$store.dispatch("assets/fetchRewards", this.currentUserAddress); + } +} diff --git a/runtime/dapp-frontend-vue/src/views/Medals/Medals.vue b/runtime/dapp-frontend-vue/src/views/Medals/Medals.vue new file mode 100644 index 00000000..99e849d1 --- /dev/null +++ b/runtime/dapp-frontend-vue/src/views/Medals/Medals.vue @@ -0,0 +1,26 @@ + + + + diff --git a/runtime/dapp-frontend-vue/tests/unit/components/RewardsList.spec.ts b/runtime/dapp-frontend-vue/tests/unit/components/RewardsList.spec.ts new file mode 100644 index 00000000..74559585 --- /dev/null +++ b/runtime/dapp-frontend-vue/tests/unit/components/RewardsList.spec.ts @@ -0,0 +1,98 @@ +/** + * This file is part of dHealth dApps Framework shared under LGPL-3.0 + * Copyright (C) 2022-present dHealth Network, All rights reserved. + * + * @package dHealth UI Components + * @subpackage Unit Tests + * @author dHealth Network + * @license LGPL-3.0 + */ +// external dependencies +import { expect } from "chai"; +import { mount, createLocalVue } from "@vue/test-utils"; + +// components page being tested +import RewardsList from "@/components/RewardsList/RewardsList.vue"; +import { MedalItem } from "@/components/RewardsList/RewardsList"; + +// mocks the AuthService completely +jest.mock("@/services/AuthService"); + +// creates local vue instance for tests +const localVue = createLocalVue(); + +const getImageUrl = () => "../../../src/assets/ELEVATE.svg"; + +const componentOptions = { + localVue, + stubs: ["router-link"], + mocks: { + propsData: { + title: "testTitle", + description: "testDescription", + medals: [ + { + image: "testImagePath", + condition: "Test condition", + relatedActivities: "Run, Cycle, Swim", + received: true, + assetId: "testID", + }, + { + image: "testImagePath2", + condition: "Test condition2", + relatedActivities: "Walk", + received: false, + assetId: "testID2", + }, + ], + }, + getImageUrl, + $route: { params: {} }, + $router: { + push: jest.fn(), + }, + $t: jest.fn(), + }, +}; + +describe("Medals ->", () => { + let widget: any; + + beforeEach(() => { + (console as any).log = jest.fn(); + widget = mount(RewardsList as any, componentOptions); + }); + + it("should display component", () => { + expect(widget.find(".dapp-rewards-list").exists()).to.be.true; + }); + + it("should display title", () => { + expect(widget.find(".dapp-rewards-list__title").text()).to.be.equal( + widget.props("title") + ); + }); + + it("should display description", () => { + expect(widget.find(".dapp-rewards-list__description").text()).to.be.equal( + widget.props("description") + ); + }); + + it("should display list of medals", () => { + expect(widget.findAll(".medal").length).to.be.equal( + widget.props("medals").length + ); + }); + + it("should display received medals equal to prop", () => { + const receivedMedals = widget + .props("medals") + .filter((medal: MedalItem) => medal.received); + + expect(widget.findAll(".received").length).to.be.equal( + receivedMedals.length + ); + }); +}); diff --git a/runtime/dapp-frontend-vue/tests/unit/views/Medals.spec.ts b/runtime/dapp-frontend-vue/tests/unit/views/Medals.spec.ts new file mode 100644 index 00000000..e575f57b --- /dev/null +++ b/runtime/dapp-frontend-vue/tests/unit/views/Medals.spec.ts @@ -0,0 +1,76 @@ +/** + * This file is part of dHealth dApps Framework shared under LGPL-3.0 + * Copyright (C) 2022-present dHealth Network, All rights reserved. + * + * @package dHealth UI Components + * @subpackage Unit Tests + * @author dHealth Network + * @license LGPL-3.0 + */ +// external dependencies +import { expect } from "chai"; +import { mount, createLocalVue } from "@vue/test-utils"; + +// components page being tested +import Medals from "@/views/Medals/Medals.vue"; + +// mocks the AuthService completely +jest.mock("@/services/AuthService"); + +// creates local vue instance for tests +const localVue = createLocalVue(); + +const getImageUrl = () => "../../../src/assets/ELEVATE.svg"; + +const componentOptions = { + localVue, + stubs: ["router-link"], + mocks: { + getImageUrl, + $route: { params: {} }, + $router: { + push: jest.fn(), + }, + $t: jest.fn(), + $store: { + dispatch: jest.fn(), + commit: jest.fn(), + getters: { + "auth/getCurrentUserAddress": "fakeAddress", + "assets/getAssets": [ + { + transactionHash: "fakeHash", + userAddress: "fakeAddress", + creationBlock: 704585, + assetId: "fakeId", + amount: 1156898, + }, + { + transactionHash: "fakeHash2", + userAddress: "fakeAddress", + creationBlock: 769585, + assetId: "fakeId", + amount: 1281998, + }, + ], + }, + }, + }, +}; + +describe("Medals ->", () => { + let widget: any; + + beforeEach(() => { + (console as any).log = jest.fn(); + widget = mount(Medals as any, componentOptions); + }); + + it("should display component", () => { + expect(widget.find(".dapp-medals").exists()).to.be.true; + }); + + it("should display at least one list", () => { + expect(widget.find(".dapp-rewards-list").exists()).to.be.true; + }); +});