diff --git a/frontend/app/analytics/page.tsx b/frontend/app/analytics/page.tsx new file mode 100644 index 0000000..b1fb8e8 --- /dev/null +++ b/frontend/app/analytics/page.tsx @@ -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 }) => ( +
+); + +const MetricCardSkeleton = () => ( +
+ + + +
+); + +const ChartSkeleton = () => ( +
+ + +
+); + +export default function AnalyticsPage() { + const [loading, setLoading] = useState(true); + const [data, setData] = useState(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 ( +
+
+
+
+

+ Analytics Dashboard +

+

+ Actionable insights into your notification performance +

+
+ +
+
+ + 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" + /> +
+
+ + 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" + /> +
+
+
+ +
+ {loading ? ( + Array.from({ length: 4 }).map((_, i) => ( + + )) + ) : ( + <> +
+

+ Total Notifications +

+

+ {data?.totalNotifications.toLocaleString()} +

+

+12.5% from last period

+
+ +
+

+ Delivered +

+

+ {data?.delivered.toLocaleString()} +

+
+

+ {data?.deliveryRate}% +

+

delivery rate

+
+
+ +
+

+ Acknowledged +

+

+ {data?.acknowledged.toLocaleString()} +

+
+

+ {data?.acknowledgmentRate}% +

+

acknowledgment rate

+
+
+ +
+

+ Failed Deliveries +

+

+ {data ? (data.totalNotifications - data.delivered).toLocaleString() : 0} +

+

-2.3% from last period

+
+ + )} +
+ +
+ {loading ? ( + + ) : ( +
+

+ Performance Trends +

+
+ +
+
+ )} +
+
+
+ ); +} diff --git a/frontend/app/globals.css b/frontend/app/globals.css new file mode 100644 index 0000000..aba323d --- /dev/null +++ b/frontend/app/globals.css @@ -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; + } +} diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx new file mode 100644 index 0000000..7122a38 --- /dev/null +++ b/frontend/app/layout.tsx @@ -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 ( + + + {children} + + + ); +} diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx new file mode 100644 index 0000000..f08f8dc --- /dev/null +++ b/frontend/app/page.tsx @@ -0,0 +1,18 @@ +import Link from "next/link"; + +export default function Home() { + return ( +
+
+

NotifyChain

+

Your analytics dashboard is ready!

+ + View Analytics Dashboard + +
+
+ ); +} diff --git a/frontend/next.config.js b/frontend/next.config.js new file mode 100644 index 0000000..658404a --- /dev/null +++ b/frontend/next.config.js @@ -0,0 +1,4 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = {}; + +module.exports = nextConfig; diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..e925c04 --- /dev/null +++ b/frontend/package.json @@ -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" + } +} diff --git a/frontend/postcss.config.mjs b/frontend/postcss.config.mjs new file mode 100644 index 0000000..1a69fd2 --- /dev/null +++ b/frontend/postcss.config.mjs @@ -0,0 +1,8 @@ +/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + tailwindcss: {}, + }, +}; + +export default config; diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts new file mode 100644 index 0000000..d43da91 --- /dev/null +++ b/frontend/tailwind.config.ts @@ -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; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..e7ff90f --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +}