@notis_ai/cli 0.2.7 → 0.2.9
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 +56 -2
- package/dist/scaffolds/notis-database/app/page.tsx +38 -41
- package/dist/scaffolds/notis-database/lib/types.ts +5 -7
- package/dist/scaffolds/notis-database/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-4.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-5.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-fixtures.json +1839 -0
- package/dist/scaffolds/notis-database/notis.config.ts +48 -1
- package/dist/scaffolds/notis-database/package-lock.json +3935 -0
- package/dist/scaffolds/notis-database/package.json +2 -1
- package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/package.json +4 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/components/DocumentEditor.tsx +93 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/components/Markdown.tsx +60 -0
- package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/components/MultiSelectActionBar.tsx +7 -2
- package/dist/scaffolds/notis-database/packages/sdk/src/config.ts +196 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/documents.ts +229 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDocument.ts +78 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDocuments.ts +121 -0
- package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/hooks/useMultiSelect.ts +38 -2
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
- package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/index.ts +30 -1
- package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/runtime.ts +76 -17
- package/dist/scaffolds/notis-database/packages/sdk/src/styles.css +186 -0
- package/dist/scaffolds/notis-database/src/mock-runtime.ts +4 -1
- package/dist/scaffolds/notis-journal/CHANGELOG.md +4 -0
- package/dist/scaffolds/notis-journal/app/page.tsx +1 -0
- package/dist/scaffolds/notis-journal/package-lock.json +4615 -0
- package/dist/scaffolds/notis-journal/package.json +2 -2
- package/dist/scaffolds/notis-journal/packages/sdk/src/config.ts +52 -1
- package/dist/scaffolds/notis-notes/app/page.tsx +39 -46
- package/dist/scaffolds/notis-notes/app/phosphor-icons.ts +596 -0
- package/dist/scaffolds/notis-notes/lib/visible-properties.ts +144 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-4.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-5.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-fixtures.json +752 -0
- package/dist/scaffolds/notis-notes/notis.config.ts +48 -3
- package/dist/scaffolds/notis-notes/package-lock.json +4636 -0
- package/dist/scaffolds/notis-notes/package.json +7 -3
- package/dist/scaffolds/notis-notes/packages/sdk/package.json +36 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/components/DocumentEditor.tsx +93 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/components/Markdown.tsx +60 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectActionBar.tsx +278 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/config.ts +196 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/documents.ts +229 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useBackend.ts +41 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDocument.ts +78 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDocuments.ts +121 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useMultiSelect.ts +539 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useNotis.ts +34 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTool.ts +64 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTools.ts +56 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/index.ts +83 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/provider.tsx +43 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/runtime.ts +220 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/styles.css +186 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/ui.ts +15 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/vite.ts +56 -0
- package/dist/scaffolds/notis-notes/packages/sdk/tsconfig.json +15 -0
- package/dist/scaffolds/notis-random/CHANGELOG.md +12 -2
- package/dist/scaffolds/notis-random/app/history/page.tsx +5 -4
- package/dist/scaffolds/notis-random/app/page.tsx +141 -74
- package/dist/scaffolds/notis-random/components/ui/button.tsx +50 -0
- package/dist/scaffolds/notis-random/components/ui/card.tsx +16 -0
- package/dist/scaffolds/notis-random/components/ui/input.tsx +23 -0
- package/dist/scaffolds/notis-random/lib/notis-tools.ts +26 -7
- package/dist/scaffolds/notis-random/lib/rng.ts +179 -19
- package/dist/scaffolds/notis-random/lib/roll-record.ts +103 -16
- package/dist/scaffolds/notis-random/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-4.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-5.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-fixtures.json +753 -0
- package/dist/scaffolds/notis-random/notis.config.ts +47 -3
- package/dist/scaffolds/notis-random/package-lock.json +4513 -0
- package/dist/scaffolds/notis-random/package.json +6 -2
- package/dist/scaffolds/notis-random/packages/sdk/package.json +36 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/components/DocumentEditor.tsx +93 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/components/Markdown.tsx +60 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectActionBar.tsx +278 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/config.ts +196 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/documents.ts +229 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useBackend.ts +41 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDocument.ts +78 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDocuments.ts +121 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useMultiSelect.ts +539 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useNotis.ts +34 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTool.ts +64 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTools.ts +56 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/index.ts +83 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/provider.tsx +43 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/runtime.ts +220 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/styles.css +186 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/ui.ts +15 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/vite.ts +56 -0
- package/dist/scaffolds/notis-random/packages/sdk/tsconfig.json +15 -0
- package/dist/scaffolds/notis-random/tailwind.config.ts +8 -1
- package/dist/scaffolds.json +8 -17
- package/package.json +2 -1
- package/skills/notis-apps/SKILL.md +438 -144
- package/skills/notis-apps/cli.md +16 -1
- package/skills/notis-cli/SKILL.md +11 -4
- package/skills/notis-onboarding/BRIEF.md +93 -0
- package/skills/notis-query/cli.md +1 -1
- package/src/cli.js +50 -12
- package/src/command-specs/apps.js +309 -41
- package/src/command-specs/auth.js +107 -0
- package/src/command-specs/index.js +4 -0
- package/src/command-specs/meta.js +68 -6
- package/src/command-specs/onboarding.js +304 -0
- package/src/command-specs/tools.js +8 -4
- package/src/runtime/app-dev-server.js +3 -2
- package/src/runtime/app-dev-sessions.js +14 -40
- package/src/runtime/app-platform.js +65 -4
- package/src/runtime/desktop-auth.js +86 -17
- package/src/runtime/errors.js +1 -0
- package/src/runtime/oauth.js +1105 -0
- package/src/runtime/output.js +16 -2
- package/src/runtime/profiles.js +542 -18
- package/src/runtime/telemetry.js +92 -0
- package/src/runtime/transport.js +66 -11
- package/template/.harness/index.html.tmpl +30 -2
- package/template/packages/sdk/src/config.ts +38 -1
- package/dist/scaffolds/notis-affiliate-prospects/CHANGELOG.md +0 -7
- package/dist/scaffolds/notis-affiliate-prospects/README.md +0 -20
- package/dist/scaffolds/notis-affiliate-prospects/app/globals.css +0 -42
- package/dist/scaffolds/notis-affiliate-prospects/app/layout.tsx +0 -7
- package/dist/scaffolds/notis-affiliate-prospects/app/page.tsx +0 -248
- package/dist/scaffolds/notis-affiliate-prospects/app/prospects/page.tsx +0 -134
- package/dist/scaffolds/notis-affiliate-prospects/app/segments/page.tsx +0 -108
- package/dist/scaffolds/notis-affiliate-prospects/components/ui/card.tsx +0 -35
- package/dist/scaffolds/notis-affiliate-prospects/index.html +0 -12
- package/dist/scaffolds/notis-affiliate-prospects/lib/affiliate-data.ts +0 -218
- package/dist/scaffolds/notis-affiliate-prospects/lib/demo-prospects.ts +0 -52
- package/dist/scaffolds/notis-affiliate-prospects/lib/notis-tools.ts +0 -100
- package/dist/scaffolds/notis-affiliate-prospects/lib/utils.ts +0 -25
- 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 +0 -187
- package/dist/scaffolds/notis-affiliate-prospects/notis.config.ts +0 -65
- package/dist/scaffolds/notis-affiliate-prospects/package.json +0 -32
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/config.ts +0 -90
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useDatabase.ts +0 -76
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useUpsertDocument.ts +0 -50
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/styles.css +0 -38
- package/dist/scaffolds/notis-affiliate-prospects/postcss.config.mjs +0 -6
- package/dist/scaffolds/notis-affiliate-prospects/src/dev-main.tsx +0 -72
- package/dist/scaffolds/notis-affiliate-prospects/src/mock-runtime.ts +0 -242
- package/dist/scaffolds/notis-affiliate-prospects/tailwind.config.ts +0 -51
- package/dist/scaffolds/notis-affiliate-prospects/tsconfig.json +0 -23
- package/dist/scaffolds/notis-affiliate-prospects/vite.config.ts +0 -11
- /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -0
- /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -0
- /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/hooks/useBackend.ts +0 -0
- /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/hooks/useNotis.ts +0 -0
- /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/hooks/useNotisNavigation.ts +0 -0
- /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/hooks/useTool.ts +0 -0
- /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/hooks/useTools.ts +0 -0
- /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/hooks/useTopBarSearch.ts +0 -0
- /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/provider.tsx +0 -0
- /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/ui.ts +0 -0
- /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/vite.ts +0 -0
- /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/tsconfig.json +0 -0
- /package/dist/scaffolds/{notis-affiliate-prospects → notis-random}/components.json +0 -0
|
@@ -1,134 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,108 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import * as React from 'react';
|
|
2
|
-
|
|
3
|
-
import { cn } from '@/lib/utils';
|
|
4
|
-
|
|
5
|
-
export const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
6
|
-
({ className, ...props }, ref) => (
|
|
7
|
-
<div
|
|
8
|
-
ref={ref}
|
|
9
|
-
className={cn('rounded-xl border border-border bg-card text-card-foreground shadow-sm', className)}
|
|
10
|
-
{...props}
|
|
11
|
-
/>
|
|
12
|
-
),
|
|
13
|
-
);
|
|
14
|
-
Card.displayName = 'Card';
|
|
15
|
-
|
|
16
|
-
export const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
17
|
-
({ className, ...props }, ref) => (
|
|
18
|
-
<div ref={ref} className={cn('flex flex-col gap-1.5 p-5', className)} {...props} />
|
|
19
|
-
),
|
|
20
|
-
);
|
|
21
|
-
CardHeader.displayName = 'CardHeader';
|
|
22
|
-
|
|
23
|
-
export const CardTitle = React.forwardRef<HTMLHeadingElement, React.HTMLAttributes<HTMLHeadingElement>>(
|
|
24
|
-
({ className, ...props }, ref) => (
|
|
25
|
-
<h3 ref={ref} className={cn('text-sm font-semibold tracking-tight', className)} {...props} />
|
|
26
|
-
),
|
|
27
|
-
);
|
|
28
|
-
CardTitle.displayName = 'CardTitle';
|
|
29
|
-
|
|
30
|
-
export const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
31
|
-
({ className, ...props }, ref) => (
|
|
32
|
-
<div ref={ref} className={cn('p-5 pt-0', className)} {...props} />
|
|
33
|
-
),
|
|
34
|
-
);
|
|
35
|
-
CardContent.displayName = 'CardContent';
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
-
<title>Affiliate Prospects (dev preview)</title>
|
|
7
|
-
</head>
|
|
8
|
-
<body>
|
|
9
|
-
<div id="root"></div>
|
|
10
|
-
<script type="module" src="/src/dev-main.tsx"></script>
|
|
11
|
-
</body>
|
|
12
|
-
</html>
|
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
export const SEGMENTS = [
|
|
2
|
-
{
|
|
3
|
-
name: 'Customer Advocate',
|
|
4
|
-
short: 'Customers',
|
|
5
|
-
description: 'Power users and existing referrers with firsthand product credibility.',
|
|
6
|
-
source: 'Rewardful + product usage',
|
|
7
|
-
},
|
|
8
|
-
{
|
|
9
|
-
name: 'Automation Agency',
|
|
10
|
-
short: 'Agencies',
|
|
11
|
-
description: 'Zapier, Make, n8n, and AI automation consultants who recommend tools to clients.',
|
|
12
|
-
source: 'Partner directories + LinkedIn',
|
|
13
|
-
},
|
|
14
|
-
{
|
|
15
|
-
name: 'AI Creator',
|
|
16
|
-
short: 'Creators',
|
|
17
|
-
description: 'AI, productivity, and automation creators, newsletters, and educators.',
|
|
18
|
-
source: 'YouTube + LinkedIn + newsletters',
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
name: 'SEO Publisher',
|
|
22
|
-
short: 'Publishers',
|
|
23
|
-
description: 'Comparison sites and writers ranking for high-intent automation searches.',
|
|
24
|
-
source: 'DataForSEO SERPs',
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
name: 'Integration Partner',
|
|
28
|
-
short: 'Partners',
|
|
29
|
-
description: 'Complementary SaaS teams, implementation partners, and integration ecosystems.',
|
|
30
|
-
source: 'Integration catalog + partner pages',
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
name: 'Community Educator',
|
|
34
|
-
short: 'Communities',
|
|
35
|
-
description: 'Operators of trusted no-code, RevOps, AI, and founder communities.',
|
|
36
|
-
source: 'Community directories + events',
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
name: 'Affiliate Marketplace',
|
|
40
|
-
short: 'Marketplaces',
|
|
41
|
-
description: 'Experienced B2B SaaS affiliates active in curated partner marketplaces.',
|
|
42
|
-
source: 'PartnerStack + impact.com',
|
|
43
|
-
},
|
|
44
|
-
] as const;
|
|
45
|
-
|
|
46
|
-
export const PIPELINE = [
|
|
47
|
-
'New',
|
|
48
|
-
'Researching',
|
|
49
|
-
'Qualified',
|
|
50
|
-
'Ready for Outreach',
|
|
51
|
-
'Contacted',
|
|
52
|
-
'Replied',
|
|
53
|
-
'Interested',
|
|
54
|
-
'Applied',
|
|
55
|
-
'Activated',
|
|
56
|
-
'Nurture',
|
|
57
|
-
'Disqualified',
|
|
58
|
-
] as const;
|
|
59
|
-
|
|
60
|
-
export type SegmentName = (typeof SEGMENTS)[number]['name'];
|
|
61
|
-
export type ProspectStatus = (typeof PIPELINE)[number];
|
|
62
|
-
|
|
63
|
-
export interface AffiliateProspect {
|
|
64
|
-
id: string;
|
|
65
|
-
name: string;
|
|
66
|
-
company: string;
|
|
67
|
-
segment: SegmentName;
|
|
68
|
-
status: ProspectStatus;
|
|
69
|
-
priority: 'P0' | 'P1' | 'P2' | 'P3';
|
|
70
|
-
fitScore: number;
|
|
71
|
-
email: string;
|
|
72
|
-
linkedInUrl: string;
|
|
73
|
-
website: string;
|
|
74
|
-
source: string;
|
|
75
|
-
audienceSize: number;
|
|
76
|
-
trafficEstimate: number;
|
|
77
|
-
enrichmentStatus: string;
|
|
78
|
-
outreachChannel: string;
|
|
79
|
-
nextAction: string;
|
|
80
|
-
nextActionDate: string;
|
|
81
|
-
lastTouchDate: string;
|
|
82
|
-
referredSignups: number;
|
|
83
|
-
activatedReferrals: number;
|
|
84
|
-
attributedMrr: number;
|
|
85
|
-
notes: string;
|
|
86
|
-
optedOut: boolean;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
interface DocumentLike {
|
|
90
|
-
id?: string;
|
|
91
|
-
document_id?: string;
|
|
92
|
-
title?: string;
|
|
93
|
-
properties?: Record<string, unknown>;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export function normalizeProspect(document: DocumentLike): AffiliateProspect {
|
|
97
|
-
const properties = document.properties ?? {};
|
|
98
|
-
return {
|
|
99
|
-
id: String(document.id ?? document.document_id ?? document.title ?? crypto.randomUUID()),
|
|
100
|
-
name: textValue(properties.Name) || document.title || 'Untitled prospect',
|
|
101
|
-
company: textValue(properties.Company),
|
|
102
|
-
segment: segmentValue(textValue(properties.Segment)),
|
|
103
|
-
status: statusValue(textValue(properties.Status)),
|
|
104
|
-
priority: priorityValue(textValue(properties.Priority)),
|
|
105
|
-
fitScore: numberValue(properties['Fit Score']),
|
|
106
|
-
email: textValue(properties.Email),
|
|
107
|
-
linkedInUrl: textValue(properties.LinkedIn),
|
|
108
|
-
website: textValue(properties.Website),
|
|
109
|
-
source: textValue(properties.Source),
|
|
110
|
-
audienceSize: numberValue(properties['Audience Size']),
|
|
111
|
-
trafficEstimate: numberValue(properties['Monthly Traffic']),
|
|
112
|
-
enrichmentStatus: textValue(properties['Enrichment Status']),
|
|
113
|
-
outreachChannel: textValue(properties['Outreach Channel']),
|
|
114
|
-
nextAction: textValue(properties['Next Action']),
|
|
115
|
-
nextActionDate: dateValue(properties['Next Action Date']),
|
|
116
|
-
lastTouchDate: dateValue(properties['Last Touch Date']),
|
|
117
|
-
referredSignups: numberValue(properties['Referred Signups']),
|
|
118
|
-
activatedReferrals: numberValue(properties['Activated Referrals']),
|
|
119
|
-
attributedMrr: numberValue(properties['Attributed MRR']),
|
|
120
|
-
notes: textValue(properties.Notes),
|
|
121
|
-
optedOut: booleanValue(properties['Opted Out']),
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export function statusTone(status: ProspectStatus): string {
|
|
126
|
-
if (status === 'Activated') return 'bg-emerald-500/10 text-emerald-700 dark:text-emerald-300';
|
|
127
|
-
if (['Interested', 'Applied'].includes(status)) return 'bg-violet-500/10 text-violet-700 dark:text-violet-300';
|
|
128
|
-
if (['Contacted', 'Replied'].includes(status)) return 'bg-sky-500/10 text-sky-700 dark:text-sky-300';
|
|
129
|
-
if (['Nurture', 'Disqualified'].includes(status)) return 'bg-muted text-muted-foreground';
|
|
130
|
-
return 'bg-amber-500/10 text-amber-700 dark:text-amber-300';
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export function compactNumber(value: number): string {
|
|
134
|
-
return new Intl.NumberFormat('en', { notation: 'compact', maximumFractionDigits: 1 }).format(value);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export function money(value: number): string {
|
|
138
|
-
return new Intl.NumberFormat('en', {
|
|
139
|
-
style: 'currency',
|
|
140
|
-
currency: 'USD',
|
|
141
|
-
maximumFractionDigits: 0,
|
|
142
|
-
}).format(value);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function segmentValue(value: string): SegmentName {
|
|
146
|
-
const normalized = value.trim().toLowerCase();
|
|
147
|
-
const aliases: Record<string, SegmentName> = {
|
|
148
|
-
'customer advocate': 'Customer Advocate',
|
|
149
|
-
'customer advocates': 'Customer Advocate',
|
|
150
|
-
'automation agency': 'Automation Agency',
|
|
151
|
-
'automation agencies': 'Automation Agency',
|
|
152
|
-
'ai creator': 'AI Creator',
|
|
153
|
-
'ai creators': 'AI Creator',
|
|
154
|
-
'seo publisher': 'SEO Publisher',
|
|
155
|
-
'seo publishers': 'SEO Publisher',
|
|
156
|
-
'integration partner': 'Integration Partner',
|
|
157
|
-
'integration partners': 'Integration Partner',
|
|
158
|
-
'community educator': 'Community Educator',
|
|
159
|
-
'community educators': 'Community Educator',
|
|
160
|
-
'affiliate marketplace': 'Affiliate Marketplace',
|
|
161
|
-
'affiliate marketplaces': 'Affiliate Marketplace',
|
|
162
|
-
};
|
|
163
|
-
return aliases[normalized] ?? 'Customer Advocate';
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function statusValue(value: string): ProspectStatus {
|
|
167
|
-
return (PIPELINE.find((status) => status === value) ?? 'New') as ProspectStatus;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function priorityValue(value: string): AffiliateProspect['priority'] {
|
|
171
|
-
return value === 'P0' || value === 'P1' || value === 'P2' || value === 'P3' ? value : 'P2';
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function textValue(value: unknown): string {
|
|
175
|
-
if (typeof value === 'string') return value;
|
|
176
|
-
if (!value || typeof value !== 'object') return '';
|
|
177
|
-
const typed = value as Record<string, unknown>;
|
|
178
|
-
if (typeof typed.url === 'string') return typed.url;
|
|
179
|
-
if (typeof typed.email === 'string') return typed.email;
|
|
180
|
-
if (typed.select && typeof typed.select === 'object') return String((typed.select as Record<string, unknown>).name ?? '');
|
|
181
|
-
if (typed.status && typeof typed.status === 'object') return String((typed.status as Record<string, unknown>).name ?? '');
|
|
182
|
-
for (const key of ['title', 'rich_text']) {
|
|
183
|
-
const rows = typed[key];
|
|
184
|
-
if (Array.isArray(rows)) {
|
|
185
|
-
return rows
|
|
186
|
-
.map((row) => {
|
|
187
|
-
if (!row || typeof row !== 'object') return '';
|
|
188
|
-
const record = row as Record<string, unknown>;
|
|
189
|
-
if (typeof record.plain_text === 'string') return record.plain_text;
|
|
190
|
-
const text = record.text as Record<string, unknown> | undefined;
|
|
191
|
-
return typeof text?.content === 'string' ? text.content : '';
|
|
192
|
-
})
|
|
193
|
-
.join('');
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
return '';
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function numberValue(value: unknown): number {
|
|
200
|
-
if (typeof value === 'number') return value;
|
|
201
|
-
if (value && typeof value === 'object' && typeof (value as Record<string, unknown>).number === 'number') {
|
|
202
|
-
return (value as Record<string, unknown>).number as number;
|
|
203
|
-
}
|
|
204
|
-
return 0;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function booleanValue(value: unknown): boolean {
|
|
208
|
-
if (typeof value === 'boolean') return value;
|
|
209
|
-
if (value && typeof value === 'object') return Boolean((value as Record<string, unknown>).checkbox);
|
|
210
|
-
return false;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function dateValue(value: unknown): string {
|
|
214
|
-
if (typeof value === 'string') return value;
|
|
215
|
-
if (!value || typeof value !== 'object') return '';
|
|
216
|
-
const date = (value as Record<string, unknown>).date;
|
|
217
|
-
return date && typeof date === 'object' ? String((date as Record<string, unknown>).start ?? '') : '';
|
|
218
|
-
}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import type { AffiliateProspect, ProspectStatus, SegmentName } from '@/lib/affiliate-data';
|
|
2
|
-
|
|
3
|
-
export const DEMO_PROSPECTS: AffiliateProspect[] = [
|
|
4
|
-
demo('Alex Morgan', 'FlowNorth', 'Automation Agency', 'Interested', 94, 'Zapier Solution Partner Directory', 22200, 0, 'Send partner economics one-pager'),
|
|
5
|
-
demo('Maya Chen', 'Agent Ops Weekly', 'AI Creator', 'Replied', 91, 'Newsletter discovery', 60000, 0, 'Share creator demo kit'),
|
|
6
|
-
demo('Noah Williams', 'Automate Better', 'SEO Publisher', 'Ready for Outreach', 88, 'DataForSEO: Zapier alternatives', 76000, 0, 'Approve comparison-page pitch'),
|
|
7
|
-
demo('Sofia Rossi', 'RevOps Circle', 'Community Educator', 'Qualified', 86, 'Community operator list', 14500, 0, 'Enrich sponsor history'),
|
|
8
|
-
demo('Theo Martin', 'Notis customer', 'Customer Advocate', 'Activated', 97, 'Rewardful + product usage', 0, 1680, ''),
|
|
9
|
-
demo('Lena Fischer', 'StackPilot', 'Integration Partner', 'Applied', 92, 'Integration overlap analysis', 62000, 0, 'Confirm co-marketing launch date'),
|
|
10
|
-
demo('Sam Patel', 'SaaS Growth Lab', 'Affiliate Marketplace', 'Contacted', 82, 'PartnerStack marketplace', 51000, 0, 'Follow up with use-case proof'),
|
|
11
|
-
demo('Jordan Lee', 'OpsCraft', 'Automation Agency', 'Qualified', 84, 'Make Partner Directory', 5400, 0, 'Review recent client verticals'),
|
|
12
|
-
demo('Camille Laurent', 'Productive AI', 'SEO Publisher', 'Activated', 90, 'DataForSEO: AI automation tools', 92000, 2940, ''),
|
|
13
|
-
demo('Owen King', 'Notis customer', 'Customer Advocate', 'Contacted', 80, 'Product usage', 0, 0, 'Invite to advocate cohort'),
|
|
14
|
-
];
|
|
15
|
-
|
|
16
|
-
function demo(
|
|
17
|
-
name: string,
|
|
18
|
-
company: string,
|
|
19
|
-
segment: SegmentName,
|
|
20
|
-
status: ProspectStatus,
|
|
21
|
-
fitScore: number,
|
|
22
|
-
source: string,
|
|
23
|
-
reach: number,
|
|
24
|
-
attributedMrr: number,
|
|
25
|
-
nextAction: string,
|
|
26
|
-
): AffiliateProspect {
|
|
27
|
-
return {
|
|
28
|
-
id: `demo-${name.toLowerCase().replace(/[^a-z0-9]+/g, '-')}`,
|
|
29
|
-
name,
|
|
30
|
-
company,
|
|
31
|
-
segment,
|
|
32
|
-
status,
|
|
33
|
-
priority: fitScore >= 90 ? 'P0' : fitScore >= 75 ? 'P1' : 'P2',
|
|
34
|
-
fitScore,
|
|
35
|
-
email: `${name.split(' ')[0].toLowerCase()}@example.com`,
|
|
36
|
-
linkedInUrl: 'https://linkedin.com',
|
|
37
|
-
website: '',
|
|
38
|
-
source,
|
|
39
|
-
audienceSize: reach,
|
|
40
|
-
trafficEstimate: 0,
|
|
41
|
-
enrichmentStatus: 'Verified',
|
|
42
|
-
outreachChannel: 'Email + LinkedIn',
|
|
43
|
-
nextAction,
|
|
44
|
-
nextActionDate: nextAction ? '2026-07-25' : '',
|
|
45
|
-
lastTouchDate: '',
|
|
46
|
-
referredSignups: attributedMrr ? 12 : 0,
|
|
47
|
-
activatedReferrals: attributedMrr ? 5 : 0,
|
|
48
|
-
attributedMrr,
|
|
49
|
-
notes: '',
|
|
50
|
-
optedOut: false,
|
|
51
|
-
};
|
|
52
|
-
}
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useCallback, useEffect, useState } from 'react';
|
|
4
|
-
import { useNotis, useTool } from '@notis/sdk';
|
|
5
|
-
|
|
6
|
-
import { normalizeProspect, type AffiliateProspect } from '@/lib/affiliate-data';
|
|
7
|
-
import { DEMO_PROSPECTS } from '@/lib/demo-prospects';
|
|
8
|
-
|
|
9
|
-
type QueryArgs = {
|
|
10
|
-
database_slug: 'affiliate_prospects_1';
|
|
11
|
-
query: { page_size: number };
|
|
12
|
-
offset?: number;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
type QueryResult = {
|
|
16
|
-
documents?: Array<{
|
|
17
|
-
id?: string;
|
|
18
|
-
document_id?: string;
|
|
19
|
-
title?: string;
|
|
20
|
-
properties?: Record<string, unknown>;
|
|
21
|
-
}>;
|
|
22
|
-
error?: string;
|
|
23
|
-
message?: string;
|
|
24
|
-
has_more?: boolean;
|
|
25
|
-
next_offset?: number | null;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
function isDevelopmentPreview(appId?: string) {
|
|
29
|
-
if (appId === 'harness-app') return true;
|
|
30
|
-
if (typeof window === 'undefined') return false;
|
|
31
|
-
return window.location.pathname.includes('/apps/affiliate-prospects-dev-');
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function useAffiliateProspects() {
|
|
35
|
-
const { app } = useNotis();
|
|
36
|
-
const query = useTool<QueryArgs, QueryResult>('LOCAL_NOTIS_DATABASE_QUERY');
|
|
37
|
-
const [prospects, setProspects] = useState<AffiliateProspect[]>([]);
|
|
38
|
-
const [localError, setLocalError] = useState<Error | null>(null);
|
|
39
|
-
const [usingPreviewFixture, setUsingPreviewFixture] = useState(false);
|
|
40
|
-
const [fetchKey, setFetchKey] = useState(0);
|
|
41
|
-
|
|
42
|
-
const refetch = useCallback(() => setFetchKey((key) => key + 1), []);
|
|
43
|
-
|
|
44
|
-
useEffect(() => {
|
|
45
|
-
let cancelled = false;
|
|
46
|
-
setLocalError(null);
|
|
47
|
-
setUsingPreviewFixture(false);
|
|
48
|
-
const fetchAllProspects = async () => {
|
|
49
|
-
const documents: NonNullable<QueryResult['documents']> = [];
|
|
50
|
-
let offset = 0;
|
|
51
|
-
while (true) {
|
|
52
|
-
const result = await query.call({
|
|
53
|
-
database_slug: 'affiliate_prospects_1',
|
|
54
|
-
query: { page_size: 500 },
|
|
55
|
-
...(offset ? { offset } : {}),
|
|
56
|
-
});
|
|
57
|
-
if (!result.documents && (result.error || result.message)) {
|
|
58
|
-
throw new Error(result.error ?? result.message);
|
|
59
|
-
}
|
|
60
|
-
documents.push(...(result.documents ?? []));
|
|
61
|
-
if (!result.has_more) return documents;
|
|
62
|
-
if (typeof result.next_offset !== 'number' || result.next_offset <= offset) {
|
|
63
|
-
throw new Error('Prospect query returned an invalid pagination offset');
|
|
64
|
-
}
|
|
65
|
-
offset = result.next_offset;
|
|
66
|
-
}
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
void fetchAllProspects()
|
|
70
|
-
.then((documents) => {
|
|
71
|
-
if (cancelled) return;
|
|
72
|
-
const normalized = documents.map(normalizeProspect);
|
|
73
|
-
const shouldUsePreviewFixture = !normalized.length && isDevelopmentPreview(app?.id);
|
|
74
|
-
setUsingPreviewFixture(shouldUsePreviewFixture);
|
|
75
|
-
setProspects(shouldUsePreviewFixture ? DEMO_PROSPECTS : normalized);
|
|
76
|
-
})
|
|
77
|
-
.catch((error) => {
|
|
78
|
-
if (cancelled) return;
|
|
79
|
-
if (isDevelopmentPreview(app?.id)) {
|
|
80
|
-
setProspects(DEMO_PROSPECTS);
|
|
81
|
-
setLocalError(null);
|
|
82
|
-
setUsingPreviewFixture(true);
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
setProspects([]);
|
|
86
|
-
setUsingPreviewFixture(false);
|
|
87
|
-
setLocalError(error instanceof Error ? error : new Error(String(error)));
|
|
88
|
-
});
|
|
89
|
-
return () => {
|
|
90
|
-
cancelled = true;
|
|
91
|
-
};
|
|
92
|
-
}, [app?.id, fetchKey, query.call]);
|
|
93
|
-
|
|
94
|
-
return {
|
|
95
|
-
prospects,
|
|
96
|
-
loading: query.loading,
|
|
97
|
-
error: usingPreviewFixture ? null : query.error ?? localError,
|
|
98
|
-
refetch,
|
|
99
|
-
};
|
|
100
|
-
}
|