Skip to content
Open
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
18 changes: 18 additions & 0 deletions apps/frontend/src/components/settings-search-index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,4 +381,22 @@ export const settingsSearchIndex: SettingsSearchEntry[] = [
keywords: ['files', 'context', 'documents', 'knowledge base'],
adminOnly: true,
},

// ── Updates ──────────────────────────────────────────────
{
page: '/settings/updates',
pageLabel: 'Updates',
section: 'Release Notes',
title: 'GitHub Releases',
description: 'See what is new in nao.',
keywords: ['changelog', 'releases', 'version', 'github', 'what is new'],
},
{
page: '/settings/updates',
pageLabel: 'Updates',
section: 'Newsletter',
title: 'nao Newsletter',
description: 'Get product updates and news from nao.',
keywords: ['newsletter', 'subscribe', 'updates', 'email', 'mailing list'],
},
];
107 changes: 107 additions & 0 deletions apps/frontend/src/components/settings/newsletter-subscription.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { useEffect, useState } from 'react';
import { Loader2, Mail, CheckCircle2 } from 'lucide-react';
import { Button } from '@/components/ui/button';

const WAITLIST_API_URL = 'https://sunshine.getnao.io/api/waitlist/';

const subscribedStorageKey = (email: string | undefined) => `newsletter-subscribed:${email ?? ''}`;

const readSubscribed = (email: string | undefined): boolean => {
try {
return localStorage.getItem(subscribedStorageKey(email)) === 'true';
} catch {
return false;
}
};

const writeSubscribed = (email: string | undefined, value: boolean) => {
try {
localStorage.setItem(subscribedStorageKey(email), String(value));
} catch {
// ignore quota / privacy-mode errors
}
};

export function NewsletterSubscription({ email }: { email?: string }) {
const [subscribed, setSubscribed] = useState(() => readSubscribed(email));
const [isSubmitting, setIsSubmitting] = useState(false);
const [message, setMessage] = useState('');

useEffect(() => {
setSubscribed(readSubscribed(email));
setMessage('');
}, [email]);

const persistSubscribed = (value: boolean) => {
setSubscribed(value);
writeSubscribed(email, value);
};

const handleSubscribe = async () => {
if (!email || isSubmitting) {
return;
}

setIsSubmitting(true);
setMessage('');

try {
const response = await fetch(WAITLIST_API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
});

let data = null;
try {
data = await response.json();
} catch {
// ignore parse errors
}

const serverMsg = String(data?.message || data?.error || data?.detail || '');
const isAlready =
response.status === 409 ||
response.status === 400 ||
/already/i.test(serverMsg) ||
/duplicate/i.test(serverMsg) ||
/exists/i.test(serverMsg);

if (response.ok || data?.success === true) {
persistSubscribed(true);
setMessage("You're subscribed!");
} else if (isAlready) {
persistSubscribed(true);
setMessage("You're already subscribed!");
} else {
setMessage('Something went wrong. Please try again.');
}
} catch {
setMessage('Could not reach the server. Please try again later.');
} finally {
setIsSubmitting(false);
}
};

return (
<div className='flex items-center justify-between'>
<div className='flex flex-col gap-0.5'>
<p className='text-sm font-medium text-foreground h-5'>nao Newsletter</p>
<p className='text-xs text-muted-foreground'>
{message || 'Get product updates and news from nao. No spam.'}
</p>
</div>
{subscribed ? (
<div className='flex items-center gap-1.5 text-sm text-emerald-600 dark:text-emerald-400'>
<CheckCircle2 className='size-4' />
Subscribed
</div>
) : (
<Button variant='outline' size='sm' disabled={isSubmitting || !email} onClick={handleSubscribe}>
{isSubmitting ? <Loader2 className='animate-spin' /> : <Mail />}
{isSubmitting ? 'Subscribing...' : 'Subscribe'}
</Button>
)}
</div>
);
}
11 changes: 11 additions & 0 deletions apps/frontend/src/components/sidebar-community.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Link } from '@tanstack/react-router';
import { Mail } from 'lucide-react';
import { cn } from '@/lib/utils';
import GithubIcon from '@/components/icons/github-icon.svg';
import SlackIcon from '@/components/icons/slack.svg';
Expand Down Expand Up @@ -39,6 +41,15 @@ export function SidebarCommunity({ isCollapsed }: SidebarCommunityProps) {
>
<SlackIcon className='size-3.5 grayscale brightness-0 dark:brightness-200 opacity-30' />
</a>
<Link
to='/settings/updates'
className={cn(
'p-1.5 rounded-md text-muted-foreground/40 hover:text-muted-foreground hover:bg-sidebar-accent transition-colors',
)}
title='Subscribe to updates'
>
<Mail className='size-3.5' />
</Link>
</div>
</div>
);
Expand Down
8 changes: 8 additions & 0 deletions apps/frontend/src/components/sidebar-settings-nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,14 @@ const settingsNavItems: NavItem[] = [
to: '/settings/context-explorer',
visible: ({ isAdmin }) => isAdmin,
},
{
label: 'nao',
type: 'divider',
},
{
label: 'Updates',
to: '/settings/updates',
},
];

interface SidebarSettingsNavProps {
Expand Down
22 changes: 22 additions & 0 deletions apps/frontend/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { Route as SidebarLayoutSettingsIndexRouteImport } from './routes/_sideba
import { Route as SidebarLayoutChatLayoutIndexRouteImport } from './routes/_sidebar-layout._chat-layout.index'
import { Route as SidebarLayoutSharedChatShareIdRouteImport } from './routes/_sidebar-layout.shared-chat.$shareId'
import { Route as SidebarLayoutSettingsUsageRouteImport } from './routes/_sidebar-layout.settings.usage'
import { Route as SidebarLayoutSettingsUpdatesRouteImport } from './routes/_sidebar-layout.settings.updates'
import { Route as SidebarLayoutSettingsProjectRouteImport } from './routes/_sidebar-layout.settings.project'
import { Route as SidebarLayoutSettingsOrganizationRouteImport } from './routes/_sidebar-layout.settings.organization'
import { Route as SidebarLayoutSettingsMemoryRouteImport } from './routes/_sidebar-layout.settings.memory'
Expand Down Expand Up @@ -106,6 +107,12 @@ const SidebarLayoutSettingsUsageRoute =
path: '/usage',
getParentRoute: () => SidebarLayoutSettingsRoute,
} as any)
const SidebarLayoutSettingsUpdatesRoute =
SidebarLayoutSettingsUpdatesRouteImport.update({
id: '/updates',
path: '/updates',
getParentRoute: () => SidebarLayoutSettingsRoute,
} as any)
const SidebarLayoutSettingsProjectRoute =
SidebarLayoutSettingsProjectRouteImport.update({
id: '/project',
Expand Down Expand Up @@ -249,6 +256,7 @@ export interface FileRoutesByFullPath {
'/settings/memory': typeof SidebarLayoutSettingsMemoryRoute
'/settings/organization': typeof SidebarLayoutSettingsOrganizationRoute
'/settings/project': typeof SidebarLayoutSettingsProjectRouteWithChildren
'/settings/updates': typeof SidebarLayoutSettingsUpdatesRoute
'/settings/usage': typeof SidebarLayoutSettingsUsageRoute
'/shared-chat/$shareId': typeof SidebarLayoutSharedChatShareIdRoute
'/settings/': typeof SidebarLayoutSettingsIndexRoute
Expand Down Expand Up @@ -280,6 +288,7 @@ export interface FileRoutesByTo {
'/settings/logs': typeof SidebarLayoutSettingsLogsRoute
'/settings/memory': typeof SidebarLayoutSettingsMemoryRoute
'/settings/organization': typeof SidebarLayoutSettingsOrganizationRoute
'/settings/updates': typeof SidebarLayoutSettingsUpdatesRoute
'/settings/usage': typeof SidebarLayoutSettingsUsageRoute
'/shared-chat/$shareId': typeof SidebarLayoutSharedChatShareIdRoute
'/settings': typeof SidebarLayoutSettingsIndexRoute
Expand Down Expand Up @@ -315,6 +324,7 @@ export interface FileRoutesById {
'/_sidebar-layout/settings/memory': typeof SidebarLayoutSettingsMemoryRoute
'/_sidebar-layout/settings/organization': typeof SidebarLayoutSettingsOrganizationRoute
'/_sidebar-layout/settings/project': typeof SidebarLayoutSettingsProjectRouteWithChildren
'/_sidebar-layout/settings/updates': typeof SidebarLayoutSettingsUpdatesRoute
'/_sidebar-layout/settings/usage': typeof SidebarLayoutSettingsUsageRoute
'/_sidebar-layout/shared-chat/$shareId': typeof SidebarLayoutSharedChatShareIdRoute
'/_sidebar-layout/_chat-layout/': typeof SidebarLayoutChatLayoutIndexRoute
Expand Down Expand Up @@ -351,6 +361,7 @@ export interface FileRouteTypes {
| '/settings/memory'
| '/settings/organization'
| '/settings/project'
| '/settings/updates'
| '/settings/usage'
| '/shared-chat/$shareId'
| '/settings/'
Expand Down Expand Up @@ -382,6 +393,7 @@ export interface FileRouteTypes {
| '/settings/logs'
| '/settings/memory'
| '/settings/organization'
| '/settings/updates'
| '/settings/usage'
| '/shared-chat/$shareId'
| '/settings'
Expand Down Expand Up @@ -416,6 +428,7 @@ export interface FileRouteTypes {
| '/_sidebar-layout/settings/memory'
| '/_sidebar-layout/settings/organization'
| '/_sidebar-layout/settings/project'
| '/_sidebar-layout/settings/updates'
| '/_sidebar-layout/settings/usage'
| '/_sidebar-layout/shared-chat/$shareId'
| '/_sidebar-layout/_chat-layout/'
Expand Down Expand Up @@ -529,6 +542,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof SidebarLayoutSettingsUsageRouteImport
parentRoute: typeof SidebarLayoutSettingsRoute
}
'/_sidebar-layout/settings/updates': {
id: '/_sidebar-layout/settings/updates'
path: '/updates'
fullPath: '/settings/updates'
preLoaderRoute: typeof SidebarLayoutSettingsUpdatesRouteImport
parentRoute: typeof SidebarLayoutSettingsRoute
}
'/_sidebar-layout/settings/project': {
id: '/_sidebar-layout/settings/project'
path: '/project'
Expand Down Expand Up @@ -746,6 +766,7 @@ interface SidebarLayoutSettingsRouteChildren {
SidebarLayoutSettingsMemoryRoute: typeof SidebarLayoutSettingsMemoryRoute
SidebarLayoutSettingsOrganizationRoute: typeof SidebarLayoutSettingsOrganizationRoute
SidebarLayoutSettingsProjectRoute: typeof SidebarLayoutSettingsProjectRouteWithChildren
SidebarLayoutSettingsUpdatesRoute: typeof SidebarLayoutSettingsUpdatesRoute
SidebarLayoutSettingsUsageRoute: typeof SidebarLayoutSettingsUsageRoute
SidebarLayoutSettingsIndexRoute: typeof SidebarLayoutSettingsIndexRoute
}
Expand All @@ -762,6 +783,7 @@ const SidebarLayoutSettingsRouteChildren: SidebarLayoutSettingsRouteChildren = {
SidebarLayoutSettingsOrganizationRoute,
SidebarLayoutSettingsProjectRoute:
SidebarLayoutSettingsProjectRouteWithChildren,
SidebarLayoutSettingsUpdatesRoute: SidebarLayoutSettingsUpdatesRoute,
SidebarLayoutSettingsUsageRoute: SidebarLayoutSettingsUsageRoute,
SidebarLayoutSettingsIndexRoute: SidebarLayoutSettingsIndexRoute,
}
Expand Down
42 changes: 42 additions & 0 deletions apps/frontend/src/routes/_sidebar-layout.settings.updates.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { createFileRoute } from '@tanstack/react-router';
import { ExternalLink } from 'lucide-react';

import { useSession } from '@/lib/auth-client';
import { NewsletterSubscription } from '@/components/settings/newsletter-subscription';
import { Button } from '@/components/ui/button';
import { SettingsCard, SettingsPageWrapper } from '@/components/ui/settings-card';

const RELEASES_URL = 'https://github.com/getnao/nao/releases';

export const Route = createFileRoute('/_sidebar-layout/settings/updates')({
component: UpdatesPage,
});

function UpdatesPage() {
const { data: session } = useSession();

return (
<SettingsPageWrapper>
<SettingsCard title='Release Notes' description='See what is new in nao.'>
<div className='flex items-center justify-between'>
<div className='flex flex-col gap-0.5'>
<p className='text-sm font-medium text-foreground h-5'>GitHub Releases</p>
<p className='text-xs text-muted-foreground'>
Browse the full changelog of nao releases on GitHub.
</p>
</div>
<Button variant='outline' size='sm' asChild>
<a href={RELEASES_URL} target='_blank' rel='noopener noreferrer'>
<ExternalLink />
View releases
</a>
</Button>
</div>
</SettingsCard>

<SettingsCard title='Newsletter' description='Stay in the loop with product news from nao.'>
<NewsletterSubscription email={session?.user?.email} />
</SettingsCard>
</SettingsPageWrapper>
);
}
Loading