@ossy/analytics 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ossy/analytics",
3
3
  "description": "Analytics module — workspace analytics for website traffic",
4
- "version": "1.1.0",
4
+ "version": "1.2.0",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
7
7
  "module": "./src/index.js",
@@ -21,5 +21,10 @@
21
21
  "/src",
22
22
  "README.md"
23
23
  ],
24
- "gitHead": "ccbb5575213d2703e3cad5608b85c3478fc421bc"
24
+ "peerDependencies": {
25
+ "@ossy/design-system": "*",
26
+ "@ossy/sdk-react": "*",
27
+ "react": "*"
28
+ },
29
+ "gitHead": "08b323d32d21600ba9519ad3f73fd5738556a68f"
25
30
  }
@@ -0,0 +1,112 @@
1
+ import React, { useMemo } from 'react'
2
+ import { useWorkspace } from '@ossy/sdk-react'
3
+ import { Definition } from '@ossy/analytics'
4
+ import { Text, Title, View } from '@ossy/design-system'
5
+ import { usePageViewStats } from './usePageViewStats.js'
6
+
7
+ export const metadata = {
8
+ id: 'analytics/home',
9
+ title: 'Analytics',
10
+ path: {
11
+ sv: '/analys',
12
+ en: '/analytics',
13
+ },
14
+ }
15
+
16
+ function SummaryCard({ label, value }) {
17
+ return (
18
+ <View
19
+ surface="primary"
20
+ roundness="m"
21
+ inset="m"
22
+ style={{ border: '1px solid var(--separator)' }}
23
+ >
24
+ <Text variant="small" style={{ opacity: 0.75 }}>{label}</Text>
25
+ <Title variant="secondary">{value}</Title>
26
+ </View>
27
+ )
28
+ }
29
+
30
+ export default function AnalyticsPage() {
31
+ const { workspace } = useWorkspace()
32
+ // Snapshot once per mount: Date.now() on every render changed `from`/`to` every tick and
33
+ // retriggered usePageViewStats (params → useEffect) in a fetch/setState loop.
34
+ const { from, to } = useMemo(() => {
35
+ const t = Date.now()
36
+ return { from: t - 30 * 24 * 60 * 60 * 1000, to: t }
37
+ }, [])
38
+
39
+ const { status, totalViews, dailyViews, topPaths } = usePageViewStats({
40
+ workspaceId: workspace?.id,
41
+ from,
42
+ to,
43
+ limit: 10,
44
+ })
45
+
46
+ return (
47
+ <View inset="l" gap="l" style={{ width: '100%', boxSizing: 'border-box' }}>
48
+ <View gap="s">
49
+ <Title>{Definition.title}</Title>
50
+ <Text>{Definition.description}</Text>
51
+ </View>
52
+
53
+ {status === 'loading' && (
54
+ <View surface="primary" roundness="m" inset="m">
55
+ <Text>Loading analytics...</Text>
56
+ </View>
57
+ )}
58
+
59
+ {status === 'error' && (
60
+ <View surface="primary" roundness="m" inset="m">
61
+ <Text>Could not load analytics data.</Text>
62
+ </View>
63
+ )}
64
+
65
+ {status === 'success' && (
66
+ <>
67
+ <View layout="row" gap="m">
68
+ <SummaryCard label="Total page views (30 days)" value={`${totalViews}`} />
69
+ <SummaryCard label="Tracked pages (30 days)" value={`${topPaths.length}`} />
70
+ <SummaryCard label="Days with traffic" value={`${dailyViews.length}`} />
71
+ </View>
72
+
73
+ <View layout="row" gap="m" style={{ alignItems: 'flex-start' }}>
74
+ <View
75
+ surface="primary"
76
+ roundness="m"
77
+ inset="m"
78
+ gap="s"
79
+ style={{ border: '1px solid var(--separator)', flex: 1 }}
80
+ >
81
+ <Title variant="secondary">Top paths</Title>
82
+ {topPaths.length === 0 && <Text variant="small">No page views yet.</Text>}
83
+ {topPaths.map((item) => (
84
+ <View key={item.path || 'unknown'} layout="row" justifyContent="space-between">
85
+ <Text variant="small">{item.path || '(unknown path)'}</Text>
86
+ <Text variant="small">{item.views}</Text>
87
+ </View>
88
+ ))}
89
+ </View>
90
+
91
+ <View
92
+ surface="primary"
93
+ roundness="m"
94
+ inset="m"
95
+ gap="s"
96
+ style={{ border: '1px solid var(--separator)', flex: 1 }}
97
+ >
98
+ <Title variant="secondary">Daily page views</Title>
99
+ {dailyViews.length === 0 && <Text variant="small">No daily data yet.</Text>}
100
+ {dailyViews.map((item) => (
101
+ <View key={item.date} layout="row" justifyContent="space-between">
102
+ <Text variant="small">{item.date}</Text>
103
+ <Text variant="small">{item.views}</Text>
104
+ </View>
105
+ ))}
106
+ </View>
107
+ </View>
108
+ </>
109
+ )}
110
+ </View>
111
+ )
112
+ }
@@ -0,0 +1,52 @@
1
+ import { useEffect, useMemo, useState } from 'react'
2
+
3
+ const API_URL = '/@ossy'
4
+
5
+ export function usePageViewStats({ workspaceId, from, to, limit = 10 }) {
6
+ const [status, setStatus] = useState('loading')
7
+ const [data, setData] = useState({
8
+ totalViews: 0,
9
+ dailyViews: [],
10
+ topPaths: [],
11
+ })
12
+
13
+ const params = useMemo(() => {
14
+ const sp = new URLSearchParams()
15
+ if (from) sp.set('from', `${from}`)
16
+ if (to) sp.set('to', `${to}`)
17
+ if (limit) sp.set('limit', `${limit}`)
18
+ return sp.toString()
19
+ }, [from, to, limit])
20
+
21
+ useEffect(() => {
22
+ if (!workspaceId) return
23
+
24
+ setStatus('loading')
25
+ fetch(`${API_URL}/resources/page-views/stats?${params}`, {
26
+ method: 'GET',
27
+ headers: {
28
+ workspaceId,
29
+ },
30
+ })
31
+ .then(res => {
32
+ if (!res.ok) return Promise.reject(new Error('Failed to fetch stats'))
33
+ return res.json()
34
+ })
35
+ .then(json => {
36
+ setData({
37
+ totalViews: json?.totalViews || 0,
38
+ dailyViews: json?.dailyViews || [],
39
+ topPaths: json?.topPaths || [],
40
+ })
41
+ setStatus('success')
42
+ })
43
+ .catch(() => {
44
+ setStatus('error')
45
+ })
46
+ }, [workspaceId, params])
47
+
48
+ return {
49
+ status,
50
+ ...data,
51
+ }
52
+ }