Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion app/components/AuthProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { DynamicContextProvider, DynamicWidget, useDynamicContext } from '@dynam
import { EthereumWalletConnectors } from '@dynamic-labs/ethereum';
import { ZeroDevSmartWalletConnectors } from '@dynamic-labs/ethereum-aa';
import { createContext, useContext, useEffect, useState, useCallback, ReactNode } from 'react';
import { trackSignInStarted, trackUserLoggedIn } from '@/lib/analytics';
import { trackSignInStarted, trackUserLoggedIn, trackUserLoggedOut } from '@/lib/analytics';
import { hasValidPromoAccess } from '@/lib/promo-access';

interface SubscriptionData {
Expand Down Expand Up @@ -305,6 +305,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
walletConnectors: [EthereumWalletConnectors, ZeroDevSmartWalletConnectors],
events: {
onLogout: () => {
trackUserLoggedOut();
setIsAuthenticated(false);
setUser(null);
setHasActiveSubscription(false);
Expand Down
2 changes: 1 addition & 1 deletion app/components/LLMChatInline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ export default function AIChatInline({ onOpenTour }: AIChatInlineProps = {}) {
setAttachmentError(null);

// Track LLM question
trackLLMQuestionAsked();
trackLLMQuestionAsked({ isFollowUp: messages.length > 0 });

// Process attachments if any
let processedAttachments: Attachment[] = [];
Expand Down
2 changes: 2 additions & 0 deletions app/components/LLMCommentaryModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import StudyQualityIndicators from "./StudyQualityIndicators";
import { useResults } from "./ResultsContext";
import { useCustomization } from "./CustomizationContext";
import { callLLM, getLLMDescription } from "@/lib/llm-client";
import { trackAIAnalysisRun } from "@/lib/analytics";

type LLMCommentaryModalProps = {
isOpen: boolean;
Expand Down Expand Up @@ -89,6 +90,7 @@ export default function LLMCommentaryModal({
localStorage.setItem(CONSENT_STORAGE_KEY, "true");
setHasConsent(true);
setShowConsentModal(false);
trackAIAnalysisRun();
fetchCommentary();
}
};
Expand Down
3 changes: 2 additions & 1 deletion app/components/StudyResultReveal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { analyzeStudyClientSide, UserStudyResult, determineEffectTypeAndSize } f
import DisclaimerModal from "./DisclaimerModal";
import LLMCommentaryModal from "./LLMCommentaryModal";
import { SavedResult } from "@/lib/results-manager";
import { trackStudyResultReveal } from "@/lib/analytics";
import { trackStudyResultReveal, trackStudyAnalysisStarted } from "@/lib/analytics";

type StudyResultRevealProps = {
studyId: number;
Expand Down Expand Up @@ -80,6 +80,7 @@ export default function StudyResultReveal({ studyId, studyAccession, snps, trait
}, [savedResult]);

const handleRevealClick = () => {
trackStudyAnalysisStarted();
setShowDisclaimer(true);
};

Expand Down
28 changes: 28 additions & 0 deletions app/dna-chat/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { Metadata } from "next";

export const metadata: Metadata = {
title: "DNA Chat - Monadic DNA Explorer",
description: "Ask private AI questions about your saved genetic results. Your DNA data never leaves your device.",
keywords: ["DNA chat", "genetic AI", "private DNA analysis", "personal genomics AI", "DNA questions"],
alternates: {
canonical: "https://explorer.monadicdna.com/dna-chat",
},
openGraph: {
title: "DNA Chat - Monadic DNA Explorer",
description: "Ask private AI questions about your saved genetic results. Your DNA data never leaves your device.",
type: "website",
url: "https://explorer.monadicdna.com/dna-chat",
siteName: "Monadic DNA Explorer",
locale: "en_US",
},
twitter: {
card: "summary_large_image",
title: "DNA Chat - Monadic DNA Explorer",
description: "Ask private AI questions about your saved genetic results. Your DNA data never leaves your device.",
creator: "@MonadicDNA",
},
};

export default function DNAChatLayout({ children }: { children: React.ReactNode }) {
return <>{children}</>;
}
5 changes: 5 additions & 0 deletions app/dna-chat/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,15 @@ import { PremiumPaywall } from "../components/PremiumPaywall";
import LLMChatInline from "../components/LLMChatInline";
import GuidedTour, { hasCompletedTour } from "../components/GuidedTour";
import { dnaChatTour } from "../components/tours/tourContent";
import { trackDNAChatViewed } from "@/lib/analytics";

export default function DNAChatPage() {
const [tourOpen, setTourOpen] = useState(false);

useEffect(() => {
trackDNAChatViewed();
}, []);

useEffect(() => {
if (!hasCompletedTour(dnaChatTour.id)) {
setTourOpen(true);
Expand Down
5 changes: 3 additions & 2 deletions app/explore/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
trackRunAllStarted,
trackQueryRun,
trackExploreTabViewed,
trackSearchModeChanged,
} from "@/lib/analytics";

// Note: Metadata must be exported from layout.tsx or a server component
Expand Down Expand Up @@ -808,7 +809,7 @@ function ExplorePage() {
name="searchMode"
value="similarity"
checked={filters.searchMode === "similarity"}
onChange={(event) => updateFilter("searchMode", event.target.value as "similarity" | "exact")}
onChange={(event) => { const m = event.target.value as "similarity" | "exact"; updateFilter("searchMode", m); trackSearchModeChanged(m); }}
/>
Similarity
</label>
Expand All @@ -818,7 +819,7 @@ function ExplorePage() {
name="searchMode"
value="exact"
checked={filters.searchMode === "exact"}
onChange={(event) => updateFilter("searchMode", event.target.value as "similarity" | "exact")}
onChange={(event) => { const m = event.target.value as "similarity" | "exact"; updateFilter("searchMode", m); trackSearchModeChanged(m); }}
/>
Exact match
</label>
Expand Down
5 changes: 3 additions & 2 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export const metadata: Metadata = {
locale: "en_US",
images: [
{
url: "https://monadicdna.com/og-image.png",
url: "/og-image.png",
width: 1199,
height: 630,
alt: "Monadic DNA Explorer - Private DNA insights from trusted genetic research",
Expand All @@ -45,7 +45,7 @@ export const metadata: Metadata = {
title: "Monadic DNA | Personal DNA insights with privacy, autonomy, and boundless curiosity",
description: "Private DNA insights from trusted genetic research. Learn from your DNA while your data remains private, protected, and entirely in your hands.",
creator: "@MonadicDNA",
images: ["https://monadicdna.com/og-image.png"],
images: ["/og-image.png"],
},
robots: {
index: true,
Expand Down Expand Up @@ -87,6 +87,7 @@ export default function RootLayout({
gtag('config', 'G-HP3FB0GX80', {
anonymize_ip: true,
});
gtag('config', 'AW-777857829');
`}
</Script>
{/* Reddit Pixel */}
Expand Down
28 changes: 28 additions & 0 deletions app/overview-report/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { Metadata } from "next";

export const metadata: Metadata = {
title: "Overview Report - Monadic DNA Explorer",
description: "Generate an AI-powered report synthesizing your saved genetic results into patterns, themes, and suggested next steps.",
keywords: ["DNA overview report", "genetic analysis report", "AI genetics", "personal genomics report"],
alternates: {
canonical: "https://explorer.monadicdna.com/overview-report",
},
openGraph: {
title: "Overview Report - Monadic DNA Explorer",
description: "Generate an AI-powered report synthesizing your saved genetic results into patterns, themes, and suggested next steps.",
type: "website",
url: "https://explorer.monadicdna.com/overview-report",
siteName: "Monadic DNA Explorer",
locale: "en_US",
},
twitter: {
card: "summary_large_image",
title: "Overview Report - Monadic DNA Explorer",
description: "Generate an AI-powered report synthesizing your saved genetic results into patterns, themes, and suggested next steps.",
creator: "@MonadicDNA",
},
};

export default function OverviewReportLayout({ children }: { children: React.ReactNode }) {
return <>{children}</>;
}
5 changes: 5 additions & 0 deletions app/overview-report/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { useResults } from "../components/ResultsContext";
import { hasValidPromoAccess } from "@/lib/promo-access";
import GuidedTour, { hasCompletedTour } from "../components/GuidedTour";
import { overviewReportTour } from "../components/tours/tourContent";
import { trackOverviewReportViewed } from "@/lib/analytics";

export default function OverviewReportPage() {
const router = useRouter();
Expand All @@ -32,6 +33,10 @@ export default function OverviewReportPage() {
return () => window.removeEventListener('premiumAccessUpdated', refreshPromoAccess);
}, []);

useEffect(() => {
trackOverviewReportViewed();
}, []);

useEffect(() => {
if (!hasCompletedTour(overviewReportTour.id)) {
setTourOpen(true);
Expand Down
37 changes: 35 additions & 2 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const metadata: Metadata = {
locale: "en_US",
images: [
{
url: "https://monadicdna.com/og-image.png",
url: "/og-image.png",
width: 1199,
height: 630,
alt: "Monadic DNA Explorer - Private DNA insights from trusted genetic research",
Expand All @@ -28,13 +28,46 @@ export const metadata: Metadata = {
card: "summary_large_image",
title: "Monadic DNA | Personal DNA insights with privacy, autonomy, and boundless curiosity",
description: "Private DNA insights from trusted genetic research. Learn from your DNA while your data remains private, protected, and entirely in your hands.",
images: ["https://monadicdna.com/og-image.png"],
images: ["/og-image.png"],
},
};

const organizationJsonLd = {
"@context": "https://schema.org",
"@type": "Organization",
"name": "Monadic DNA",
"url": "https://explorer.monadicdna.com",
"logo": "https://explorer.monadicdna.com/explorer-logo.png",
"sameAs": ["https://x.com/MonadicDNA"],
};

const websiteJsonLd = {
"@context": "https://schema.org",
"@type": "WebSite",
"name": "Monadic DNA Explorer",
"url": "https://explorer.monadicdna.com",
"description": "Private DNA insights from trusted genetic research.",
"potentialAction": {
"@type": "SearchAction",
"target": {
"@type": "EntryPoint",
"urlTemplate": "https://explorer.monadicdna.com/explore?q={search_term_string}",
},
"query-input": "required name=search_term_string",
},
};

export default function HomePage() {
return (
<div className="app-container">
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(organizationJsonLd) }}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(websiteJsonLd) }}
/>
<MenuBar />
<Suspense>
<LandingClient />
Expand Down
95 changes: 44 additions & 51 deletions app/sitemap.ts
Original file line number Diff line number Diff line change
@@ -1,62 +1,55 @@
import { MetadataRoute } from 'next';
import { executeQuerySingle, executeQuery } from '@/lib/db';

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://monadicdna.com';
const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://explorer.monadicdna.com';
const BATCH_SIZE = 10_000;

// Static pages
const staticPages: MetadataRoute.Sitemap = [
{
url: baseUrl,
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 1.0,
},
{
url: `${baseUrl}/explore`,
lastModified: new Date(),
changeFrequency: 'daily',
priority: 0.9,
},
{
url: `${baseUrl}/dna-chat`,
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 0.8,
},
{
url: `${baseUrl}/overview-report`,
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 0.8,
},
{
url: `${baseUrl}/subscribe`,
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 0.7,
},
];
// generateSitemaps splits the study pages across multiple sitemap files,
// each with up to BATCH_SIZE URLs, staying within the 50K-per-file limit.
export async function generateSitemaps() {
try {
const row = await executeQuerySingle<{ max_id: number }>(
'SELECT MAX(id) AS max_id FROM gwas_catalog',
[]
);
const maxId = row?.max_id ?? 1000;
const count = Math.ceil(maxId / BATCH_SIZE);
// id 0 is reserved for static pages; ids 1..n are study batches
return Array.from({ length: count + 1 }, (_, i) => ({ id: i }));
} catch {
return [{ id: 0 }];
}
}

// Dynamic study pages - we'll include a sample of studies
// In production, you might want to:
// 1. Query your database for all study IDs
// 2. Generate sitemap index files for large datasets (50K+ URLs)
// 3. Use dynamic sitemap generation or sitemap index
export default async function sitemap({ id }: { id: number }): Promise<MetadataRoute.Sitemap> {
// id === 0: static pages only
if (id === 0) {
return [
{ url: SITE_URL, lastModified: new Date(), changeFrequency: 'weekly', priority: 1.0 },
{ url: `${SITE_URL}/explore`, lastModified: new Date(), changeFrequency: 'daily', priority: 0.9 },
{ url: `${SITE_URL}/dna-chat`, lastModified: new Date(), changeFrequency: 'weekly', priority: 0.8 },
{ url: `${SITE_URL}/overview-report`, lastModified: new Date(), changeFrequency: 'weekly', priority: 0.8 },
{ url: `${SITE_URL}/subscribe`, lastModified: new Date(), changeFrequency: 'weekly', priority: 0.7 },
];
}

// For now, we'll include the first 1000 studies as an example
// This keeps the sitemap under 50K URLs limit
const studyPages: MetadataRoute.Sitemap = [];
// ids 1..n: study pages in BATCH_SIZE chunks
const batchIndex = id - 1;
const minId = batchIndex * BATCH_SIZE + 1;
const maxId = (batchIndex + 1) * BATCH_SIZE;

// Generate URLs for first 1000 studies (you can adjust this)
for (let i = 1; i <= 1000; i++) {
studyPages.push({
url: `${baseUrl}/study/${i}`,
try {
const rows = await executeQuery<{ id: number }>(
'SELECT id FROM gwas_catalog WHERE id BETWEEN $1 AND $2 ORDER BY id',
[minId, maxId]
);
return rows.map(row => ({
url: `${SITE_URL}/study/${row.id}`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.6,
});
}));
} catch {
return [];
}

// Combine all pages
return [...staticPages, ...studyPages];
}
Loading
Loading