Explorar o código

Add page-views component

sbkwgh %!s(int64=8) %!d(string=hai) anos
pai
achega
e44e7cf452

+ 1 - 0
package.json

@@ -10,6 +10,7 @@
   },
   "dependencies": {
     "axios": "^0.15.3",
+    "d3": "^4.9.1",
     "highlight.js": "^9.10.0",
     "lodash.throttle": "^4.1.1",
     "marked": "^0.3.6",

+ 24 - 0
src/assets/scss/variables.scss

@@ -56,6 +56,30 @@ $color__lightblue--primary: #03A9F4;
 	animation-timing-function: linear;
 }
 
+@mixin loading-overlay($background-color: #fff, $border-radius: 0.25rem) {
+	width: 100%;
+	height: 100%;
+	position: absolute;
+	background-color: $background-color;
+	z-index: 1;
+	top: 0;
+	position: absolute;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	opacity: 0;
+	pointer-events: none;
+	transition: all 0.2s;
+	@include user-select(none);
+	cursor: default;
+	border-radius: $border-radius;
+
+	@at-root #{&}--show {
+		opacity: 1;
+		pointer-events: all;
+	}
+}
+
 @mixin text($family: $font--role-default, $size: 1rem, $weight: 300) {
 	font-family: $family;
 	font-size: $size;

+ 5 - 3
src/components/routes/AdminDashboard.vue

@@ -2,8 +2,8 @@
 	<div class='admin_dashboard'>
 		<div class='admin_dashboard__row'>
 			<div class='admin_dashboard__card admin_dashboard__card--3'>
-				card
-				<div class='admin_dashboard__card__title'>A title here</div>
+				<page-views></page-views>
+				<div class='admin_dashboard__card__title'>Page views over the past week</div>
 			</div>
 			<div class='admin_dashboard__card admin_dashboard__card--2'>
 				<new-posts></new-posts>
@@ -25,11 +25,13 @@
 
 <script>
 	import NewPosts from '../widgets/NewPosts'
+	import PageViews from '../widgets/PageViews'
 
 	export default {
 		name: 'AdminDashboard',
 		components: {
-			NewPosts
+			NewPosts,
+			PageViews
 		}
 	}
 </script>

+ 17 - 0
src/components/widgets/NewPosts.vue

@@ -1,5 +1,8 @@
 <template>
 	<div class='widgets__new_post'>
+		<div class='widgets__new_post__overlay' :class='{ "widgets__new_post__overlay--show" : loading }'>
+			<loading-icon></loading-icon>
+		</div>
 		<div class='widgets__new_post__main'>
 			<template v-if='count'>
 				{{count}} new {{count | pluralize('post')}}
@@ -26,13 +29,22 @@
 </template>
 
 <script>
+	import LoadingIcon from '../LoadingIcon'
+
 	export default {
 		name: 'NewPosts',
+		components: { LoadingIcon },
 		data () {
 			return {
+				loading: true,
 				count: 1,
 				change: -2,
 			}
+		},
+		created () {
+			setTimeout(() => {
+				this.loading = false;
+			}, 3000)
 		}
 	}
 </script>
@@ -47,11 +59,16 @@
 		height: 100%;
 		border-radius: 0.25rem 0.25rem 0 0;
 		display: flex;
+		position: relative;
 		flex-direction: column;
 		padding: 0.5rem;
 		align-items: center;
 		justify-content: center;
 
+		@at-root #{&}__overlay {
+			@include loading-overlay(#3498db, 0.25rem 0.25rem 0 0);
+		}
+
 		@at-root #{&}__main {
 			font-size: 2.5rem;
 			font-family: $font--role-emphasis;

+ 208 - 0
src/components/widgets/PageViews.vue

@@ -0,0 +1,208 @@
+<template>
+	<div class='widgets__page_views' ref='container'>
+		<div class='widgets__page_views__overlay' :class='{ "widgets__page_views__overlay--show" : loading }'>
+			<loading-icon></loading-icon>
+		</div>
+		<div
+			class='widgets__page_views__tooltip'
+			:class='{ "widgets__page_views__tooltip--show": tooltipShow }'
+			:style='{ "left": tooltipX, "top": tooltipY }'
+		>
+			{{data[tooltipItem].pageViews}} {{data[tooltipItem] | pluralize('page view') }}
+		</div>
+		<svg>
+			<g
+				ref='y_axis'
+				class='widgets__page_views__axis'
+				:transform='"translate(" + 3*padding + ",0)"'
+			></g>
+			<g
+				ref='x_axis'
+				class='widgets__page_views__axis widgets__page_views__axis--x'
+				:transform='"translate(0,150)"'
+			></g>
+			<path :d='linePath' fill='none' stroke-width='2' stroke='#fff'></path>
+			<circle
+				v-for='circle in circles'
+				:cx='circle.x'
+				:cy='circle.y'
+				r='4'
+				fill='rgb(255, 237, 127)'
+			>
+			</circle>
+			<circle
+				v-for='(circle, $index) in circles'
+				:cx='circle.x'
+				:cy='circle.y'
+				r='10'
+				fill='rgba(0, 0, 0, 0)'
+
+				@mousemove='showTooltip($event, $index)'
+				@mouseout='hideTooltip'
+			>
+			</circle>
+		</svg>
+	</div>
+</template>
+
+<script>
+	import LoadingIcon from '../LoadingIcon'
+
+	import * as d3 from 'd3'
+	import throttle from 'lodash.throttle'
+
+	export default {
+		name: 'PageViews',
+		components: { LoadingIcon },
+		data () {
+			let data = [
+				{ pageViews: Math.floor(Math.random() * 100) },
+				{ pageViews: Math.floor(Math.random() * 100) },
+				{ pageViews: Math.floor(Math.random() * 100) },
+				{ pageViews: Math.floor(Math.random() * 100) },
+				{ pageViews: Math.floor(Math.random() * 100) },
+				{ pageViews: Math.floor(Math.random() * 100) },
+				{ pageViews: Math.floor(Math.random() * 100) }
+			]
+
+			let x = d3
+				.scaleLinear()
+				.domain([0, 0])
+				.range([0, 0])
+
+			let y = d3
+				.scaleLinear()
+				.domain([0, 0])
+				.range([0, 0])
+
+			return {
+				loading: false,
+				padding: 10,
+				
+				tooltipX: 0,
+				tooltipY: 0,
+				tooltipShow: false,
+				tooltipItem: 0,
+
+				data, x, y,
+			}
+		},
+		methods: {
+			setXFunc () {
+				let width = this.$refs.container.getBoundingClientRect().width - this.padding*2
+
+				this.x = d3
+					.scaleLinear()
+					.domain([0, this.data.length-1])
+					.range([this.padding * 4, width])
+			},
+			setYFunc () {
+				let height = this.$refs.container.getBoundingClientRect().height - this.padding*2
+
+				this.y = d3
+					.scaleLinear()
+					.domain([d3.max(this.data.map(d => d.pageViews)), 0])
+					.range([this.padding*1.5, height - this.padding/2])
+			},
+			updateFuncs () {
+				this.setXFunc()
+				this.setYFunc()
+
+				d3.select(this.$refs.x_axis).call(d3.axisBottom(this.x).tickSize(0))
+			},
+			showTooltip (e, i) {
+				this.tooltipShow = true
+				this.tooltipX = e.clientX + 'px'
+				this.tooltipY = e.clientY - 30 + 'px'
+				this.tooltipItem = i
+			},
+			hideTooltip () {
+				this.tooltipShow = false
+			}
+		},
+		computed: {
+			linePath () {
+				let line = 	d3
+					.line()
+					.curve(d3.curveCatmullRom)
+					.x((d, i) => this.x(i))
+					.y(d => this.y(d.pageViews))
+
+				return line(this.data)
+			},
+			circles () {
+				return this.data.map((d, i) => {
+					return { x: this.x(i), y: this.y(d.pageViews) }
+				})
+			}
+		},
+		mounted () {
+			this.updateFuncs()
+
+			let resizeCb = throttle(() => {
+				this.updateFuncs()
+			}, 200)
+			window.addEventListener('resize', resizeCb)
+
+			d3.select(this.$refs.y_axis).call(d3.axisLeft(this.y.nice()))
+			d3.select(this.$refs.x_axis).call(d3.axisBottom(this.x).tickSize(0))
+		}
+	}
+</script>
+
+<style lang='scss'>
+	@import '../../assets/scss/variables.scss';
+
+	.widgets__page_views {
+		background-color: #f39c12;
+		width: 100%;
+		height: 100%;
+		overflow: hidden;
+		border-radius: 0.25rem 0.25rem 0 0;
+		position: relative;
+
+		@at-root #{&}__overlay {
+			@include loading-overlay(#f39c12, 0.25rem 0.25rem 0 0);
+		}
+
+		@at-root #{&}__tooltip {
+			position: fixed;
+			background-color: rgba(256, 256, 256, 0.9);
+			pointer-events: none;
+			display: inline-block;
+			opacity: 0;
+			padding: 0.25rem;
+			z-index: 1;
+			border-radius: 0.25rem;
+			transition: all 0.2s;
+
+			@at-root #{&}--show {
+				opacity: 1;
+			}
+		}
+
+		@at-root #{&}__axis {
+			line {
+				stroke: #fff;
+			}
+			path {
+				stroke: #fff;
+			}
+			text {
+				fill: #fff;
+			}
+
+			@at-root #{&}--x {
+				text {
+					transform: translate(0, 2px);
+				}
+			}
+
+		}
+
+		svg {
+			height: 100%;
+			width: 100%;
+		}
+	}
+</style>