Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
265 changes: 265 additions & 0 deletions frontend/app/analytics/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
"use client";

import { useState, useEffect } from "react";
import { Line } from "react-chartjs-2";
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
Filler,
} from "chart.js";
import { format, subDays } from "date-fns";

ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
Filler
);

interface AnalyticsData {
totalNotifications: number;
delivered: number;
acknowledged: number;
deliveryRate: number;
acknowledgmentRate: number;
trendData: {
dates: string[];
deliveryRates: number[];
acknowledgmentRates: number[];
};
}

const Skeleton = ({ className = "" }: { className?: string }) => (
<div
className={`animate-pulse bg-slate-200 rounded ${className}`}
></div>
);

const MetricCardSkeleton = () => (
<div className="bg-white p-6 rounded-xl shadow-sm border border-slate-100">
<Skeleton className="h-4 w-24 mb-2" />
<Skeleton className="h-10 w-32 mb-2" />
<Skeleton className="h-4 w-20" />
</div>
);

const ChartSkeleton = () => (
<div className="bg-white p-6 rounded-xl shadow-sm border border-slate-100">
<Skeleton className="h-6 w-48 mb-6" />
<Skeleton className="h-64 w-full rounded-lg" />
</div>
);

export default function AnalyticsPage() {
const [loading, setLoading] = useState(true);
const [data, setData] = useState<AnalyticsData | null>(null);
const [startDate, setStartDate] = useState(
format(subDays(new Date(), 30), "yyyy-MM-dd")
);
const [endDate, setEndDate] = useState(format(new Date(), "yyyy-MM-dd"));

const fetchAnalytics = async () => {
setLoading(true);
await new Promise((resolve) => setTimeout(resolve, 1200));

const dates: string[] = [];
const deliveryRates: number[] = [];
const acknowledgmentRates: number[] = [];

for (let i = 29; i >= 0; i--) {
const date = subDays(new Date(), i);
dates.push(format(date, "MMM d"));
deliveryRates.push(85 + Math.random() * 14);
acknowledgmentRates.push(60 + Math.random() * 25);
}

setData({
totalNotifications: 12847,
delivered: 12243,
acknowledged: 8721,
deliveryRate: 95.3,
acknowledgmentRate: 71.3,
trendData: {
dates,
deliveryRates,
acknowledgmentRates,
},
});

setLoading(false);
};

useEffect(() => {
fetchAnalytics();
}, [startDate, endDate]);

const chartData = {
labels: data?.trendData.dates || [],
datasets: [
{
label: "Delivery Rate",
data: data?.trendData.deliveryRates || [],
borderColor: "#4f46e5",
backgroundColor: "rgba(79, 70, 229, 0.1)",
fill: true,
tension: 0.4,
pointRadius: 3,
},
{
label: "Acknowledgment Rate",
data: data?.trendData.acknowledgmentRates || [],
borderColor: "#10b981",
backgroundColor: "rgba(16, 185, 129, 0.1)",
fill: true,
tension: 0.4,
pointRadius: 3,
},
],
};

const chartOptions = {
responsive: true,
maintainAspectRatio: true,
plugins: {
legend: {
position: "top" as const,
labels: {
padding: 20,
usePointStyle: true,
},
},
},
scales: {
y: {
beginAtZero: true,
max: 100,
ticks: {
callback: (value: any) => `${value}%`,
},
},
},
};

return (
<div className="min-h-screen bg-slate-50 p-4 md:p-8">
<div className="max-w-7xl mx-auto">
<div className="flex flex-col md:flex-row md:items-center justify-between mb-8 gap-4">
<div>
<h1 className="text-2xl md:text-3xl font-bold text-slate-800 mb-2">
Analytics Dashboard
</h1>
<p className="text-slate-500">
Actionable insights into your notification performance
</p>
</div>

<div className="flex flex-col sm:flex-row gap-3">
<div className="flex flex-col gap-1">
<label className="text-xs font-medium text-slate-500">Start Date</label>
<input
type="date"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
className="px-4 py-2 border border-slate-200 rounded-lg bg-white text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-xs font-medium text-slate-500">End Date</label>
<input
type="date"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
className="px-4 py-2 border border-slate-200 rounded-lg bg-white text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
</div>
</div>

<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
{loading ? (
Array.from({ length: 4 }).map((_, i) => (
<MetricCardSkeleton key={i} />
))
) : (
<>
<div className="bg-white p-6 rounded-xl shadow-sm border border-slate-100">
<p className="text-slate-500 text-sm font-medium mb-2">
Total Notifications
</p>
<p className="text-3xl font-bold text-slate-800 mb-1">
{data?.totalNotifications.toLocaleString()}
</p>
<p className="text-green-600 text-sm">+12.5% from last period</p>
</div>

<div className="bg-white p-6 rounded-xl shadow-sm border border-slate-100">
<p className="text-slate-500 text-sm font-medium mb-2">
Delivered
</p>
<p className="text-3xl font-bold text-slate-800 mb-1">
{data?.delivered.toLocaleString()}
</p>
<div className="flex items-center gap-2">
<p className="text-indigo-600 text-sm font-semibold">
{data?.deliveryRate}%
</p>
<p className="text-slate-400 text-sm">delivery rate</p>
</div>
</div>

<div className="bg-white p-6 rounded-xl shadow-sm border border-slate-100">
<p className="text-slate-500 text-sm font-medium mb-2">
Acknowledged
</p>
<p className="text-3xl font-bold text-slate-800 mb-1">
{data?.acknowledged.toLocaleString()}
</p>
<div className="flex items-center gap-2">
<p className="text-emerald-600 text-sm font-semibold">
{data?.acknowledgmentRate}%
</p>
<p className="text-slate-400 text-sm">acknowledgment rate</p>
</div>
</div>

<div className="bg-white p-6 rounded-xl shadow-sm border border-slate-100">
<p className="text-slate-500 text-sm font-medium mb-2">
Failed Deliveries
</p>
<p className="text-3xl font-bold text-slate-800 mb-1">
{data ? (data.totalNotifications - data.delivered).toLocaleString() : 0}
</p>
<p className="text-red-600 text-sm">-2.3% from last period</p>
</div>
</>
)}
</div>

<div className="grid grid-cols-1 gap-4">
{loading ? (
<ChartSkeleton />
) : (
<div className="bg-white p-6 rounded-xl shadow-sm border border-slate-100">
<h2 className="text-lg font-semibold text-slate-800 mb-6">
Performance Trends
</h2>
<div className="h-64 md:h-80">
<Line data={chartData} options={chartOptions} />
</div>
</div>
)}
</div>
</div>
</div>
);
}
20 changes: 20 additions & 0 deletions frontend/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
--background: #f8fafc;
--foreground: #0f172a;
}

body {
color: var(--foreground);
background: var(--background);
font-family: Arial, Helvetica, sans-serif;
}

@layer utilities {
.text-balance {
text-wrap: balance;
}
}
21 changes: 21 additions & 0 deletions frontend/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { Metadata } from "next";
import "./globals.css";

export const metadata: Metadata = {
title: "NotifyChain - Analytics Dashboard",
description: "Actionable insights into notification performance",
};

export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className="antialiased">
{children}
</body>
</html>
);
}
18 changes: 18 additions & 0 deletions frontend/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Link from "next/link";

export default function Home() {
return (
<div className="min-h-screen bg-slate-50 flex items-center justify-center">
<div className="text-center max-w-md p-8">
<h1 className="text-4xl font-bold mb-4 text-slate-800">NotifyChain</h1>
<p className="text-slate-600 mb-8">Your analytics dashboard is ready!</p>
<Link
href="/analytics"
className="inline-flex items-center justify-center px-8 py-3 border border-transparent text-base font-medium rounded-full text-white bg-indigo-600 hover:bg-indigo-700 transition-all"
>
View Analytics Dashboard
</Link>
</div>
</div>
);
}
4 changes: 4 additions & 0 deletions frontend/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};

module.exports = nextConfig;
29 changes: 29 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "notify-chain-frontend",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1",
"next": "14.2.14",
"chart.js": "^4.4.1",
"react-chartjs-2": "^5.2.0",
"date-fns": "^3.6.0"
},
"devDependencies": {
"typescript": "^5",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"eslint": "^8",
"eslint-config-next": "14.2.14"
}
}
8 changes: 8 additions & 0 deletions frontend/postcss.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};

export default config;
19 changes: 19 additions & 0 deletions frontend/tailwind.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { Config } from "tailwindcss";

const config: Config = {
content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
colors: {
background: "var(--background)",
foreground: "var(--foreground)",
},
},
},
plugins: [],
};
export default config;
Loading
Loading