@notis_ai/cli 0.2.5 → 0.2.7
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/README.md +33 -9
- package/dist/scaffolds/notis-affiliate-prospects/CHANGELOG.md +7 -0
- package/dist/scaffolds/notis-affiliate-prospects/README.md +20 -0
- package/dist/scaffolds/notis-affiliate-prospects/app/globals.css +42 -0
- package/dist/scaffolds/notis-affiliate-prospects/app/layout.tsx +7 -0
- package/dist/scaffolds/notis-affiliate-prospects/app/page.tsx +248 -0
- package/dist/scaffolds/notis-affiliate-prospects/app/prospects/page.tsx +134 -0
- package/dist/scaffolds/notis-affiliate-prospects/app/segments/page.tsx +108 -0
- package/dist/scaffolds/notis-affiliate-prospects/components/ui/card.tsx +35 -0
- package/dist/scaffolds/notis-affiliate-prospects/components.json +20 -0
- package/dist/scaffolds/notis-affiliate-prospects/index.html +12 -0
- package/dist/scaffolds/notis-affiliate-prospects/lib/affiliate-data.ts +218 -0
- package/dist/scaffolds/notis-affiliate-prospects/lib/demo-prospects.ts +52 -0
- package/dist/scaffolds/notis-affiliate-prospects/lib/notis-tools.ts +100 -0
- package/dist/scaffolds/notis-affiliate-prospects/lib/utils.ts +25 -0
- package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-fixtures.json +187 -0
- package/dist/scaffolds/notis-affiliate-prospects/notis.config.ts +65 -0
- package/dist/scaffolds/notis-affiliate-prospects/package.json +32 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/package.json +32 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/components/MultiSelectActionBar.tsx +273 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/config.ts +90 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useBackend.ts +41 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useMultiSelect.ts +503 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useNotis.ts +34 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useTool.ts +64 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useTools.ts +56 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useUpsertDocument.ts +50 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/index.ts +54 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/provider.tsx +43 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/runtime.ts +161 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/styles.css +38 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/ui.ts +15 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/vite.ts +56 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/tsconfig.json +15 -0
- package/dist/scaffolds/notis-affiliate-prospects/postcss.config.mjs +6 -0
- package/dist/scaffolds/notis-affiliate-prospects/src/dev-main.tsx +72 -0
- package/dist/scaffolds/notis-affiliate-prospects/src/mock-runtime.ts +242 -0
- package/dist/scaffolds/notis-affiliate-prospects/tailwind.config.ts +51 -0
- package/dist/scaffolds/notis-affiliate-prospects/tsconfig.json +23 -0
- package/dist/scaffolds/notis-affiliate-prospects/vite.config.ts +11 -0
- package/dist/scaffolds/notis-database/CHANGELOG.md +5 -0
- package/dist/scaffolds/notis-database/notis.config.ts +0 -1
- package/dist/scaffolds/notis-database/src/mock-runtime.ts +1 -0
- package/dist/scaffolds/notis-journal/CHANGELOG.md +25 -0
- package/dist/scaffolds/notis-journal/app/globals.css +37 -0
- package/dist/scaffolds/notis-journal/app/insights/page.tsx +513 -0
- package/dist/scaffolds/notis-journal/app/journal-core.tsx +362 -0
- package/dist/scaffolds/notis-journal/app/journal-ui.tsx +337 -0
- package/dist/scaffolds/notis-journal/app/layout.tsx +6 -0
- package/dist/scaffolds/notis-journal/app/page.tsx +485 -0
- package/dist/scaffolds/notis-journal/components/ui/badge.tsx +28 -0
- package/dist/scaffolds/notis-journal/components/ui/button.tsx +53 -0
- package/dist/scaffolds/notis-journal/components/ui/card.tsx +56 -0
- package/dist/scaffolds/notis-journal/components.json +20 -0
- package/dist/scaffolds/notis-journal/index.html +12 -0
- package/dist/scaffolds/notis-journal/lib/utils.ts +6 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-4.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-5.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-6.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-fixtures.json +132 -0
- package/dist/scaffolds/notis-journal/notis.config.ts +93 -0
- package/dist/scaffolds/notis-journal/package.json +34 -0
- package/dist/scaffolds/notis-journal/packages/sdk/package.json +36 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/DocumentEditor.tsx +93 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/Markdown.tsx +60 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectActionBar.tsx +278 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/config.ts +145 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/documents.ts +229 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useBackend.ts +41 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocument.ts +78 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocuments.ts +121 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useMultiSelect.ts +539 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotis.ts +34 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTool.ts +64 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTools.ts +56 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/index.ts +83 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/provider.tsx +43 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/runtime.ts +220 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/styles.css +186 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/ui.ts +15 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/vite.ts +56 -0
- package/dist/scaffolds/notis-journal/packages/sdk/tsconfig.json +15 -0
- package/dist/scaffolds/notis-journal/postcss.config.mjs +8 -0
- package/dist/scaffolds/notis-journal/skills/journal-onboarding/SKILL.md +120 -0
- package/dist/scaffolds/notis-journal/src/dev-main.tsx +58 -0
- package/dist/scaffolds/notis-journal/src/mock-runtime.ts +197 -0
- package/dist/scaffolds/notis-journal/tailwind.config.ts +58 -0
- package/dist/scaffolds/notis-journal/tsconfig.json +23 -0
- package/dist/scaffolds/notis-journal/vite.config.ts +10 -0
- package/dist/scaffolds/notis-notes/CHANGELOG.md +5 -0
- package/dist/scaffolds/notis-notes/app/page.tsx +17 -373
- package/dist/scaffolds/notis-notes/notis.config.ts +0 -1
- package/dist/scaffolds/notis-random/CHANGELOG.md +5 -0
- package/dist/scaffolds/notis-random/notis.config.ts +0 -1
- package/dist/scaffolds/notis-random/src/mock-runtime.ts +1 -0
- package/dist/scaffolds.json +22 -0
- package/package.json +3 -3
- package/skills/notis-apps/SKILL.md +43 -5
- package/skills/notis-apps/cli.md +22 -6
- package/skills/notis-cli/SKILL.md +20 -2
- package/skills/notis-query/cli.md +1 -1
- package/src/cli.js +8 -1
- package/src/command-specs/apps.js +307 -36
- package/src/command-specs/diagnostics.js +674 -0
- package/src/command-specs/helpers.js +4 -1
- package/src/command-specs/index.js +7 -1
- package/src/command-specs/meta.js +8 -2
- package/src/command-specs/smoke.js +386 -0
- package/src/command-specs/tools.js +237 -44
- package/src/runtime/agent-browser.js +204 -21
- package/src/runtime/app-changelog.js +79 -0
- package/src/runtime/app-dev-server.js +21 -4
- package/src/runtime/app-dev-sessions.js +99 -1
- package/src/runtime/app-platform.js +197 -18
- package/src/runtime/assets/store-screenshot-dark.png +0 -0
- package/src/runtime/desktop-auth.js +93 -0
- package/src/runtime/output.js +12 -1
- package/src/runtime/profiles.js +72 -16
- package/src/runtime/store-screenshot.js +138 -0
- package/src/runtime/transport.js +24 -82
- package/template/.harness/index.html.tmpl +128 -6
- package/template/CHANGELOG.md +5 -0
- package/template/app/page.tsx +11 -41
- package/template/notis.config.ts +0 -1
- package/template/package-lock.json +4137 -0
- package/template/package.json +1 -0
- package/template/packages/sdk/package.json +4 -0
- package/template/packages/sdk/src/components/DocumentEditor.tsx +93 -0
- package/template/packages/sdk/src/components/Markdown.tsx +60 -0
- package/template/packages/sdk/src/components/MultiSelectActionBar.tsx +7 -2
- package/template/packages/sdk/src/config.ts +74 -0
- package/template/packages/sdk/src/documents.ts +229 -0
- package/template/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
- package/template/packages/sdk/src/hooks/useDocument.ts +78 -0
- package/template/packages/sdk/src/hooks/useDocuments.ts +121 -0
- package/template/packages/sdk/src/hooks/useMultiSelect.ts +38 -2
- package/template/packages/sdk/src/hooks/useUpsertDocument.ts +54 -9
- package/template/packages/sdk/src/index.ts +30 -1
- package/template/packages/sdk/src/runtime.ts +76 -17
- package/template/packages/sdk/src/styles.css +148 -0
- /package/{template → dist/scaffolds/notis-affiliate-prospects}/packages/sdk/src/hooks/useDatabase.ts +0 -0
package/README.md
CHANGED
|
@@ -105,9 +105,9 @@ Examples:
|
|
|
105
105
|
|
|
106
106
|
### `npx --package @notis_ai/cli@latest -- notis apps verify [dir]`
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
Validate every route and the production Store listing contract.
|
|
109
109
|
|
|
110
|
-
When to use: After notis apps
|
|
110
|
+
When to use: After notis apps screenshot, before deploy. Catches render-time crashes, missing runtime calls, and incomplete listing media that the build step cannot detect.
|
|
111
111
|
|
|
112
112
|
Options:
|
|
113
113
|
- `--routes <slugs>` — Comma-separated route slugs. Default: every route in manifest.
|
|
@@ -125,23 +125,25 @@ Examples:
|
|
|
125
125
|
|
|
126
126
|
### `npx --package @notis_ai/cli@latest -- notis apps screenshot [dir]`
|
|
127
127
|
|
|
128
|
-
Capture listing
|
|
128
|
+
Capture configured listing route/scenario states via the headless harness.
|
|
129
129
|
|
|
130
|
-
When to use: Generate metadata/screenshot-N.png for the App Store listing. Apps are icon-led (like Raycast) — there is no cover image, only these screenshots. Run before notis apps verify / deploy / publish.
|
|
130
|
+
When to use: Generate the 3–6 declared metadata/screenshot-N.png files for the App Store listing. Apps are icon-led (like Raycast) — there is no cover image, only these screenshots. Each screenshot may set a focus selector to remove empty canvas and a light or dark theme that also controls its Store frame. Run before notis apps verify / deploy / publish.
|
|
131
131
|
|
|
132
132
|
Options:
|
|
133
|
-
- `--routes <slugs>` — Comma-separated route slugs. Default: every
|
|
133
|
+
- `--routes <slugs>` — Comma-separated route slugs. Default: every configured screenshot state.
|
|
134
134
|
- `--port <n>` — Loopback port. Default: auto-pick.
|
|
135
135
|
- `--width <px>` — Viewport width. Default: 2000.
|
|
136
136
|
- `--height <px>` — Viewport height. Default: 1250 (16:10).
|
|
137
137
|
- `--output-dir <dir>` — Where to write screenshot-N.png. Default: metadata/.
|
|
138
138
|
- `--mode <mode>` — stub | live. Default stub. Live renders against real data via the CLI JWT (requires a linked app), so screenshots show actual content instead of empty states.
|
|
139
|
+
- `--raw` — Write the unframed harness capture instead of the default Store presentation.
|
|
139
140
|
- `--skip-build` — Skip notis apps build; reuse existing .notis/output/.
|
|
140
141
|
|
|
141
142
|
Examples:
|
|
142
|
-
- `npx --package @notis_ai/cli@latest -- notis apps screenshot`
|
|
143
|
+
- `npx --package @notis_ai/cli@latest -- notis apps screenshot # honors notis.config.ts screenshot scenarios`
|
|
143
144
|
- `npx --package @notis_ai/cli@latest -- notis apps screenshot --routes home,history`
|
|
144
145
|
- `npx --package @notis_ai/cli@latest -- notis apps screenshot --mode live # populated screenshots from real data`
|
|
146
|
+
- `npx --package @notis_ai/cli@latest -- notis apps screenshot --raw # diagnostic capture without Store framing`
|
|
145
147
|
|
|
146
148
|
### `npx --package @notis_ai/cli@latest -- notis apps link <app-id> [dir]`
|
|
147
149
|
|
|
@@ -184,6 +186,20 @@ Examples:
|
|
|
184
186
|
- `npx --package @notis_ai/cli@latest -- notis apps deploy --app-id abc123`
|
|
185
187
|
- `npx --package @notis_ai/cli@latest -- notis apps deploy --direct`
|
|
186
188
|
|
|
189
|
+
### `npx --package @notis_ai/cli@latest -- notis apps publish [dir]`
|
|
190
|
+
|
|
191
|
+
Submit the deployed app for Store review.
|
|
192
|
+
|
|
193
|
+
When to use: After the user explicitly confirms the App Details page and Store listing are ready. Requires the current local project to match the latest deployed version.
|
|
194
|
+
|
|
195
|
+
Options:
|
|
196
|
+
- `--app-id <id>` — Override linked app ID.
|
|
197
|
+
- `--confirm-ready` — Confirm the user approved the current App Details page for Store submission.
|
|
198
|
+
|
|
199
|
+
Examples:
|
|
200
|
+
- `npx --package @notis_ai/cli@latest -- notis apps publish --confirm-ready`
|
|
201
|
+
- `npx --package @notis_ai/cli@latest -- notis apps publish ./my-app --confirm-ready`
|
|
202
|
+
|
|
187
203
|
### `npx --package @notis_ai/cli@latest -- notis apps doctor [dir]`
|
|
188
204
|
|
|
189
205
|
Check project health and readiness.
|
|
@@ -238,6 +254,7 @@ When to use: Use this as the escape hatch for integrations or Notis tools withou
|
|
|
238
254
|
|
|
239
255
|
Options:
|
|
240
256
|
- `--arguments <json>` — JSON object, @file path, or - for stdin.
|
|
257
|
+
- `--arguments-file <path>` — Read the JSON arguments object from a file.
|
|
241
258
|
- `--file <argument-path=local-path>` — Upload a local file into a file-uploadable tool argument. Repeatable.
|
|
242
259
|
- `--get-schema` — Display the tool parameter schema without executing.
|
|
243
260
|
- `--dry-run` — Validate arguments against the tool schema without executing.
|
|
@@ -248,6 +265,7 @@ Examples:
|
|
|
248
265
|
- `npx --package @notis_ai/cli@latest -- notis tools exec LOCAL_NOTIS_DATABASE_QUERY --get-schema`
|
|
249
266
|
- `npx --package @notis_ai/cli@latest -- notis tools exec LOCAL_NOTIS_DATABASE_QUERY --dry-run --arguments '{"database_slug":"tasks","query":{}}'`
|
|
250
267
|
- `npx --package @notis_ai/cli@latest -- notis tools exec LOCAL_NOTIS_DATABASE_QUERY --arguments @query.json`
|
|
268
|
+
- `npx --package @notis_ai/cli@latest -- notis tools exec LOCAL_NOTIS_DATABASE_QUERY --arguments-file query.json`
|
|
251
269
|
- `npx --package @notis_ai/cli@latest -- notis tools exec LOCAL_NOTIS_DATABASE_QUERY --arguments - < query.json`
|
|
252
270
|
- `npx --package @notis_ai/cli@latest -- notis tools exec composio-dropbox-upload_file --arguments '{"path":"/target/in/dropbox.pdf"}' --file content=./Invoice.pdf`
|
|
253
271
|
|
|
@@ -265,13 +283,19 @@ Examples:
|
|
|
265
283
|
|
|
266
284
|
### `npx --package @notis_ai/cli@latest -- notis tools link <toolkit>`
|
|
267
285
|
|
|
268
|
-
|
|
286
|
+
Connect or reconnect an integration toolkit.
|
|
269
287
|
|
|
270
|
-
When to use: Use this when a tool requires authentication
|
|
288
|
+
When to use: Use this when a tool requires authentication or an active connection must be replaced.
|
|
289
|
+
|
|
290
|
+
Options:
|
|
291
|
+
- `--reconnect` — Replace the existing account instead of adding another connection.
|
|
292
|
+
- `--connection-id <id>` — Exact connection id to replace when multiple accounts exist.
|
|
293
|
+
- `--label <label>` — Account label for a new or replacement connection.
|
|
294
|
+
- `--credentials <json>` — Credential JSON object, @file path, or - for stdin. Prefer stdin so secrets do not enter shell history.
|
|
271
295
|
|
|
272
296
|
Examples:
|
|
273
297
|
- `npx --package @notis_ai/cli@latest -- notis tools link github`
|
|
274
|
-
- `npx --package @notis_ai/cli@latest -- notis tools link
|
|
298
|
+
- `npx --package @notis_ai/cli@latest -- notis tools link dataforseo --reconnect --credentials - < credentials.json`
|
|
275
299
|
|
|
276
300
|
|
|
277
301
|
## Meta Commands
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [Affiliate recruiting control center] - 2026-07-23
|
|
4
|
+
|
|
5
|
+
- Added the campaign dashboard, measurable affiliate pipeline, and priority queue.
|
|
6
|
+
- Added responsive prospect and segment views.
|
|
7
|
+
- Added local fixtures for every recruiting segment so the app can be tested before installation.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Affiliate Prospects
|
|
2
|
+
|
|
3
|
+
Local-first Notis app for monitoring the affiliate recruiting pipeline.
|
|
4
|
+
|
|
5
|
+
The app is intentionally a read-only campaign surface. Notis automations and Codex workers own
|
|
6
|
+
discovery, enrichment, outreach preparation, analytics reconciliation, and record updates. This
|
|
7
|
+
keeps one source of truth while avoiding business logic in the UI.
|
|
8
|
+
|
|
9
|
+
## Routes
|
|
10
|
+
|
|
11
|
+
- Dashboard: KPIs, pipeline, priority queue, and segment coverage.
|
|
12
|
+
- Prospects: responsive reference table for every lead and next action.
|
|
13
|
+
- Segments: scorecards linked to the source-controlled segment playbooks.
|
|
14
|
+
|
|
15
|
+
## Data
|
|
16
|
+
|
|
17
|
+
The deployed app references `affiliate_prospects_1` and `affiliate_outreach_events_1`. Their
|
|
18
|
+
canonical schema is documented in `campaign/affiliate-network/data-schema.md`. During local
|
|
19
|
+
development the app uses deterministic mock prospects so it can be reviewed before any workspace
|
|
20
|
+
installation.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
.affiliate-app-shell {
|
|
6
|
+
container-type: inline-size;
|
|
7
|
+
padding: clamp(1rem, 2.5cqi, 2rem);
|
|
8
|
+
max-width: 1440px;
|
|
9
|
+
margin-inline: auto;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.affiliate-stat-grid {
|
|
13
|
+
display: grid;
|
|
14
|
+
grid-template-columns: repeat(auto-fit, minmax(min(180px, 100%), 1fr));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.affiliate-dashboard-grid {
|
|
18
|
+
display: grid;
|
|
19
|
+
grid-template-columns: minmax(0, 1fr);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@container (min-width: 940px) {
|
|
23
|
+
.affiliate-dashboard-grid {
|
|
24
|
+
grid-template-columns: minmax(0, 1.55fr) minmax(300px, 0.75fr);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.affiliate-data-table {
|
|
29
|
+
min-width: 900px;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@media (max-width: 767px) {
|
|
33
|
+
.affiliate-desktop-table {
|
|
34
|
+
display: none;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@media (min-width: 768px) {
|
|
39
|
+
.affiliate-mobile-cards {
|
|
40
|
+
display: none;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
4
|
+
import { useNotisNavigation, useTopBarSearch } from '@notis/sdk';
|
|
5
|
+
import {
|
|
6
|
+
ArrowClockwiseIcon,
|
|
7
|
+
ArrowRightIcon,
|
|
8
|
+
CheckCircleIcon,
|
|
9
|
+
CurrencyDollarIcon,
|
|
10
|
+
FunnelIcon,
|
|
11
|
+
PaperPlaneTiltIcon,
|
|
12
|
+
SparkleIcon,
|
|
13
|
+
UsersThreeIcon,
|
|
14
|
+
} from '@phosphor-icons/react';
|
|
15
|
+
|
|
16
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
17
|
+
import {
|
|
18
|
+
PIPELINE,
|
|
19
|
+
SEGMENTS,
|
|
20
|
+
compactNumber,
|
|
21
|
+
money,
|
|
22
|
+
statusTone,
|
|
23
|
+
type AffiliateProspect,
|
|
24
|
+
} from '@/lib/affiliate-data';
|
|
25
|
+
import { useAffiliateProspects } from '@/lib/notis-tools';
|
|
26
|
+
|
|
27
|
+
export default function DashboardPage() {
|
|
28
|
+
const { prospects, loading, error, refetch } = useAffiliateProspects();
|
|
29
|
+
const navigation = useNotisNavigation();
|
|
30
|
+
const [search, setSearch] = useState('');
|
|
31
|
+
const { setLoading } = useTopBarSearch({
|
|
32
|
+
value: search,
|
|
33
|
+
onChange: setSearch,
|
|
34
|
+
placeholder: 'Filter affiliate prospects…',
|
|
35
|
+
});
|
|
36
|
+
useEffect(() => setLoading(loading), [loading, setLoading]);
|
|
37
|
+
|
|
38
|
+
const filtered = useMemo(() => {
|
|
39
|
+
const query = search.trim().toLowerCase();
|
|
40
|
+
if (!query) return prospects;
|
|
41
|
+
return prospects.filter((prospect) =>
|
|
42
|
+
[prospect.name, prospect.company, prospect.segment, prospect.status, prospect.source]
|
|
43
|
+
.join(' ')
|
|
44
|
+
.toLowerCase()
|
|
45
|
+
.includes(query),
|
|
46
|
+
);
|
|
47
|
+
}, [prospects, search]);
|
|
48
|
+
|
|
49
|
+
const metrics = useMemo(() => {
|
|
50
|
+
const qualified = prospects.filter((p) => p.fitScore >= 75 && p.status !== 'Disqualified').length;
|
|
51
|
+
const active = prospects.filter((p) => p.status === 'Activated').length;
|
|
52
|
+
const conversations = prospects.filter((p) =>
|
|
53
|
+
['Replied', 'Interested', 'Applied', 'Activated'].includes(p.status),
|
|
54
|
+
).length;
|
|
55
|
+
const mrr = prospects.reduce((sum, p) => sum + p.attributedMrr, 0);
|
|
56
|
+
return { qualified, active, conversations, mrr };
|
|
57
|
+
}, [prospects]);
|
|
58
|
+
|
|
59
|
+
const priority = useMemo(
|
|
60
|
+
() =>
|
|
61
|
+
[...filtered]
|
|
62
|
+
.filter((p) => !p.optedOut && !['Activated', 'Disqualified'].includes(p.status))
|
|
63
|
+
.sort((a, b) => b.fitScore - a.fitScore)
|
|
64
|
+
.slice(0, 6),
|
|
65
|
+
[filtered],
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const segmentRows = useMemo(
|
|
69
|
+
() =>
|
|
70
|
+
SEGMENTS.map((segment) => {
|
|
71
|
+
const rows = prospects.filter((p) => p.segment === segment.name);
|
|
72
|
+
const engaged = rows.filter((p) => ['Replied', 'Interested', 'Applied', 'Activated'].includes(p.status)).length;
|
|
73
|
+
return { ...segment, total: rows.length, engaged };
|
|
74
|
+
}).sort((a, b) => b.total - a.total),
|
|
75
|
+
[prospects],
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<main data-store-screenshot="dashboard" className="affiliate-app-shell space-y-6">
|
|
80
|
+
<header className="flex flex-wrap items-start justify-between gap-4">
|
|
81
|
+
<div>
|
|
82
|
+
<div className="flex items-center gap-2 text-xs font-medium text-muted-foreground">
|
|
83
|
+
<SparkleIcon size={14} weight="fill" />
|
|
84
|
+
Affiliate recruiting control center
|
|
85
|
+
</div>
|
|
86
|
+
<h1 className="mt-1 text-2xl font-semibold tracking-tight">Campaign dashboard</h1>
|
|
87
|
+
<p className="mt-1 max-w-2xl text-sm text-muted-foreground">
|
|
88
|
+
One place to source, qualify, contact, and activate affiliates—while Notis watches the
|
|
89
|
+
pipeline and local Codex workers do the heavy research.
|
|
90
|
+
</p>
|
|
91
|
+
</div>
|
|
92
|
+
<button
|
|
93
|
+
type="button"
|
|
94
|
+
onClick={refetch}
|
|
95
|
+
className="inline-flex min-h-11 items-center gap-2 rounded-md border border-border bg-background px-3 text-sm text-muted-foreground transition hover:bg-muted hover:text-foreground"
|
|
96
|
+
>
|
|
97
|
+
<ArrowClockwiseIcon className={loading ? 'animate-spin' : ''} size={16} />
|
|
98
|
+
Refresh
|
|
99
|
+
</button>
|
|
100
|
+
</header>
|
|
101
|
+
|
|
102
|
+
{error ? (
|
|
103
|
+
<div className="rounded-lg border border-destructive/20 bg-destructive/10 px-4 py-3 text-sm text-destructive">
|
|
104
|
+
{error.message}
|
|
105
|
+
</div>
|
|
106
|
+
) : null}
|
|
107
|
+
|
|
108
|
+
<section className="affiliate-stat-grid gap-3">
|
|
109
|
+
<Metric icon={UsersThreeIcon} label="Prospects" value={compactNumber(prospects.length)} />
|
|
110
|
+
<Metric icon={FunnelIcon} label="Qualified" value={compactNumber(metrics.qualified)} />
|
|
111
|
+
<Metric icon={PaperPlaneTiltIcon} label="Conversations" value={compactNumber(metrics.conversations)} />
|
|
112
|
+
<Metric icon={CheckCircleIcon} label="Activated" value={compactNumber(metrics.active)} />
|
|
113
|
+
<Metric icon={CurrencyDollarIcon} label="Attributed MRR" value={money(metrics.mrr)} />
|
|
114
|
+
</section>
|
|
115
|
+
|
|
116
|
+
<section className="affiliate-dashboard-grid gap-4">
|
|
117
|
+
<Card>
|
|
118
|
+
<CardHeader className="flex-row items-center justify-between">
|
|
119
|
+
<div>
|
|
120
|
+
<CardTitle>Priority queue</CardTitle>
|
|
121
|
+
<p className="mt-1 text-xs text-muted-foreground">Highest-fit prospects needing a next action.</p>
|
|
122
|
+
</div>
|
|
123
|
+
<button
|
|
124
|
+
type="button"
|
|
125
|
+
onClick={() => navigation.toRoute('/prospects')}
|
|
126
|
+
className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
|
|
127
|
+
>
|
|
128
|
+
All prospects <ArrowRightIcon size={13} />
|
|
129
|
+
</button>
|
|
130
|
+
</CardHeader>
|
|
131
|
+
<CardContent>
|
|
132
|
+
{loading && !prospects.length ? (
|
|
133
|
+
<p className="py-8 text-center text-sm text-muted-foreground">Loading the pipeline…</p>
|
|
134
|
+
) : priority.length ? (
|
|
135
|
+
<div className="divide-y divide-border">
|
|
136
|
+
{priority.map((prospect) => <PriorityRow key={prospect.id} prospect={prospect} />)}
|
|
137
|
+
</div>
|
|
138
|
+
) : (
|
|
139
|
+
<p className="rounded-lg border border-dashed border-border py-8 text-center text-sm text-muted-foreground">
|
|
140
|
+
No prospects need attention.
|
|
141
|
+
</p>
|
|
142
|
+
)}
|
|
143
|
+
</CardContent>
|
|
144
|
+
</Card>
|
|
145
|
+
|
|
146
|
+
<Card>
|
|
147
|
+
<CardHeader>
|
|
148
|
+
<CardTitle>Pipeline</CardTitle>
|
|
149
|
+
<p className="text-xs text-muted-foreground">Current distribution across recruiting stages.</p>
|
|
150
|
+
</CardHeader>
|
|
151
|
+
<CardContent className="space-y-3">
|
|
152
|
+
{PIPELINE.slice(0, 9).map((status) => {
|
|
153
|
+
const count = prospects.filter((p) => p.status === status).length;
|
|
154
|
+
const width = prospects.length ? Math.max((count / prospects.length) * 100, count ? 4 : 0) : 0;
|
|
155
|
+
return (
|
|
156
|
+
<div key={status}>
|
|
157
|
+
<div className="mb-1 flex items-center justify-between text-xs">
|
|
158
|
+
<span className="text-muted-foreground">{status}</span>
|
|
159
|
+
<span className="font-medium tabular-nums">{count}</span>
|
|
160
|
+
</div>
|
|
161
|
+
<div className="h-1.5 overflow-hidden rounded-full bg-muted">
|
|
162
|
+
<div className="h-full rounded-full bg-primary/70" style={{ width: `${width}%` }} />
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
})}
|
|
167
|
+
</CardContent>
|
|
168
|
+
</Card>
|
|
169
|
+
</section>
|
|
170
|
+
|
|
171
|
+
<Card>
|
|
172
|
+
<CardHeader className="flex-row items-center justify-between">
|
|
173
|
+
<div>
|
|
174
|
+
<CardTitle>Segment coverage</CardTitle>
|
|
175
|
+
<p className="mt-1 text-xs text-muted-foreground">Where the campaign is deep—and where sourcing should run next.</p>
|
|
176
|
+
</div>
|
|
177
|
+
<button
|
|
178
|
+
type="button"
|
|
179
|
+
onClick={() => navigation.toRoute('/segments')}
|
|
180
|
+
className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
|
|
181
|
+
>
|
|
182
|
+
Playbooks <ArrowRightIcon size={13} />
|
|
183
|
+
</button>
|
|
184
|
+
</CardHeader>
|
|
185
|
+
<CardContent>
|
|
186
|
+
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-4">
|
|
187
|
+
{segmentRows.map((segment) => (
|
|
188
|
+
<div key={segment.name} className="rounded-lg border border-border p-3">
|
|
189
|
+
<p className="text-sm font-medium">{segment.short}</p>
|
|
190
|
+
<p className="mt-1 text-xs text-muted-foreground">{segment.source}</p>
|
|
191
|
+
<div className="mt-3 flex items-end justify-between">
|
|
192
|
+
<span className="text-2xl font-semibold tabular-nums">{segment.total}</span>
|
|
193
|
+
<span className="text-xs text-muted-foreground">{segment.engaged} engaged</span>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
))}
|
|
197
|
+
</div>
|
|
198
|
+
</CardContent>
|
|
199
|
+
</Card>
|
|
200
|
+
</main>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function Metric({
|
|
205
|
+
icon: Icon,
|
|
206
|
+
label,
|
|
207
|
+
value,
|
|
208
|
+
}: {
|
|
209
|
+
icon: typeof UsersThreeIcon;
|
|
210
|
+
label: string;
|
|
211
|
+
value: string;
|
|
212
|
+
}) {
|
|
213
|
+
return (
|
|
214
|
+
<Card className="p-4">
|
|
215
|
+
<div className="flex items-center justify-between gap-3">
|
|
216
|
+
<div>
|
|
217
|
+
<p className="text-xs text-muted-foreground">{label}</p>
|
|
218
|
+
<p className="mt-1 text-2xl font-semibold tabular-nums">{value}</p>
|
|
219
|
+
</div>
|
|
220
|
+
<div className="rounded-lg bg-muted p-2 text-muted-foreground">
|
|
221
|
+
<Icon size={18} />
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
</Card>
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function PriorityRow({ prospect }: { prospect: AffiliateProspect }) {
|
|
229
|
+
return (
|
|
230
|
+
<div className="flex flex-wrap items-center gap-3 py-3 first:pt-0 last:pb-0">
|
|
231
|
+
<div className="flex min-w-0 flex-1 items-center gap-3">
|
|
232
|
+
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-muted text-xs font-semibold">
|
|
233
|
+
{prospect.name.split(/\s+/).slice(0, 2).map((part) => part[0]).join('')}
|
|
234
|
+
</div>
|
|
235
|
+
<div className="min-w-0">
|
|
236
|
+
<p className="truncate text-sm font-medium">{prospect.name}</p>
|
|
237
|
+
<p className="truncate text-xs text-muted-foreground">
|
|
238
|
+
{prospect.company || prospect.segment} · {prospect.nextAction || 'Review qualification'}
|
|
239
|
+
</p>
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
<span className={`rounded-full px-2 py-1 text-[11px] font-medium ${statusTone(prospect.status)}`}>
|
|
243
|
+
{prospect.status}
|
|
244
|
+
</span>
|
|
245
|
+
<span className="w-12 text-right text-sm font-semibold tabular-nums">{prospect.fitScore}</span>
|
|
246
|
+
</div>
|
|
247
|
+
);
|
|
248
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
4
|
+
import { useTopBarSearch } from '@notis/sdk';
|
|
5
|
+
import { ArrowClockwiseIcon, CalendarBlankIcon, EnvelopeSimpleIcon, LinkedinLogoIcon } from '@phosphor-icons/react';
|
|
6
|
+
|
|
7
|
+
import { Card } from '@/components/ui/card';
|
|
8
|
+
import { compactNumber, statusTone } from '@/lib/affiliate-data';
|
|
9
|
+
import { useAffiliateProspects } from '@/lib/notis-tools';
|
|
10
|
+
|
|
11
|
+
export default function ProspectsPage() {
|
|
12
|
+
const { prospects, loading, error, refetch } = useAffiliateProspects();
|
|
13
|
+
const [search, setSearch] = useState('');
|
|
14
|
+
const { setLoading } = useTopBarSearch({
|
|
15
|
+
value: search,
|
|
16
|
+
onChange: setSearch,
|
|
17
|
+
placeholder: 'Search name, company, segment, status, or source…',
|
|
18
|
+
});
|
|
19
|
+
useEffect(() => setLoading(loading), [loading, setLoading]);
|
|
20
|
+
|
|
21
|
+
const rows = useMemo(() => {
|
|
22
|
+
const query = search.trim().toLowerCase();
|
|
23
|
+
return [...prospects]
|
|
24
|
+
.filter((prospect) =>
|
|
25
|
+
!query ||
|
|
26
|
+
[prospect.name, prospect.company, prospect.segment, prospect.status, prospect.source]
|
|
27
|
+
.join(' ')
|
|
28
|
+
.toLowerCase()
|
|
29
|
+
.includes(query),
|
|
30
|
+
)
|
|
31
|
+
.sort((a, b) => b.fitScore - a.fitScore);
|
|
32
|
+
}, [prospects, search]);
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<main data-store-screenshot="prospects" className="affiliate-app-shell space-y-5">
|
|
36
|
+
<header className="flex flex-wrap items-start justify-between gap-4">
|
|
37
|
+
<div>
|
|
38
|
+
<p className="text-xs font-medium text-muted-foreground">Affiliate recruiting CRM</p>
|
|
39
|
+
<h1 className="mt-1 text-2xl font-semibold tracking-tight">{rows.length} prospects</h1>
|
|
40
|
+
<p className="mt-1 text-sm text-muted-foreground">
|
|
41
|
+
Ask Notis to add, enrich, qualify, or move prospects; this view stays the reference layer.
|
|
42
|
+
</p>
|
|
43
|
+
</div>
|
|
44
|
+
<button
|
|
45
|
+
type="button"
|
|
46
|
+
onClick={refetch}
|
|
47
|
+
className="inline-flex min-h-11 items-center gap-2 rounded-md border border-border px-3 text-sm text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
48
|
+
>
|
|
49
|
+
<ArrowClockwiseIcon className={loading ? 'animate-spin' : ''} size={16} />
|
|
50
|
+
Refresh
|
|
51
|
+
</button>
|
|
52
|
+
</header>
|
|
53
|
+
|
|
54
|
+
{error ? <p className="rounded-lg bg-destructive/10 px-4 py-3 text-sm text-destructive">{error.message}</p> : null}
|
|
55
|
+
|
|
56
|
+
<Card className="affiliate-desktop-table overflow-x-auto">
|
|
57
|
+
<table className="affiliate-data-table w-full border-collapse text-left">
|
|
58
|
+
<thead className="border-b border-border bg-muted/40 text-[11px] uppercase tracking-wide text-muted-foreground">
|
|
59
|
+
<tr>
|
|
60
|
+
<th className="px-4 py-3 font-medium">Prospect</th>
|
|
61
|
+
<th className="px-4 py-3 font-medium">Segment</th>
|
|
62
|
+
<th className="px-4 py-3 font-medium">Status</th>
|
|
63
|
+
<th className="px-4 py-3 text-right font-medium">Fit</th>
|
|
64
|
+
<th className="px-4 py-3 font-medium">Source</th>
|
|
65
|
+
<th className="px-4 py-3 font-medium">Next action</th>
|
|
66
|
+
<th className="px-4 py-3 font-medium">Reach</th>
|
|
67
|
+
</tr>
|
|
68
|
+
</thead>
|
|
69
|
+
<tbody className="divide-y divide-border">
|
|
70
|
+
{rows.map((prospect) => (
|
|
71
|
+
<tr key={prospect.id} className="transition hover:bg-muted/30">
|
|
72
|
+
<td className="px-4 py-3">
|
|
73
|
+
<p className="text-sm font-medium">{prospect.name}</p>
|
|
74
|
+
<p className="text-xs text-muted-foreground">{prospect.company || prospect.website}</p>
|
|
75
|
+
</td>
|
|
76
|
+
<td className="px-4 py-3 text-xs text-muted-foreground">{prospect.segment}</td>
|
|
77
|
+
<td className="px-4 py-3">
|
|
78
|
+
<span className={`rounded-full px-2 py-1 text-[11px] font-medium ${statusTone(prospect.status)}`}>
|
|
79
|
+
{prospect.status}
|
|
80
|
+
</span>
|
|
81
|
+
</td>
|
|
82
|
+
<td className="px-4 py-3 text-right text-sm font-semibold tabular-nums">{prospect.fitScore}</td>
|
|
83
|
+
<td className="px-4 py-3 text-xs text-muted-foreground">{prospect.source}</td>
|
|
84
|
+
<td className="px-4 py-3">
|
|
85
|
+
<p className="max-w-52 truncate text-xs">{prospect.nextAction || '—'}</p>
|
|
86
|
+
<p className="text-[11px] text-muted-foreground">{formatDate(prospect.nextActionDate)}</p>
|
|
87
|
+
</td>
|
|
88
|
+
<td className="px-4 py-3">
|
|
89
|
+
<div className="flex items-center gap-2 text-muted-foreground">
|
|
90
|
+
{prospect.email ? <EnvelopeSimpleIcon size={15} /> : null}
|
|
91
|
+
{prospect.linkedInUrl ? <LinkedinLogoIcon size={15} /> : null}
|
|
92
|
+
<span className="text-[11px]">{compactNumber(prospect.audienceSize || prospect.trafficEstimate)}</span>
|
|
93
|
+
</div>
|
|
94
|
+
</td>
|
|
95
|
+
</tr>
|
|
96
|
+
))}
|
|
97
|
+
</tbody>
|
|
98
|
+
</table>
|
|
99
|
+
</Card>
|
|
100
|
+
|
|
101
|
+
<div className="affiliate-mobile-cards space-y-3">
|
|
102
|
+
{rows.map((prospect) => (
|
|
103
|
+
<Card key={prospect.id} className="p-4">
|
|
104
|
+
<div className="flex items-start justify-between gap-3">
|
|
105
|
+
<div className="min-w-0">
|
|
106
|
+
<p className="truncate text-sm font-medium">{prospect.name}</p>
|
|
107
|
+
<p className="truncate text-xs text-muted-foreground">{prospect.company || prospect.segment}</p>
|
|
108
|
+
</div>
|
|
109
|
+
<span className="text-sm font-semibold tabular-nums">{prospect.fitScore}</span>
|
|
110
|
+
</div>
|
|
111
|
+
<div className="mt-3 flex flex-wrap items-center gap-2">
|
|
112
|
+
<span className={`rounded-full px-2 py-1 text-[11px] font-medium ${statusTone(prospect.status)}`}>
|
|
113
|
+
{prospect.status}
|
|
114
|
+
</span>
|
|
115
|
+
<span className="rounded-full bg-muted px-2 py-1 text-[11px] text-muted-foreground">{prospect.segment}</span>
|
|
116
|
+
</div>
|
|
117
|
+
<div className="mt-3 flex items-center gap-2 text-xs text-muted-foreground">
|
|
118
|
+
<CalendarBlankIcon size={14} />
|
|
119
|
+
{prospect.nextAction || 'Review qualification'} · {formatDate(prospect.nextActionDate)}
|
|
120
|
+
</div>
|
|
121
|
+
</Card>
|
|
122
|
+
))}
|
|
123
|
+
</div>
|
|
124
|
+
</main>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function formatDate(value: string): string {
|
|
129
|
+
if (!value) return '';
|
|
130
|
+
const date = new Date(value);
|
|
131
|
+
return Number.isNaN(date.getTime())
|
|
132
|
+
? value
|
|
133
|
+
: new Intl.DateTimeFormat('en', { month: 'short', day: 'numeric', timeZone: 'UTC' }).format(date);
|
|
134
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
|
+
import {
|
|
5
|
+
ArrowSquareOutIcon,
|
|
6
|
+
ChartLineUpIcon,
|
|
7
|
+
CheckCircleIcon,
|
|
8
|
+
MagnifyingGlassIcon,
|
|
9
|
+
UsersIcon,
|
|
10
|
+
} from '@phosphor-icons/react';
|
|
11
|
+
|
|
12
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
13
|
+
import { SEGMENTS, compactNumber } from '@/lib/affiliate-data';
|
|
14
|
+
import { useAffiliateProspects } from '@/lib/notis-tools';
|
|
15
|
+
|
|
16
|
+
export default function SegmentsPage() {
|
|
17
|
+
const { prospects, loading, error } = useAffiliateProspects();
|
|
18
|
+
const segments = useMemo(
|
|
19
|
+
() =>
|
|
20
|
+
SEGMENTS.map((segment) => {
|
|
21
|
+
const rows = prospects.filter((prospect) => prospect.segment === segment.name);
|
|
22
|
+
const qualified = rows.filter((prospect) => prospect.fitScore >= 75 && prospect.status !== 'Disqualified').length;
|
|
23
|
+
const activated = rows.filter((prospect) => prospect.status === 'Activated').length;
|
|
24
|
+
const reach = rows.reduce((sum, prospect) => sum + prospect.audienceSize + prospect.trafficEstimate, 0);
|
|
25
|
+
return { ...segment, total: rows.length, qualified, activated, reach };
|
|
26
|
+
}),
|
|
27
|
+
[prospects],
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<main data-store-screenshot="segments" className="affiliate-app-shell space-y-6">
|
|
32
|
+
<header>
|
|
33
|
+
<p className="text-xs font-medium text-muted-foreground">Seven focused acquisition motions</p>
|
|
34
|
+
<h1 className="mt-1 text-2xl font-semibold tracking-tight">Recruiting segments</h1>
|
|
35
|
+
<p className="mt-1 max-w-2xl text-sm text-muted-foreground">
|
|
36
|
+
Each segment has its own sourcing signal, qualification model, and outreach playbook in
|
|
37
|
+
<span className="font-medium text-foreground"> campaign/affiliate-network/segments</span>.
|
|
38
|
+
</p>
|
|
39
|
+
</header>
|
|
40
|
+
|
|
41
|
+
{error ? (
|
|
42
|
+
<p className="rounded-lg bg-destructive/10 px-4 py-3 text-sm text-destructive">{error.message}</p>
|
|
43
|
+
) : null}
|
|
44
|
+
{loading && !prospects.length ? (
|
|
45
|
+
<p className="rounded-lg border border-dashed border-border py-8 text-center text-sm text-muted-foreground">
|
|
46
|
+
Loading segment coverage…
|
|
47
|
+
</p>
|
|
48
|
+
) : null}
|
|
49
|
+
|
|
50
|
+
<section className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
|
|
51
|
+
{segments.map((segment, index) => (
|
|
52
|
+
<Card key={segment.name}>
|
|
53
|
+
<CardHeader>
|
|
54
|
+
<div className="flex items-start justify-between gap-3">
|
|
55
|
+
<div>
|
|
56
|
+
<p className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
|
|
57
|
+
Segment {index + 1}
|
|
58
|
+
</p>
|
|
59
|
+
<CardTitle className="mt-1 text-base">{segment.name}</CardTitle>
|
|
60
|
+
</div>
|
|
61
|
+
<div className="rounded-lg bg-muted p-2 text-muted-foreground">
|
|
62
|
+
<UsersIcon size={18} />
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
<p className="text-xs leading-relaxed text-muted-foreground">{segment.description}</p>
|
|
66
|
+
</CardHeader>
|
|
67
|
+
<CardContent>
|
|
68
|
+
<div className="grid grid-cols-3 gap-2">
|
|
69
|
+
<Kpi icon={MagnifyingGlassIcon} value={segment.total} label="Found" />
|
|
70
|
+
<Kpi icon={ChartLineUpIcon} value={segment.qualified} label="Qualified" />
|
|
71
|
+
<Kpi icon={CheckCircleIcon} value={segment.activated} label="Active" />
|
|
72
|
+
</div>
|
|
73
|
+
<div className="mt-4 rounded-lg bg-muted/50 p-3">
|
|
74
|
+
<p className="text-[11px] uppercase tracking-wide text-muted-foreground">Primary source</p>
|
|
75
|
+
<p className="mt-1 text-sm font-medium">{segment.source}</p>
|
|
76
|
+
<p className="mt-2 text-xs text-muted-foreground">
|
|
77
|
+
Combined discoverable reach: {compactNumber(segment.reach)}
|
|
78
|
+
</p>
|
|
79
|
+
</div>
|
|
80
|
+
<div className="mt-4 flex items-center justify-between text-xs text-muted-foreground">
|
|
81
|
+
<span>Playbook is source-controlled</span>
|
|
82
|
+
<ArrowSquareOutIcon size={14} />
|
|
83
|
+
</div>
|
|
84
|
+
</CardContent>
|
|
85
|
+
</Card>
|
|
86
|
+
))}
|
|
87
|
+
</section>
|
|
88
|
+
</main>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function Kpi({
|
|
93
|
+
icon: Icon,
|
|
94
|
+
value,
|
|
95
|
+
label,
|
|
96
|
+
}: {
|
|
97
|
+
icon: typeof UsersIcon;
|
|
98
|
+
value: number;
|
|
99
|
+
label: string;
|
|
100
|
+
}) {
|
|
101
|
+
return (
|
|
102
|
+
<div className="rounded-lg border border-border p-2.5">
|
|
103
|
+
<Icon size={14} className="text-muted-foreground" />
|
|
104
|
+
<p className="mt-2 text-lg font-semibold tabular-nums">{value}</p>
|
|
105
|
+
<p className="text-[10px] text-muted-foreground">{label}</p>
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
}
|