diff --git a/apps/api/src/trpc/routers/sources.ts b/apps/api/src/trpc/routers/sources.ts index 73295c2..9df1c50 100644 --- a/apps/api/src/trpc/routers/sources.ts +++ b/apps/api/src/trpc/routers/sources.ts @@ -31,11 +31,9 @@ export const sourcesRouter = createTRPCRouter({ return getSourceCategoryShares(ctx.db, input); }), - getPublicationGraph: protectedProcedure - .input(getPublicationsSchema) - .query(async ({ ctx, input }) => { - return getSourcePublicationGraph(ctx.db, input); - }), + getPublications: protectedProcedure.input(getPublicationsSchema).query(async ({ ctx, input }) => { + return getSourcePublicationGraph(ctx.db, input); + }), list: protectedProcedure.query(async ({ ctx }) => getSources(ctx.db)), diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json index 3f49a41..82188c4 100644 --- a/apps/api/tsconfig.json +++ b/apps/api/tsconfig.json @@ -1,4 +1,11 @@ { + "compilerOptions": { + "paths": { + "#api/*": ["./src/*"], + "#db/*": ["../../packages/db/src/*"], + "#domain/*": ["../../packages/domain/src/*"] + } + }, "extends": "@basango/tsconfig/base.json", "include": ["src"] } diff --git a/apps/crawler/tsconfig.json b/apps/crawler/tsconfig.json index 3f49a41..cdddfed 100644 --- a/apps/crawler/tsconfig.json +++ b/apps/crawler/tsconfig.json @@ -1,4 +1,10 @@ { + "compilerOptions": { + "paths": { + "#crawler/*": ["./src/*"], + "#domain/*": ["../../packages/domain/src/*"] + } + }, "extends": "@basango/tsconfig/base.json", "include": ["src"] } diff --git a/apps/dashboard/src/app/[locale]/(app)/(sidebar)/sources/page.tsx b/apps/dashboard/src/app/[locale]/(app)/(sidebar)/sources/page.tsx index 058212b..6af136c 100644 --- a/apps/dashboard/src/app/[locale]/(app)/(sidebar)/sources/page.tsx +++ b/apps/dashboard/src/app/[locale]/(app)/(sidebar)/sources/page.tsx @@ -1,9 +1,9 @@ -import { Source } from "@basango/domain/models/sources"; import { Button } from "@basango/ui/components/button"; import { PlusIcon } from "lucide-react"; import { Metadata } from "next"; import Link from "next/link"; +import { RouterOutputs } from "#api/trpc/routers/_app"; import { SourceCreateModal } from "#dashboard/components/modals/source-create-modal"; import { PageLayout } from "#dashboard/components/shell/page-layout"; import { SourceCard } from "#dashboard/components/source-card"; @@ -13,11 +13,13 @@ export const metadata: Metadata = { title: "Sources | Basango Dashboard", }; +type Source = RouterOutputs["sources"]["list"][number]; + export default async function Page() { const queryClient = getQueryClient(); - prefetch(trpc.sources.get.queryOptions()); - const sources: Source[] = await queryClient.fetchQuery(trpc.sources.get.queryOptions()); + prefetch(trpc.sources.list.queryOptions()); + const sources = await queryClient.fetchQuery(trpc.sources.list.queryOptions()); return ( diff --git a/apps/dashboard/src/components/article-card.tsx b/apps/dashboard/src/components/article-card.tsx index 741b76c..7aec1e6 100644 --- a/apps/dashboard/src/components/article-card.tsx +++ b/apps/dashboard/src/components/article-card.tsx @@ -23,18 +23,8 @@ type ArticleCardProps = { article: Article; }; -function getDescription(article: Article) { - return ( - article.metadata?.description ?? - article.excerpt ?? - "No description was provided for this article." - ); -} - export function ArticleCard({ article }: ArticleCardProps) { const [copied, setCopied] = React.useState(false); - const description = getDescription(article); - const imageUrl = article.image ?? undefined; const copyLink = React.useCallback(async () => { try { @@ -50,12 +40,12 @@ export function ArticleCard({ article }: ArticleCardProps) {
- {imageUrl ? ( + {article.image ? ( {article.title} ) : (
@@ -63,7 +53,7 @@ export function ArticleCard({ article }: ArticleCardProps) {
)}
- {article.sourceName} + {article.source?.name}
@@ -103,14 +93,18 @@ export function ArticleCard({ article }: ArticleCardProps) { {article.title} -

{description}

+

+ {article.metadata?.description ?? + article.excerpt ?? + "No description was provided for this article."} +

{formatDate(article.publishedAt.toISOString(), "PP", false)} - {formatRelativeTime(new Date(article.publishedAt))} + {formatRelativeTime(article.publishedAt)}
{article.readingTime} min
diff --git a/apps/dashboard/src/components/charts/area-chart.tsx b/apps/dashboard/src/components/charts/area-chart.tsx index ffbbfae..eb61f25 100644 --- a/apps/dashboard/src/components/charts/area-chart.tsx +++ b/apps/dashboard/src/components/charts/area-chart.tsx @@ -5,11 +5,11 @@ import { Area, AreaChart as BaseAreachart, CartesianGrid, XAxis, YAxis } from "r import { formatDate, formatNumber } from "#dashboard/utils/utils"; -type AreaChartProps = { - data: unknown; +type AreaChartProps = { + data: T[]; }; -export function AreaChart({ data }: AreaChartProps) { +export function AreaChart({ data }: AreaChartProps) { return ( diff --git a/apps/dashboard/src/components/charts/articles/publication-graph-chart.tsx b/apps/dashboard/src/components/charts/articles/publication-graph-chart.tsx index e763c4c..118767a 100644 --- a/apps/dashboard/src/components/charts/articles/publication-graph-chart.tsx +++ b/apps/dashboard/src/components/charts/articles/publication-graph-chart.tsx @@ -1,4 +1,3 @@ -// @ts-nocheck "use client"; import { @@ -33,7 +32,7 @@ export function PublicationGraphChart() { const period = useChartPeriodFilter(); const { data } = useQuery( - trpc.articles.getPublicationGraph.queryOptions({ + trpc.articles.getPublications.queryOptions({ range: period.range, }), ); @@ -45,19 +44,19 @@ export function PublicationGraphChart() { {formatNumber(data?.meta?.current)} articles
- + vs previous
- +
- + @@ -68,7 +67,7 @@ export function PublicationGraphChart() { {formatNumber(data?.meta?.current)} vs {formatNumber(data?.meta?.previous)} articles - + period {data?.meta?.previous === 0 && data?.meta?.current === 0 && ( (no articles yet) diff --git a/apps/dashboard/src/components/charts/articles/source-distribution-chart.tsx b/apps/dashboard/src/components/charts/articles/source-distribution-chart.tsx index cd97a2a..9cb3614 100644 --- a/apps/dashboard/src/components/charts/articles/source-distribution-chart.tsx +++ b/apps/dashboard/src/components/charts/articles/source-distribution-chart.tsx @@ -1,4 +1,3 @@ -// @ts-nocheck "use client"; import { diff --git a/apps/dashboard/src/components/charts/bar-chart.tsx b/apps/dashboard/src/components/charts/bar-chart.tsx index 37116fb..fcefcf1 100644 --- a/apps/dashboard/src/components/charts/bar-chart.tsx +++ b/apps/dashboard/src/components/charts/bar-chart.tsx @@ -5,11 +5,11 @@ import { Bar, BarChart as BaseBarChart, CartesianGrid, XAxis } from "recharts"; import { formatDate } from "#dashboard/utils/utils"; -type BarChartProps = { - data: unknown; +type BarChartProps = { + data: T[]; }; -export function BarChart({ data }: BarChartProps) { +export function BarChart({ data }: BarChartProps) { return ( diff --git a/apps/dashboard/src/components/charts/chart-filters.tsx b/apps/dashboard/src/components/charts/chart-filters.tsx index 4bda641..a92b57b 100644 --- a/apps/dashboard/src/components/charts/chart-filters.tsx +++ b/apps/dashboard/src/components/charts/chart-filters.tsx @@ -25,8 +25,6 @@ const DEFAULT_PERIOD_OPTIONS = [ { label: "Last 12 months", value: 365 }, ] as const; -type DateInput = number | Date | null | undefined; - const createRangeFromDays = (days: number): DateRange => { const end = new Date(); @@ -75,7 +73,7 @@ export function useChartPeriodFilter(options: ChartPeriodFilterOptions = {}) { return undefined; }, [from, to]); - const range = useMemo(() => { + const calendarRange: DateRange | undefined = useMemo(() => { if (from && to) { return { from, to }; } @@ -83,7 +81,10 @@ export function useChartPeriodFilter(options: ChartPeriodFilterOptions = {}) { return defaultRange; }, [defaultRange, from, to]); + const range = useMemo(() => formatDomainDateRange(calendarRange), [calendarRange]); + return { + calendarRange, defaultDays, keys: { fromKey, toKey }, range, @@ -118,19 +119,22 @@ export function ChartPeriodPicker({ paramKey = "chartPeriod", disabled, }: ChartPeriodPickerProps & { disabled?: boolean }) { - const { range, selectedRange, keys, setState } = useChartPeriodFilter({ defaultDays, paramKey }); + const { calendarRange, selectedRange, keys, setState } = useChartPeriodFilter({ + defaultDays, + paramKey, + }); const [open, setOpen] = useState(false); const selectValue = useMemo(() => { - if (!range?.from || !range?.to) { + if (!calendarRange?.from || !calendarRange?.to) { return "custom"; } - const diff = differenceInCalendarDays(range.to, range.from) + 1; + const diff = differenceInCalendarDays(calendarRange.to, calendarRange.from) + 1; const match = options.find((option) => option.value === diff); return match ? String(match.value) : "custom"; - }, [options, range]); + }, [calendarRange, options]); const handlePresetChange = (value: string) => { if (value === "custom") { @@ -160,7 +164,7 @@ export function ChartPeriodPicker({ }; const displayLabel = - formatDateRange(range) ?? + formatDateRange(calendarRange) ?? options.find((option) => String(option.value) === selectValue)?.label ?? "Select range"; @@ -196,7 +200,7 @@ export function ChartPeriodPicker({ mode="range" numberOfMonths={2} onSelect={handleCalendarSelect} - selected={(selectedRange ?? range) as DateRange | undefined} + selected={(selectedRange ?? calendarRange) as DateRange | undefined} />
@@ -252,8 +256,14 @@ export function ChartLimitToggle({ ); } -function formatDateRange(range?: { from?: DateInput; to?: DateInput }) { +function formatDateRange(range?: DateRange) { if (!range?.from || !range?.to) return null; return `${format(range.from, "MMM d, yyyy")} - ${format(range.to, "MMM d, yyyy")}`; } + +function formatDomainDateRange(range?: DateRange) { + if (!range?.from || !range?.to) return undefined; + + return { end: range.to, start: range.from }; +} diff --git a/apps/dashboard/src/components/charts/sources/category-shares-chart.tsx b/apps/dashboard/src/components/charts/sources/category-shares-chart.tsx index b7a3dc6..85e4e70 100644 --- a/apps/dashboard/src/components/charts/sources/category-shares-chart.tsx +++ b/apps/dashboard/src/components/charts/sources/category-shares-chart.tsx @@ -1,4 +1,3 @@ -// @ts-nocheck "use client"; import { @@ -11,6 +10,7 @@ import { import { useQuery } from "@tanstack/react-query"; import { Bar, BarChart, Legend, ResponsiveContainer, XAxis, YAxis } from "recharts"; +import { RouterOutputs } from "#api/trpc/routers/_app"; import { ChartLimitToggle, useChartLimitFilter } from "#dashboard/components/charts/chart-filters"; import { useTRPC } from "#dashboard/trpc/client"; import { getColorFromName } from "#dashboard/utils/categories"; @@ -19,6 +19,8 @@ type Props = { sourceId: string; }; +type CategoryShare = RouterOutputs["sources"]["getCategoryShares"]["items"][number]; + export function CategorySharesChart({ sourceId }: Props) { const trpc = useTRPC(); const { limit } = useChartLimitFilter(); @@ -29,11 +31,12 @@ export function CategorySharesChart({ sourceId }: Props) { limit, }), ); + const items: CategoryShare[] = data?.items || []; const chartData = [ { name: "Total", - ...Object.fromEntries(data?.items.map((item) => [item.category, item.count])), + ...Object.fromEntries(items.map((item) => [item.category, item.count])), }, ]; @@ -44,7 +47,7 @@ export function CategorySharesChart({ sourceId }: Props) { Category Shares showing top {limit} categories for this source
- +
diff --git a/apps/dashboard/src/components/charts/sources/publication-graph-chart.tsx b/apps/dashboard/src/components/charts/sources/publication-graph-chart.tsx index 4189366..4088196 100644 --- a/apps/dashboard/src/components/charts/sources/publication-graph-chart.tsx +++ b/apps/dashboard/src/components/charts/sources/publication-graph-chart.tsx @@ -1,4 +1,3 @@ -// @ts-nocheck "use client"; import { @@ -37,7 +36,7 @@ export function PublicationGraphChart({ sourceId }: Props) { const period = useChartPeriodFilter(); const { data } = useQuery( - trpc.sources.getPublicationGraph.queryOptions({ + trpc.sources.getPublications.queryOptions({ id: sourceId, range: period.range, }), @@ -50,11 +49,11 @@ export function PublicationGraphChart({ sourceId }: Props) { Publication Graph Showing total crawled articles for the selected period
- + - + diff --git a/apps/dashboard/src/components/forms/source-edit-form.tsx b/apps/dashboard/src/components/forms/source-edit-form.tsx index f30e19e..398a2a6 100644 --- a/apps/dashboard/src/components/forms/source-edit-form.tsx +++ b/apps/dashboard/src/components/forms/source-edit-form.tsx @@ -1,7 +1,7 @@ "use client"; import type { RouterOutputs } from "@basango/api/trpc/routers/_app"; -import { updateSourceSchema } from "@basango/domain/models/sources"; +import { updateSourceSchema } from "@basango/domain/models"; import { Field, FieldDescription, diff --git a/apps/dashboard/src/components/forms/source-form.tsx b/apps/dashboard/src/components/forms/source-form.tsx index f3aac15..47c03b8 100644 --- a/apps/dashboard/src/components/forms/source-form.tsx +++ b/apps/dashboard/src/components/forms/source-form.tsx @@ -1,6 +1,6 @@ "use client"; -import { createSourceSchema } from "@basango/domain/models/sources"; +import { createSourceSchema } from "@basango/domain/models"; import { Field, FieldDescription, diff --git a/apps/dashboard/src/components/source-card.tsx b/apps/dashboard/src/components/source-card.tsx index 1a65ea4..0e46262 100644 --- a/apps/dashboard/src/components/source-card.tsx +++ b/apps/dashboard/src/components/source-card.tsx @@ -1,6 +1,5 @@ "use client"; -import { Source } from "@basango/domain/models/sources"; import { Card, CardContent, @@ -17,6 +16,7 @@ import { } from "@basango/ui/components/chart"; import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"; +import { RouterOutputs } from "#api/trpc/routers/_app"; import { formatDate, formatNumber } from "#dashboard/utils/utils"; const chartConfig = { @@ -29,7 +29,11 @@ const chartConfig = { }, } satisfies ChartConfig; -export function SourceCard({ source }: { source: Source }) { +type Props = { + source: RouterOutputs["sources"]["list"][number]; +}; + +export function SourceCard({ source }: Props) { return ( diff --git a/apps/dashboard/tsconfig.json b/apps/dashboard/tsconfig.json index 6748d46..120672d 100644 --- a/apps/dashboard/tsconfig.json +++ b/apps/dashboard/tsconfig.json @@ -1,5 +1,11 @@ { "compilerOptions": { + "paths": { + "#api/*": ["../api/src/*"], + "#dashboard/*": ["./src/*"], + "#db/*": ["../../packages/db/src/*"], + "#domain/*": ["../../packages/domain/src/*"] + }, "plugins": [ { "name": "next" diff --git a/apps/mobile/package.json b/apps/mobile/package.json index ea11b99..80c8f05 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -35,9 +35,11 @@ "private": true, "scripts": { "android": "expo start --android", + "clean": "rm -rf .expo node_modules", "ios": "expo start --ios", "lint": "expo lint", "start": "expo start", + "typecheck": "tsc --noEmit", "web": "expo start --web" }, "version": "1.0.0" diff --git a/apps/mobile/tsconfig.json b/apps/mobile/tsconfig.json index 28fa6a1..a74e2a2 100644 --- a/apps/mobile/tsconfig.json +++ b/apps/mobile/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "paths": { - "#mobile/*": ["./*"] + "#mobile/*": ["./src/*"] }, "strict": true }, diff --git a/packages/db/src/queries/articles.ts b/packages/db/src/queries/articles.ts index 1e4e4ba..3478d6b 100644 --- a/packages/db/src/queries/articles.ts +++ b/packages/db/src/queries/articles.ts @@ -1,5 +1,6 @@ import { DEFAULT_TIMEZONE } from "@basango/domain/constants"; import { + Article, Distribution, Distributions, ID, @@ -143,7 +144,7 @@ export async function getArticles(db: Database, params: GetArticlesParams) { .orderBy(desc(articles.publishedAt), desc(articles.id)) .limit(pagination.limit + 1); - return buildPaginatedResult(rows, pagination, { + return buildPaginatedResult
(rows, pagination, { date: "publishedAt", id: "id", }); @@ -154,7 +155,7 @@ export async function getArticlesPublicationGraph( params: GetPublicationsParams, ): Promise { const [startDate, endDate] = buildDateRange(params.range); - const [previousRangeStart, previousRangeEnd] = buildPreviousRange([startDate, endDate]); + const [previousStart, previousEnd] = buildPreviousRange([startDate, endDate]); const data = await db.execute(sql` WITH bounds AS ( @@ -193,8 +194,8 @@ export async function getArticlesPublicationGraph( sql` SELECT COALESCE(COUNT(*)::int, 0) AS count FROM article a - WHERE a.published_at >= timezone(${DEFAULT_TIMEZONE}, ${previousRangeStart}) - AND a.published_at <= timezone(${DEFAULT_TIMEZONE}, ${previousRangeEnd}) + WHERE a.published_at >= timezone(${DEFAULT_TIMEZONE}, ${previousStart}) + AND a.published_at <= timezone(${DEFAULT_TIMEZONE}, ${previousEnd}) `, ) .then((res) => res.rows); diff --git a/packages/db/src/queries/sources.ts b/packages/db/src/queries/sources.ts index b796280..99560fc 100644 --- a/packages/db/src/queries/sources.ts +++ b/packages/db/src/queries/sources.ts @@ -24,7 +24,7 @@ export async function getSources(db: Database) { rows.map(async (row) => ({ ...row, articles: await countArticlesBySourceId(db, row.id), - publicationGraph: await getSourcePublicationGraph(db, { id: row.id }), + publications: await getSourcePublicationGraph(db, { id: row.id }), })), ); } diff --git a/packages/db/tsconfig.json b/packages/db/tsconfig.json index c2116b5..933a2ac 100644 --- a/packages/db/tsconfig.json +++ b/packages/db/tsconfig.json @@ -1,4 +1,10 @@ { + "compilerOptions": { + "paths": { + "#db/*": ["./src/*"], + "#domain/*": ["../domain/src/*"] + } + }, "exclude": ["node_modules"], "extends": "@basango/tsconfig/base.json", "include": ["src"] diff --git a/packages/domain/src/models/articles.ts b/packages/domain/src/models/articles.ts index c57b70c..aeef020 100644 --- a/packages/domain/src/models/articles.ts +++ b/packages/domain/src/models/articles.ts @@ -2,6 +2,8 @@ import { z } from "@hono/zod-openapi"; import { idSchema, sentimentSchema } from "#domain/models/shared"; +import { sourceSchema } from "./sources"; + // schemas export const articleMetadataSchema = z.object({ author: z.string().optional().openapi({ @@ -70,11 +72,19 @@ export const articleSchema = z.object({ description: "The date and time when the article was created in the system.", example: "2023-01-01T12:00:00Z", }), + excerpt: z.string().optional().openapi({ + description: "A brief excerpt or summary of the article.", + example: "This article discusses the latest advancements in AI technology.", + }), hash: z.string().min(1).openapi({ description: "The unique hash of the article link.", example: "d41d8cd98f00b204e9800998ecf8427e", }), id: idSchema, + image: z.url().optional().openapi({ + description: "The URL of the main image associated with the article.", + example: "https://example.com/image.jpg", + }), link: z.string().url().openapi({ description: "The URL of the article.", example: "https://example.com/article", @@ -84,6 +94,11 @@ export const articleSchema = z.object({ description: "The publication date of the article as a Date object.", example: "2023-01-01T00:00:00Z", }), + readingTime: z.number().int().min(1).openapi({ + description: "Estimated reading time of the article in minutes.", + example: 5, + }), + source: sourceSchema.optional(), sourceId: z.union([z.uuid(), z.string().min(1)]).openapi({ description: "The unique identifier of the source from which the article was crawled.", example: "b3e1c8f4-5d6a-4c9e-8f1e-2d3c4b5a6f7g", diff --git a/packages/domain/tsconfig.json b/packages/domain/tsconfig.json index c2116b5..1faf88c 100644 --- a/packages/domain/tsconfig.json +++ b/packages/domain/tsconfig.json @@ -1,4 +1,9 @@ { + "compilerOptions": { + "paths": { + "#domain/*": ["./src/*"] + } + }, "exclude": ["node_modules"], "extends": "@basango/tsconfig/base.json", "include": ["src"] diff --git a/packages/logger/tsconfig.json b/packages/logger/tsconfig.json index c390cc9..c2116b5 100644 --- a/packages/logger/tsconfig.json +++ b/packages/logger/tsconfig.json @@ -1,5 +1,5 @@ { "exclude": ["node_modules"], "extends": "@basango/tsconfig/base.json", - "include": ["src/**/*"] + "include": ["src"] } diff --git a/packages/tsconfig/base.json b/packages/tsconfig/base.json index c9c0231..fba6a59 100644 --- a/packages/tsconfig/base.json +++ b/packages/tsconfig/base.json @@ -11,13 +11,6 @@ "moduleDetection": "force", "moduleResolution": "Bundler", "noUncheckedIndexedAccess": true, - "paths": { - "#api/*": ["../../apps/api/src/*"], - "#crawler/*": ["../../apps/crawler/src/*"], - "#dashboard/*": ["../../apps/dashboard/src/*"], - "#db/*": ["../db/src/*"], - "#domain/*": ["../domain/src/*"] - }, "resolveJsonModule": true, "skipLibCheck": true, "strict": true,