diff --git a/admin.go b/admin.go
index 4f42570..9923c73 100644
--- a/admin.go
+++ b/admin.go
@@ -3,8 +3,10 @@ package main
import (
"bytes"
"encoding/json"
+ "fmt"
"net/http"
+ "github.com/jmoiron/sqlx/types"
"github.com/labstack/echo"
)
@@ -15,10 +17,8 @@ type configScript struct {
Messengers []string `json:"messengers"`
}
-// handleGetStats returns a collection of general statistics.
-func handleGetStats(c echo.Context) error {
- app := c.Get("app").(*App)
- return c.JSON(http.StatusOK, okResp{app.Runner.GetMessengerNames()})
+type dashboardStats struct {
+ Stats types.JSONText `db:"stats"`
}
// handleGetConfigScript returns general configuration as a Javascript
@@ -41,3 +41,18 @@ func handleGetConfigScript(c echo.Context) error {
j.Encode(out)
return c.Blob(http.StatusOK, "application/javascript", b.Bytes())
}
+
+// handleGetDashboardStats returns general states for the dashboard.
+func handleGetDashboardStats(c echo.Context) error {
+ var (
+ app = c.Get("app").(*App)
+ out dashboardStats
+ )
+
+ if err := app.Queries.GetDashboardStats.Get(&out); err != nil {
+ return echo.NewHTTPError(http.StatusInternalServerError,
+ fmt.Sprintf("Error fetching dashboard stats: %s", pqErrMsg(err)))
+ }
+
+ return c.JSON(http.StatusOK, okResp{out.Stats})
+}
diff --git a/frontend/my/package.json b/frontend/my/package.json
index d51b916..7c90fe1 100644
--- a/frontend/my/package.json
+++ b/frontend/my/package.json
@@ -5,6 +5,7 @@
"dependencies": {
"antd": "^3.6.5",
"axios": "^0.18.0",
+ "bizcharts": "^3.2.5-beta.4",
"dayjs": "^1.7.5",
"react": "^16.4.1",
"react-app-rewire-less": "^2.1.3",
diff --git a/frontend/my/src/Dashboard.js b/frontend/my/src/Dashboard.js
index 63aa962..7a3da08 100644
--- a/frontend/my/src/Dashboard.js
+++ b/frontend/my/src/Dashboard.js
@@ -1,14 +1,123 @@
-import React from 'react';
+import { Col, Row, notification, Card, Tooltip, Icon } from "antd"
+import React from "react";
+import { Chart, Axis, Geom, Tooltip as BizTooltip } from 'bizcharts';
+
+import * as cs from "./constants"
class Dashboard extends React.PureComponent {
- componentDidMount = () => {
- this.props.pageTitle("Dashboard")
- }
- render() {
- return (
-
Welcome
- );
- }
+ state = {
+ stats: null
+ }
+
+ campaignTypes = ["running", "finished", "paused", "draft", "scheduled", "cancelled"]
+
+ componentDidMount = () => {
+ this.props.pageTitle("Dashboard")
+
+ this.props.request(cs.Routes.GetDashboarcStats, cs.MethodGet).then((resp) => {
+ this.setState({ stats: resp.data.data })
+ }).catch(e => {
+ notification["error"]({ message: "Error", description: e.message })
+ })
+ }
+
+ orZero(v) {
+ return v ? v : 0
+ }
+
+ render() {
+ return (
+
+ Welcome
+
+ { this.state.stats &&
+
+
+
+
+
+
+ { this.orZero(this.state.stats.subscribers.enabled) }
+
+
+
+
+ { this.orZero(this.state.stats.subscribers.blacklisted) }
+
+
+
+
+ { this.orZero(this.state.stats.orphan_subscribers) }
+
+
+
+
+
+
+
+
+ { this.orZero(this.state.stats.lists.public) }
+
+
+
+
+ { this.orZero(this.state.stats.lists.private) }
+
+
+
+
+
+
+
+
+
+
+
+
+ { this.state.stats.campaign_views.reduce((total, v) => total + v.count, 0) }
+ {' '}
+ views
+
+
+
+
+
+
+
+
+
+
+
+ { this.state.stats.link_clicks.reduce((total, v) => total + v.count, 0) }
+ {' '}
+ clicks
+
+
+
+
+
+
+
+
+
+
+
+
+
+ { this.campaignTypes.map((key, count) =>
+
+ { key }
+ { count }
+
+ )}
+
+
+
+
+ }
+
+ );
+ }
}
export default Dashboard;
diff --git a/frontend/my/src/constants.js b/frontend/my/src/constants.js
index 11a42a6..1430593 100644
--- a/frontend/my/src/constants.js
+++ b/frontend/my/src/constants.js
@@ -52,6 +52,7 @@ export const SubscriptionStatusUnsubscribed = "unsubscribed"
// API routes.
export const Routes = {
+ GetDashboarcStats: "/api/dashboard/stats",
GetUsers: "/api/users",
GetLists: "/api/lists",
diff --git a/frontend/my/src/index.css b/frontend/my/src/index.css
index 91bfd67..d36f6fc 100644
--- a/frontend/my/src/index.css
+++ b/frontend/my/src/index.css
@@ -48,11 +48,14 @@ body {
}
.content-body {
- background: #fff;
- padding: 24px;
min-height: 90vh;
}
+section.content {
+ padding: 24px;
+ background: #fff;
+}
+
.logo {
padding: 30px;
}
@@ -69,10 +72,19 @@ body {
width: 20px;
}
+.ant-card-head-title {
+ font-size: .85em !important;
+ color: #999 !important;
+}
+
.broken {
margin: 100px;
}
+.hidden {
+ display: none;
+}
+
/* Form */
@@ -90,6 +102,14 @@ td .ant-tag {
margin-top: 5px;
}
+/* Dashboard */
+.dashboard {
+ margin: 24px;
+}
+ .dashboard .campaign-counts .name {
+ text-transform: capitalize;
+ }
+
/* Templates */
.wysiwyg {
padding: 30px;
diff --git a/main.go b/main.go
index 4071a89..21e5f23 100644
--- a/main.go
+++ b/main.go
@@ -80,6 +80,7 @@ func init() {
func registerHandlers(e *echo.Echo) {
e.GET("/", handleIndexPage)
e.GET("/api/config.js", handleGetConfigScript)
+ e.GET("/api/dashboard/stats", handleGetDashboardStats)
e.GET("/api/users", handleGetUsers)
e.POST("/api/users", handleCreateUser)
e.DELETE("/api/users/:id", handleDeleteUser)
diff --git a/queries.go b/queries.go
index e60f157..e83be4d 100644
--- a/queries.go
+++ b/queries.go
@@ -8,6 +8,8 @@ import (
// Queries contains all prepared SQL queries.
type Queries struct {
+ GetDashboardStats *sqlx.Stmt `query:"get-dashboard-stats"`
+
InsertSubscriber *sqlx.Stmt `query:"insert-subscriber"`
UpsertSubscriber *sqlx.Stmt `query:"upsert-subscriber"`
BlacklistSubscriber *sqlx.Stmt `query:"blacklist-subscriber"`
diff --git a/queries.sql b/queries.sql
index 2dd704f..c6335ca 100644
--- a/queries.sql
+++ b/queries.sql
@@ -416,22 +416,37 @@ INSERT INTO link_clicks (campaign_id, subscriber_id, link_id)
RETURNING (SELECT url FROM link);
--- -- name: get-stats
--- WITH lists AS (
--- SELECT type, COUNT(id) AS num FROM lists GROUP BY type
--- ),
--- subs AS (
--- SELECT status, COUNT(id) AS num FROM subscribers GROUP by status
--- ),
--- orphans AS (
--- SELECT COUNT(id) FROM subscribers LEFT JOIN subscriber_lists ON (subscribers.id = subscriber_lists.subscriber_id)
--- WHERE subscriber_lists.subscriber_id IS NULL
--- ),
--- camps AS (
--- SELECT status, COUNT(id) AS num FROM campaigns GROUP by status
--- )
--- SELECT JSON_BUILD_OBJECT('lists', lists);
--- row_to_json(t)
--- from (
--- select type, num from lists
--- ) t,
\ No newline at end of file
+-- name: get-dashboard-stats
+WITH lists AS (
+ SELECT JSON_OBJECT_AGG(type, num) FROM (SELECT type, COUNT(id) AS num FROM lists GROUP BY type) row
+),
+subs AS (
+ SELECT JSON_OBJECT_AGG(status, num) FROM (SELECT status, COUNT(id) AS num FROM subscribers GROUP by status) row
+),
+orphans AS (
+ SELECT COUNT(id) FROM subscribers LEFT JOIN subscriber_lists ON (subscribers.id = subscriber_lists.subscriber_id)
+ WHERE subscriber_lists.subscriber_id IS NULL
+),
+camps AS (
+ SELECT JSON_OBJECT_AGG(status, num) FROM (SELECT status, COUNT(id) AS num FROM campaigns GROUP by status) row
+),
+clicks AS (
+ -- Clicks by day for the last 3 months
+ SELECT JSON_AGG(ROW_TO_JSON(row))
+ FROM (SELECT COUNT(*) AS count, created_at::DATE as date
+ FROM link_clicks GROUP by date ORDER BY date DESC LIMIT 100
+ ) row
+),
+views AS (
+ -- Views by day for the last 3 months
+ SELECT JSON_AGG(ROW_TO_JSON(row))
+ FROM (SELECT COUNT(*) AS count, created_at::DATE as date
+ FROM campaign_views GROUP by date ORDER BY date DESC LIMIT 100
+ ) row
+)
+SELECT JSON_BUILD_OBJECT('lists', COALESCE((SELECT * FROM lists), '[]'),
+ 'subscribers', COALESCE((SELECT * FROM subs), '[]'),
+ 'orphan_subscribers', (SELECT * FROM orphans),
+ 'campaigns', COALESCE((SELECT * FROM camps), '[]'),
+ 'link_clicks', COALESCE((SELECT * FROM clicks), '[]'),
+ 'campaign_views', COALESCE((SELECT * FROM views), '[]')) AS stats;