@robosystems/core 0.8.8 → 0.8.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@robosystems/core",
3
- "version": "0.8.8",
3
+ "version": "0.8.9",
4
4
  "description": "Shared RoboSystems frontend core library (auth, contexts, task monitoring, UI components) for the RoboSystems Next.js apps",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -11,7 +11,8 @@ export interface MarkdownProseProps {
11
11
  * search results). Theme-aware via `dark:prose-dark` — the consuming apps
12
12
  * define the `typography.dark` variant in their Tailwind configs; do NOT
13
13
  * switch to `dark:prose-invert`, which doesn't resolve under their
14
- * Tailwind v4 + typography-plugin setup (see research/ResearchArticle.tsx).
14
+ * Tailwind v4 + typography-plugin setup (first hit in the research article,
15
+ * now roboinvestor-app's src/components/research/ResearchArticle.tsx).
15
16
  * Works in server and client components alike.
16
17
  */
17
18
  export declare function MarkdownProse({ children, size, className, }: MarkdownProseProps): import("react").JSX.Element;
@@ -7,7 +7,8 @@ import { twMerge } from 'tailwind-merge';
7
7
  * search results). Theme-aware via `dark:prose-dark` — the consuming apps
8
8
  * define the `typography.dark` variant in their Tailwind configs; do NOT
9
9
  * switch to `dark:prose-invert`, which doesn't resolve under their
10
- * Tailwind v4 + typography-plugin setup (see research/ResearchArticle.tsx).
10
+ * Tailwind v4 + typography-plugin setup (first hit in the research article,
11
+ * now roboinvestor-app's src/components/research/ResearchArticle.tsx).
11
12
  * Works in server and client components alike.
12
13
  */
13
14
  export function MarkdownProse({ children, size = 'base', className, }) {
@@ -43,5 +43,5 @@ export function LandingFooter({ tagline, productLinks, contactModal: ContactModa
43
43
  return (_jsx("li", { children: _jsx(Link, Object.assign({ href: cfg.url }, (isCurrent
44
44
  ? {}
45
45
  : { target: '_blank', rel: 'noopener noreferrer' }), { className: linkClass, children: cfg.displayName })) }, app));
46
- }) })] }), _jsxs("div", { children: [_jsx("h3", { className: headingClass, children: "Company" }), _jsxs("ul", { className: "space-y-2 text-sm", children: [_jsx("li", { children: _jsx(Link, Object.assign({ href: researchHref }, researchLinkProps, { className: linkClass, children: "Research" })) }), _jsx("li", { children: _jsx(Link, { href: "https://holon.robosystems.ai", target: "_blank", rel: "noopener noreferrer", className: linkClass, children: "Holon Viewer" }) }), _jsx("li", { children: _jsx(Link, Object.assign({ href: resolvedBlogHref }, blogLinkProps, { className: linkClass, children: "Blog" })) }), _jsx("li", { children: _jsx(Link, Object.assign({ href: `${companyBase}/pages/privacy` }, companyLinkProps, { className: linkClass, children: "Privacy" })) }), _jsx("li", { children: _jsx(Link, Object.assign({ href: `${companyBase}/pages/terms` }, companyLinkProps, { className: linkClass, children: "Terms" })) }), _jsx("li", { children: _jsx(Link, { href: "https://trust.robosystems.ai", target: "_blank", rel: "noopener noreferrer", className: linkClass, children: "Trust Center" }) }), _jsx("li", { children: _jsx(Link, { href: "https://status.robosystems.ai", target: "_blank", rel: "noopener noreferrer", className: linkClass, children: "Status" }) }), ContactModal && (_jsx("li", { children: _jsx("button", { onClick: () => setShowContact(true), className: linkClass, children: "Contact" }) }))] })] })] }), _jsx("div", { className: "mt-8 border-t border-gray-800 pt-8 text-center text-sm text-gray-400", children: _jsxs("p", { children: ["\u00A9 ", new Date().getFullYear(), " RFS LLC. All rights reserved."] }) })] }), ContactModal && (_jsx(ContactModal, { isOpen: showContact, onClose: () => setShowContact(false) }))] }));
46
+ }) })] }), _jsxs("div", { children: [_jsx("h3", { className: headingClass, children: "Company" }), _jsxs("ul", { className: "space-y-2 text-sm", children: [_jsx("li", { children: _jsx(Link, Object.assign({ href: researchHref }, researchLinkProps, { className: linkClass, children: "Research" })) }), _jsx("li", { children: _jsx(Link, { href: "https://xbrlkit.com", target: "_blank", rel: "noopener noreferrer", className: linkClass, children: "xbrlkit viewer" }) }), _jsx("li", { children: _jsx(Link, Object.assign({ href: resolvedBlogHref }, blogLinkProps, { className: linkClass, children: "Blog" })) }), _jsx("li", { children: _jsx(Link, Object.assign({ href: `${companyBase}/pages/privacy` }, companyLinkProps, { className: linkClass, children: "Privacy" })) }), _jsx("li", { children: _jsx(Link, Object.assign({ href: `${companyBase}/pages/terms` }, companyLinkProps, { className: linkClass, children: "Terms" })) }), _jsx("li", { children: _jsx(Link, { href: "https://trust.robosystems.ai", target: "_blank", rel: "noopener noreferrer", className: linkClass, children: "Trust Center" }) }), _jsx("li", { children: _jsx(Link, { href: "https://status.robosystems.ai", target: "_blank", rel: "noopener noreferrer", className: linkClass, children: "Status" }) }), ContactModal && (_jsx("li", { children: _jsx("button", { onClick: () => setShowContact(true), className: linkClass, children: "Contact" }) }))] })] })] }), _jsx("div", { className: "mt-8 border-t border-gray-800 pt-8 text-center text-sm text-gray-400", children: _jsxs("p", { children: ["\u00A9 ", new Date().getFullYear(), " RFS LLC. All rights reserved."] }) })] }), ContactModal && (_jsx(ContactModal, { isOpen: showContact, onClose: () => setShowContact(false) }))] }));
47
47
  }
@@ -1,19 +0,0 @@
1
- import type { CoverageItem } from './types';
2
- export interface CoverageBrowserProps {
3
- items: CoverageItem[];
4
- /** Base path for card links, forwarded to the grid (defaults to /research). */
5
- hrefBase?: string;
6
- /** Override the search box placeholder. */
7
- placeholder?: string;
8
- }
9
- /**
10
- * The research listing with a live client-side filter above it. A drop-in
11
- * replacement for {@link CoverageGrid}: the full grid renders on first paint
12
- * (so the server-rendered SEO page keeps every card in its HTML), and typing
13
- * narrows it in the browser. Matches ticker, company, title, and tags —
14
- * someone who types "Green Thumb" finds GTBIF without knowing the symbol.
15
- *
16
- * Filtering the whole in-memory catalog is fine at this scale; when the
17
- * catalog outgrows a single fetch this is where server-backed search slots in.
18
- */
19
- export declare function CoverageBrowser({ items, hrefBase, placeholder, }: CoverageBrowserProps): import("react").JSX.Element;
@@ -1,35 +0,0 @@
1
- 'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { TextInput } from 'flowbite-react';
4
- import { useMemo, useState } from 'react';
5
- import { HiSearch } from 'react-icons/hi';
6
- import { CoverageGrid } from './CoverageGrid';
7
- /**
8
- * The research listing with a live client-side filter above it. A drop-in
9
- * replacement for {@link CoverageGrid}: the full grid renders on first paint
10
- * (so the server-rendered SEO page keeps every card in its HTML), and typing
11
- * narrows it in the browser. Matches ticker, company, title, and tags —
12
- * someone who types "Green Thumb" finds GTBIF without knowing the symbol.
13
- *
14
- * Filtering the whole in-memory catalog is fine at this scale; when the
15
- * catalog outgrows a single fetch this is where server-backed search slots in.
16
- */
17
- export function CoverageBrowser({ items, hrefBase, placeholder = 'Search by ticker, company, or title…', }) {
18
- const [query, setQuery] = useState('');
19
- const filtered = useMemo(() => {
20
- const term = query.trim().toLowerCase();
21
- if (!term)
22
- return items;
23
- return items.filter((item) => item.ticker.toLowerCase().includes(term) ||
24
- item.company.toLowerCase().includes(term) ||
25
- item.title.toLowerCase().includes(term) ||
26
- item.tags.some((tag) => tag.toLowerCase().includes(term)));
27
- }, [items, query]);
28
- // Empty catalog: let the grid render its own "no coverage yet" state, no
29
- // point showing a search box over nothing.
30
- if (!items.length) {
31
- return _jsx(CoverageGrid, { items: items, hrefBase: hrefBase });
32
- }
33
- const term = query.trim();
34
- return (_jsxs("div", { className: "space-y-6", children: [_jsx(TextInput, { icon: HiSearch, type: "search", value: query, onChange: (e) => setQuery(e.target.value), placeholder: placeholder, "aria-label": "Search research" }), term && filtered.length === 0 ? (_jsxs("p", { className: "py-12 text-center text-gray-500 dark:text-gray-400", children: ["No results for \u201C", term, "\u201D."] })) : (_jsx(CoverageGrid, { items: filtered, hrefBase: hrefBase }))] }));
35
- }
@@ -1,12 +0,0 @@
1
- import type { CoverageItem } from './types';
2
- /**
3
- * A coverage tile: full-bleed thumbnail on top, then ticker/label, title, summary, and
4
- * prior-report count. Self-contained (not the shared Flowbite Card) so the cover image
5
- * sits flush at the top of every card regardless of how much text each one has — the
6
- * shared card theme centers its content (`justify-center`), which left the covers at
7
- * different heights across a row.
8
- */
9
- export declare function CoverageCard({ item, hrefBase, }: {
10
- item: CoverageItem;
11
- hrefBase?: string;
12
- }): import("react").JSX.Element;
@@ -1,13 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import Link from 'next/link';
3
- /**
4
- * A coverage tile: full-bleed thumbnail on top, then ticker/label, title, summary, and
5
- * prior-report count. Self-contained (not the shared Flowbite Card) so the cover image
6
- * sits flush at the top of every card regardless of how much text each one has — the
7
- * shared card theme centers its content (`justify-center`), which left the covers at
8
- * different heights across a row.
9
- */
10
- export function CoverageCard({ item, hrefBase = '/research', }) {
11
- var _a;
12
- return (_jsx(Link, { href: `${hrefBase}/${item.ticker.toLowerCase()}`, className: "group block h-full", children: _jsxs("div", { className: "flex h-full flex-col overflow-hidden rounded-lg border border-gray-200 bg-white/80 shadow-lg backdrop-blur-sm transition-shadow duration-300 hover:shadow-xl dark:border-gray-700 dark:bg-zinc-950", children: [item.assets.thumbnail && (_jsx("img", { src: item.assets.thumbnail, alt: item.title, className: "aspect-video w-full object-cover" })), _jsxs("div", { className: "flex flex-1 flex-col gap-2 p-5", children: [_jsxs("div", { className: "flex items-center gap-2 text-xs font-medium text-gray-500 dark:text-gray-400", children: [_jsx("span", { className: "rounded bg-cyan-500/10 px-2 py-0.5 font-semibold text-cyan-600 dark:text-cyan-400", children: item.ticker }), item.coverage_label && _jsx("span", { children: item.coverage_label }), _jsx("span", { className: "ml-auto", children: (_a = item.date) === null || _a === void 0 ? void 0 : _a.slice(0, 10) })] }), _jsx("h3", { className: "text-lg font-bold text-gray-900 group-hover:text-cyan-600 dark:text-white dark:group-hover:text-cyan-400", children: item.title }), _jsx("p", { className: "line-clamp-3 text-sm text-gray-600 dark:text-gray-300", children: item.summary }), item.history.length > 0 && (_jsxs("p", { className: "mt-auto pt-1 text-xs text-gray-400", children: ["+", item.history.length, " prior", ' ', item.history.length === 1 ? 'report' : 'reports'] }))] })] }) }));
13
- }
@@ -1,6 +0,0 @@
1
- import type { CoverageItem } from './types';
2
- /** Responsive grid of coverage cards. */
3
- export declare function CoverageGrid({ items, hrefBase, }: {
4
- items: CoverageItem[];
5
- hrefBase?: string;
6
- }): import("react").JSX.Element;
@@ -1,9 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { CoverageCard } from './CoverageCard';
3
- /** Responsive grid of coverage cards. */
4
- export function CoverageGrid({ items, hrefBase, }) {
5
- if (!items.length) {
6
- return (_jsx("p", { className: "py-12 text-center text-gray-500 dark:text-gray-400", children: "No coverage yet \u2014 check back soon." }));
7
- }
8
- return (_jsx("div", { className: "grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3", children: items.map((item) => (_jsx(CoverageCard, { item: item, hrefBase: hrefBase }, item.ticker))) }));
9
- }
@@ -1,5 +0,0 @@
1
- import type { CoverageVersion } from './types';
2
- /** The continuing-coverage timeline: prior dated reports with links to their archived assets. */
3
- export declare function CoverageHistory({ history }: {
4
- history: CoverageVersion[];
5
- }): import("react").JSX.Element;
@@ -1,7 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- /** The continuing-coverage timeline: prior dated reports with links to their archived assets. */
3
- export function CoverageHistory({ history }) {
4
- if (!history.length)
5
- return null;
6
- return (_jsxs("section", { className: "mt-12 border-t border-gray-200 pt-8 dark:border-gray-800", children: [_jsx("h2", { className: "mb-4 text-xl font-bold text-gray-900 dark:text-white", children: "Coverage history" }), _jsx("ol", { className: "space-y-3", children: history.map((h) => (_jsxs("li", { className: "flex flex-col gap-1 rounded-lg border border-gray-200 p-4 sm:flex-row sm:items-center sm:gap-4 dark:border-gray-800", children: [_jsx("span", { className: "shrink-0 rounded bg-gray-100 px-2 py-0.5 text-xs font-semibold text-gray-600 dark:bg-gray-800 dark:text-gray-300", children: h.version }), _jsxs("span", { className: "flex-1 text-sm text-gray-700 dark:text-gray-200", children: [h.title || 'Prior report', h.legacy_ticker && (_jsxs("em", { className: "text-gray-400", children: [" (as ", h.legacy_ticker, ")"] }))] }), _jsxs("span", { className: "flex gap-3 text-xs", children: [(h.youtube_url || h.assets.video) && (_jsx("a", { href: h.youtube_url || h.assets.video, className: "text-cyan-600 hover:underline dark:text-cyan-400", children: "Video" })), h.assets.podcast_mp3 && (_jsx("a", { href: h.assets.podcast_mp3, className: "text-cyan-600 hover:underline dark:text-cyan-400", children: "Podcast" })), h.assets.brief && (_jsx("a", { href: h.assets.brief, className: "text-cyan-600 hover:underline dark:text-cyan-400", children: "Brief" }))] })] }, h.version))) })] }));
7
- }
@@ -1,16 +0,0 @@
1
- import type { CoverageItem } from './types';
2
- /**
3
- * The full coverage report: native video, the Q&A podcast (surfaced as a prominent
4
- * "Listen" card right under the video), then the brief rendered from markdown (its own
5
- * leading H1 is stripped — we render the title above it), and the continuing-coverage
6
- * history. Works in a server component (SSR'd for SEO) or a client one.
7
- * Theme-aware: readable in both light and dark. The prose body sets an explicit
8
- * light + `dark:` color for every element it renders (headings, p, strong, em, links,
9
- * lists, code, blockquote, hr, tables) rather than relying on `prose-invert` /
10
- * `dark:prose-invert` — the latter doesn't resolve from a `.dark` class in the
11
- * Tailwind v4 + typography-plugin setup these apps use.
12
- */
13
- export declare function ResearchArticle({ item, briefMarkdown, }: {
14
- item: CoverageItem;
15
- briefMarkdown?: string;
16
- }): import("react").JSX.Element;
@@ -1,25 +0,0 @@
1
- import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
- import ReactMarkdown from 'react-markdown';
3
- import remarkGfm from 'remark-gfm';
4
- import { youtubeId } from './catalog';
5
- import { CoverageHistory } from './CoverageHistory';
6
- /**
7
- * The full coverage report: native video, the Q&A podcast (surfaced as a prominent
8
- * "Listen" card right under the video), then the brief rendered from markdown (its own
9
- * leading H1 is stripped — we render the title above it), and the continuing-coverage
10
- * history. Works in a server component (SSR'd for SEO) or a client one.
11
- * Theme-aware: readable in both light and dark. The prose body sets an explicit
12
- * light + `dark:` color for every element it renders (headings, p, strong, em, links,
13
- * lists, code, blockquote, hr, tables) rather than relying on `prose-invert` /
14
- * `dark:prose-invert` — the latter doesn't resolve from a `.dark` class in the
15
- * Tailwind v4 + typography-plugin setup these apps use.
16
- */
17
- export function ResearchArticle({ item, briefMarkdown, }) {
18
- var _a;
19
- const body = (briefMarkdown || '').replace(/^#\s.*(\r?\n)+/, '');
20
- const ytId = youtubeId(item.youtube_url);
21
- const podcastYtId = youtubeId(item.podcast_youtube_url);
22
- return (_jsxs("article", { className: "mx-auto max-w-3xl", children: [_jsxs("div", { className: "mb-3 flex flex-wrap items-center gap-2 text-sm text-gray-500 dark:text-gray-400", children: [_jsxs("span", { className: "rounded bg-cyan-500/10 px-2 py-0.5 font-semibold text-cyan-600 dark:text-cyan-400", children: [item.company, " \u00B7 ", item.ticker] }), item.coverage_label && _jsx("span", { children: item.coverage_label }), _jsx("span", { children: (_a = item.date) === null || _a === void 0 ? void 0 : _a.slice(0, 10) })] }), _jsx("h1", { className: "mb-6 text-3xl font-bold text-gray-900 dark:text-white", children: item.title }), ytId ? (_jsx("div", { className: "mb-8 aspect-video w-full overflow-hidden rounded-xl bg-black", children: _jsx("iframe", { className: "h-full w-full", src: `https://www.youtube.com/embed/${ytId}`, title: item.title, loading: "lazy", allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share", allowFullScreen: true }) })) : (item.assets.video && (_jsx("video", { controls: true, poster: item.assets.thumbnail, src: item.assets.video, className: "mb-8 aspect-video w-full rounded-xl bg-black" }))), (item.assets.podcast_mp3 || podcastYtId) && (_jsx("section", { className: "mb-8", children: item.assets.podcast_mp3 ? (_jsxs("div", { className: "rounded-xl border border-cyan-500/30 bg-cyan-50/60 p-4 dark:bg-gray-900/50", children: [_jsxs("div", { className: "mb-2 flex items-center justify-between gap-3", children: [_jsx("p", { className: "text-sm font-semibold text-cyan-600 dark:text-cyan-400", children: "\uD83C\uDF99 Listen \u2014 Q&A podcast" }), podcastYtId && (_jsx("a", { href: item.podcast_youtube_url, target: "_blank", rel: "noopener noreferrer", className: "shrink-0 text-xs text-cyan-600 hover:underline dark:text-cyan-400", children: "Watch on YouTube \u2197" }))] }), _jsx("audio", { controls: true, preload: "none", src: item.assets.podcast_mp3, className: "w-full" })] })) : (
23
- // no MP3 yet — fall back to the YouTube video player
24
- _jsxs(_Fragment, { children: [_jsx("h2", { className: "mb-3 text-xl font-bold text-gray-900 dark:text-white", children: "Listen \u2014 Q&A podcast" }), _jsx("div", { className: "aspect-video w-full overflow-hidden rounded-xl bg-black", children: _jsx("iframe", { className: "h-full w-full", src: `https://www.youtube.com/embed/${podcastYtId}`, title: `${item.title} — Q&A podcast`, loading: "lazy", allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share", allowFullScreen: true }) })] })) })), body && (_jsx("div", { className: "prose prose-lg prose-headings:font-heading prose-headings:font-bold prose-headings:text-gray-900 dark:prose-headings:text-white prose-p:text-gray-700 dark:prose-p:text-gray-300 prose-p:leading-relaxed prose-a:text-cyan-600 dark:prose-a:text-cyan-400 prose-a:no-underline hover:prose-a:text-cyan-500 dark:hover:prose-a:text-cyan-300 prose-strong:text-gray-900 dark:prose-strong:text-white prose-strong:font-semibold prose-em:text-gray-700 dark:prose-em:text-gray-300 prose-code:text-cyan-700 dark:prose-code:text-cyan-400 prose-code:bg-gray-100 dark:prose-code:bg-gray-800 prose-code:px-2 prose-code:py-1 prose-code:rounded prose-pre:bg-gray-900 prose-pre:border prose-pre:border-gray-800 prose-blockquote:border-l-cyan-500 prose-blockquote:text-gray-600 dark:prose-blockquote:text-gray-400 prose-blockquote:italic prose-ul:text-gray-700 dark:prose-ul:text-gray-300 prose-ol:text-gray-700 dark:prose-ol:text-gray-300 prose-li:marker:text-cyan-500 prose-hr:border-gray-200 dark:prose-hr:border-gray-800 prose-table:border-gray-300 dark:prose-table:border-gray-700 prose-th:bg-gray-100 dark:prose-th:bg-gray-900 prose-th:text-gray-900 dark:prose-th:text-white prose-td:text-gray-700 dark:prose-td:text-gray-300 max-w-none", children: _jsx(ReactMarkdown, { remarkPlugins: [remarkGfm], children: body }) })), _jsx(CoverageHistory, { history: item.history }), _jsxs("footer", { className: "mt-12 border-t border-gray-200 pt-6 dark:border-gray-800", children: [_jsx("p", { className: "text-sm text-gray-500 dark:text-gray-400", children: "Audio & music produced with ElevenLabs." }), _jsxs("a", { href: "https://elevenlabs.io/startup-grants", target: "_blank", rel: "noopener noreferrer", "aria-label": "Backed by the ElevenLabs Grants program", className: "mt-3 inline-block", children: [_jsx("img", { src: "/images/logos/elevenlabs-grants.webp", alt: "Backed by the ElevenLabs Grants program", width: 200, className: "h-auto dark:hidden" }), _jsx("img", { src: "/images/logos/elevenlabs-grants-white.webp", alt: "Backed by the ElevenLabs Grants program", width: 200, className: "hidden h-auto dark:block" })] }), _jsxs("p", { className: "mt-2 text-xs text-gray-400 dark:text-gray-500", children: ["Using ElevenLabs yourself? Our", ' ', _jsx("a", { href: "https://try.elevenlabs.io/v9z3wzm97gk3", target: "_blank", rel: "noopener noreferrer", className: "hover:underline", children: "referral link" }), ' ', "costs you nothing extra and supports this research."] })] })] }));
25
- }
@@ -1,14 +0,0 @@
1
- import type { Catalog, CoverageItem } from './types';
2
- export declare const CATALOG_URL: string;
3
- /** Fetch the full catalog. `revalidate` (seconds) drives Next ISR on the server. */
4
- export declare function fetchCatalog(revalidate?: number): Promise<Catalog>;
5
- /** All coverage items, newest first (the catalog is already sorted, but be defensive). */
6
- export declare function getAllCoverage(revalidate?: number): Promise<CoverageItem[]>;
7
- /** One company's coverage (latest + history), or null if not in the catalog. */
8
- export declare function getCoverage(ticker: string, revalidate?: number): Promise<CoverageItem | null>;
9
- /** Tickers for generateStaticParams(). */
10
- export declare function getCoverageTickers(): Promise<string[]>;
11
- /** Fetch a brief's raw markdown (rendered to HTML by ResearchArticle). */
12
- export declare function fetchBrief(url: string, revalidate?: number): Promise<string>;
13
- /** Extract a YouTube video id from a youtu.be / watch?v= / embed URL (or null). */
14
- export declare function youtubeId(url?: string | null): string | null;
@@ -1,42 +0,0 @@
1
- // Data access for the research catalog. The catalog is a single public JSON file served
2
- // via the CloudFront CDN, produced by `just reindex` in robosystems-content-machine.
3
- // Fetched server-side (SSG/ISR) in the public site and client-side in the logged-in app.
4
- export const CATALOG_URL = process.env.NEXT_PUBLIC_RESEARCH_CATALOG_URL ||
5
- 'https://assets.robosystems.ai/content/index.json';
6
- /** Fetch the full catalog. `revalidate` (seconds) drives Next ISR on the server. */
7
- export async function fetchCatalog(revalidate = 3600) {
8
- const res = await fetch(CATALOG_URL, { next: { revalidate } });
9
- if (!res.ok)
10
- throw new Error(`Research catalog fetch failed: ${res.status}`);
11
- return (await res.json());
12
- }
13
- /** All coverage items, newest first (the catalog is already sorted, but be defensive). */
14
- export async function getAllCoverage(revalidate = 3600) {
15
- const { items } = await fetchCatalog(revalidate);
16
- return [...items].sort((a, b) => (b.date || '').localeCompare(a.date || ''));
17
- }
18
- /** One company's coverage (latest + history), or null if not in the catalog. */
19
- export async function getCoverage(ticker, revalidate = 3600) {
20
- const { items } = await fetchCatalog(revalidate);
21
- const t = ticker.toUpperCase();
22
- return items.find((i) => i.ticker.toUpperCase() === t) || null;
23
- }
24
- /** Tickers for generateStaticParams(). */
25
- export async function getCoverageTickers() {
26
- const { items } = await fetchCatalog();
27
- return items.map((i) => i.ticker);
28
- }
29
- /** Fetch a brief's raw markdown (rendered to HTML by ResearchArticle). */
30
- export async function fetchBrief(url, revalidate = 3600) {
31
- const res = await fetch(url, { next: { revalidate } });
32
- if (!res.ok)
33
- throw new Error(`Brief fetch failed: ${res.status}`);
34
- return await res.text();
35
- }
36
- /** Extract a YouTube video id from a youtu.be / watch?v= / embed URL (or null). */
37
- export function youtubeId(url) {
38
- if (!url)
39
- return null;
40
- const m = url.match(/(?:youtu\.be\/|[?&]v=|embed\/)([A-Za-z0-9_-]{6,})/);
41
- return m ? m[1] : null;
42
- }
@@ -1,7 +0,0 @@
1
- export { CATALOG_URL, fetchBrief, fetchCatalog, getAllCoverage, getCoverage, getCoverageTickers, youtubeId, } from './catalog';
2
- export { CoverageBrowser, type CoverageBrowserProps } from './CoverageBrowser';
3
- export { CoverageCard } from './CoverageCard';
4
- export { CoverageGrid } from './CoverageGrid';
5
- export { CoverageHistory } from './CoverageHistory';
6
- export { ResearchArticle } from './ResearchArticle';
7
- export * from './types';
package/research/index.js DELETED
@@ -1,10 +0,0 @@
1
- // Research portal module — shared across robosystems-app (public/SEO) and
2
- // roboinvestor-app (logged-in). Data layer reads the S3 catalog produced by
3
- // robosystems-content-machine; components render the coverage + continuing-coverage thread.
4
- export { CATALOG_URL, fetchBrief, fetchCatalog, getAllCoverage, getCoverage, getCoverageTickers, youtubeId, } from './catalog';
5
- export { CoverageBrowser } from './CoverageBrowser';
6
- export { CoverageCard } from './CoverageCard';
7
- export { CoverageGrid } from './CoverageGrid';
8
- export { CoverageHistory } from './CoverageHistory';
9
- export { ResearchArticle } from './ResearchArticle';
10
- export * from './types';
@@ -1,38 +0,0 @@
1
- export interface CoverageAssets {
2
- video?: string;
3
- short?: string;
4
- podcast_mp3?: string;
5
- podcast_mp4?: string;
6
- brief?: string;
7
- thumbnail?: string;
8
- }
9
- export interface CoverageVersion {
10
- version: string;
11
- date?: string;
12
- title?: string;
13
- legacy_ticker?: string;
14
- youtube_url?: string;
15
- assets: CoverageAssets;
16
- }
17
- export interface CoverageItem {
18
- ticker: string;
19
- company: string;
20
- title: string;
21
- summary: string;
22
- tags: string[];
23
- campaign?: string | null;
24
- campaign_slug?: string | null;
25
- coverage_label?: string | null;
26
- date: string;
27
- version: string;
28
- youtube_url?: string;
29
- short_youtube_url?: string;
30
- podcast_youtube_url?: string;
31
- assets: CoverageAssets;
32
- history: CoverageVersion[];
33
- }
34
- export interface Catalog {
35
- generated: string;
36
- count: number;
37
- items: CoverageItem[];
38
- }
package/research/types.js DELETED
@@ -1,4 +0,0 @@
1
- // Shared types for the equity-research catalog produced by robosystems-content-machine
2
- // (https://assets.robosystems.ai/content/index.json). The company is the durable
3
- // entity; each run is a dated quarterly report version (latest + history[]).
4
- export {};