diff --git a/apps/web/src/routeTree.gen.ts b/apps/web/src/routeTree.gen.ts index d7e1116d55..6c1e312e24 100644 --- a/apps/web/src/routeTree.gen.ts +++ b/apps/web/src/routeTree.gen.ts @@ -23,7 +23,6 @@ import { Route as ViewIndexRouteImport } from './routes/_view/index' import { Route as WebhookStripeRouteImport } from './routes/webhook/stripe' import { Route as WebhookNangoRouteImport } from './routes/webhook/nango' import { Route as ApiTemplatesRouteImport } from './routes/api/templates' -import { Route as ViewTemplatesRouteImport } from './routes/_view/templates' import { Route as ViewSecurityRouteImport } from './routes/_view/security' import { Route as ViewRoadmapRouteImport } from './routes/_view/roadmap' import { Route as ViewPricingRouteImport } from './routes/_view/pricing' @@ -35,6 +34,7 @@ import { Route as ViewBrandRouteImport } from './routes/_view/brand' import { Route as ViewAboutRouteImport } from './routes/_view/about' import { Route as ViewDocsRouteRouteImport } from './routes/_view/docs/route' import { Route as ViewAppRouteRouteImport } from './routes/_view/app/route' +import { Route as ViewTemplatesIndexRouteImport } from './routes/_view/templates/index' import { Route as ViewLegalIndexRouteImport } from './routes/_view/legal/index' import { Route as ViewDownloadIndexRouteImport } from './routes/_view/download/index' import { Route as ViewDocsIndexRouteImport } from './routes/_view/docs/index' @@ -42,7 +42,9 @@ import { Route as ViewChangelogIndexRouteImport } from './routes/_view/changelog import { Route as ViewBlogIndexRouteImport } from './routes/_view/blog/index' import { Route as ViewAppIndexRouteImport } from './routes/_view/app/index' import { Route as ApiTweetIdRouteImport } from './routes/api/tweet.$id' +import { Route as ApiImagesSplatRouteImport } from './routes/api/images.$' import { Route as ViewVsSlugRouteImport } from './routes/_view/vs/$slug' +import { Route as ViewTemplatesSlugRouteImport } from './routes/_view/templates/$slug' import { Route as ViewSolutionSalesRouteImport } from './routes/_view/solution/sales' import { Route as ViewSolutionRecruitingRouteImport } from './routes/_view/solution/recruiting' import { Route as ViewSolutionProjectManagementRouteImport } from './routes/_view/solution/project-management' @@ -146,11 +148,6 @@ const ApiTemplatesRoute = ApiTemplatesRouteImport.update({ path: '/api/templates', getParentRoute: () => rootRouteImport, } as any) -const ViewTemplatesRoute = ViewTemplatesRouteImport.update({ - id: '/templates', - path: '/templates', - getParentRoute: () => ViewRouteRoute, -} as any) const ViewSecurityRoute = ViewSecurityRouteImport.update({ id: '/security', path: '/security', @@ -206,6 +203,11 @@ const ViewAppRouteRoute = ViewAppRouteRouteImport.update({ path: '/app', getParentRoute: () => ViewRouteRoute, } as any) +const ViewTemplatesIndexRoute = ViewTemplatesIndexRouteImport.update({ + id: '/templates/', + path: '/templates/', + getParentRoute: () => ViewRouteRoute, +} as any) const ViewLegalIndexRoute = ViewLegalIndexRouteImport.update({ id: '/legal/', path: '/legal/', @@ -241,11 +243,21 @@ const ApiTweetIdRoute = ApiTweetIdRouteImport.update({ path: '/api/tweet/$id', getParentRoute: () => rootRouteImport, } as any) +const ApiImagesSplatRoute = ApiImagesSplatRouteImport.update({ + id: '/api/images/$', + path: '/api/images/$', + getParentRoute: () => rootRouteImport, +} as any) const ViewVsSlugRoute = ViewVsSlugRouteImport.update({ id: '/vs/$slug', path: '/vs/$slug', getParentRoute: () => ViewRouteRoute, } as any) +const ViewTemplatesSlugRoute = ViewTemplatesSlugRouteImport.update({ + id: '/templates/$slug', + path: '/templates/$slug', + getParentRoute: () => ViewRouteRoute, +} as any) const ViewSolutionSalesRoute = ViewSolutionSalesRouteImport.update({ id: '/solution/sales', path: '/solution/sales', @@ -438,7 +450,6 @@ export interface FileRoutesByFullPath { '/pricing': typeof ViewPricingRoute '/roadmap': typeof ViewRoadmapRoute '/security': typeof ViewSecurityRoute - '/templates': typeof ViewTemplatesRoute '/api/templates': typeof ApiTemplatesRoute '/webhook/nango': typeof WebhookNangoRoute '/webhook/stripe': typeof WebhookStripeRoute @@ -476,7 +487,9 @@ export interface FileRoutesByFullPath { '/solution/project-management': typeof ViewSolutionProjectManagementRoute '/solution/recruiting': typeof ViewSolutionRecruitingRoute '/solution/sales': typeof ViewSolutionSalesRoute + '/templates/$slug': typeof ViewTemplatesSlugRoute '/vs/$slug': typeof ViewVsSlugRoute + '/api/images/$': typeof ApiImagesSplatRoute '/api/tweet/$id': typeof ApiTweetIdRoute '/app/': typeof ViewAppIndexRoute '/blog': typeof ViewBlogIndexRoute @@ -484,6 +497,7 @@ export interface FileRoutesByFullPath { '/docs/': typeof ViewDocsIndexRoute '/download': typeof ViewDownloadIndexRoute '/legal': typeof ViewLegalIndexRoute + '/templates': typeof ViewTemplatesIndexRoute } export interface FileRoutesByTo { '/auth': typeof AuthRoute @@ -504,7 +518,6 @@ export interface FileRoutesByTo { '/pricing': typeof ViewPricingRoute '/roadmap': typeof ViewRoadmapRoute '/security': typeof ViewSecurityRoute - '/templates': typeof ViewTemplatesRoute '/api/templates': typeof ApiTemplatesRoute '/webhook/nango': typeof WebhookNangoRoute '/webhook/stripe': typeof WebhookStripeRoute @@ -542,7 +555,9 @@ export interface FileRoutesByTo { '/solution/project-management': typeof ViewSolutionProjectManagementRoute '/solution/recruiting': typeof ViewSolutionRecruitingRoute '/solution/sales': typeof ViewSolutionSalesRoute + '/templates/$slug': typeof ViewTemplatesSlugRoute '/vs/$slug': typeof ViewVsSlugRoute + '/api/images/$': typeof ApiImagesSplatRoute '/api/tweet/$id': typeof ApiTweetIdRoute '/app': typeof ViewAppIndexRoute '/blog': typeof ViewBlogIndexRoute @@ -550,6 +565,7 @@ export interface FileRoutesByTo { '/docs': typeof ViewDocsIndexRoute '/download': typeof ViewDownloadIndexRoute '/legal': typeof ViewLegalIndexRoute + '/templates': typeof ViewTemplatesIndexRoute } export interface FileRoutesById { __root__: typeof rootRouteImport @@ -574,7 +590,6 @@ export interface FileRoutesById { '/_view/pricing': typeof ViewPricingRoute '/_view/roadmap': typeof ViewRoadmapRoute '/_view/security': typeof ViewSecurityRoute - '/_view/templates': typeof ViewTemplatesRoute '/api/templates': typeof ApiTemplatesRoute '/webhook/nango': typeof WebhookNangoRoute '/webhook/stripe': typeof WebhookStripeRoute @@ -612,7 +627,9 @@ export interface FileRoutesById { '/_view/solution/project-management': typeof ViewSolutionProjectManagementRoute '/_view/solution/recruiting': typeof ViewSolutionRecruitingRoute '/_view/solution/sales': typeof ViewSolutionSalesRoute + '/_view/templates/$slug': typeof ViewTemplatesSlugRoute '/_view/vs/$slug': typeof ViewVsSlugRoute + '/api/images/$': typeof ApiImagesSplatRoute '/api/tweet/$id': typeof ApiTweetIdRoute '/_view/app/': typeof ViewAppIndexRoute '/_view/blog/': typeof ViewBlogIndexRoute @@ -620,6 +637,7 @@ export interface FileRoutesById { '/_view/docs/': typeof ViewDocsIndexRoute '/_view/download/': typeof ViewDownloadIndexRoute '/_view/legal/': typeof ViewLegalIndexRoute + '/_view/templates/': typeof ViewTemplatesIndexRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath @@ -644,7 +662,6 @@ export interface FileRouteTypes { | '/pricing' | '/roadmap' | '/security' - | '/templates' | '/api/templates' | '/webhook/nango' | '/webhook/stripe' @@ -682,7 +699,9 @@ export interface FileRouteTypes { | '/solution/project-management' | '/solution/recruiting' | '/solution/sales' + | '/templates/$slug' | '/vs/$slug' + | '/api/images/$' | '/api/tweet/$id' | '/app/' | '/blog' @@ -690,6 +709,7 @@ export interface FileRouteTypes { | '/docs/' | '/download' | '/legal' + | '/templates' fileRoutesByTo: FileRoutesByTo to: | '/auth' @@ -710,7 +730,6 @@ export interface FileRouteTypes { | '/pricing' | '/roadmap' | '/security' - | '/templates' | '/api/templates' | '/webhook/nango' | '/webhook/stripe' @@ -748,7 +767,9 @@ export interface FileRouteTypes { | '/solution/project-management' | '/solution/recruiting' | '/solution/sales' + | '/templates/$slug' | '/vs/$slug' + | '/api/images/$' | '/api/tweet/$id' | '/app' | '/blog' @@ -756,6 +777,7 @@ export interface FileRouteTypes { | '/docs' | '/download' | '/legal' + | '/templates' id: | '__root__' | '/_view' @@ -779,7 +801,6 @@ export interface FileRouteTypes { | '/_view/pricing' | '/_view/roadmap' | '/_view/security' - | '/_view/templates' | '/api/templates' | '/webhook/nango' | '/webhook/stripe' @@ -817,7 +838,9 @@ export interface FileRouteTypes { | '/_view/solution/project-management' | '/_view/solution/recruiting' | '/_view/solution/sales' + | '/_view/templates/$slug' | '/_view/vs/$slug' + | '/api/images/$' | '/api/tweet/$id' | '/_view/app/' | '/_view/blog/' @@ -825,6 +848,7 @@ export interface FileRouteTypes { | '/_view/docs/' | '/_view/download/' | '/_view/legal/' + | '/_view/templates/' fileRoutesById: FileRoutesById } export interface RootRouteChildren { @@ -841,6 +865,7 @@ export interface RootRouteChildren { ApiTemplatesRoute: typeof ApiTemplatesRoute WebhookNangoRoute: typeof WebhookNangoRoute WebhookStripeRoute: typeof WebhookStripeRoute + ApiImagesSplatRoute: typeof ApiImagesSplatRoute ApiTweetIdRoute: typeof ApiTweetIdRoute } @@ -944,13 +969,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ApiTemplatesRouteImport parentRoute: typeof rootRouteImport } - '/_view/templates': { - id: '/_view/templates' - path: '/templates' - fullPath: '/templates' - preLoaderRoute: typeof ViewTemplatesRouteImport - parentRoute: typeof ViewRouteRoute - } '/_view/security': { id: '/_view/security' path: '/security' @@ -1028,6 +1046,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ViewAppRouteRouteImport parentRoute: typeof ViewRouteRoute } + '/_view/templates/': { + id: '/_view/templates/' + path: '/templates' + fullPath: '/templates' + preLoaderRoute: typeof ViewTemplatesIndexRouteImport + parentRoute: typeof ViewRouteRoute + } '/_view/legal/': { id: '/_view/legal/' path: '/legal' @@ -1077,6 +1102,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ApiTweetIdRouteImport parentRoute: typeof rootRouteImport } + '/api/images/$': { + id: '/api/images/$' + path: '/api/images/$' + fullPath: '/api/images/$' + preLoaderRoute: typeof ApiImagesSplatRouteImport + parentRoute: typeof rootRouteImport + } '/_view/vs/$slug': { id: '/_view/vs/$slug' path: '/vs/$slug' @@ -1084,6 +1116,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ViewVsSlugRouteImport parentRoute: typeof ViewRouteRoute } + '/_view/templates/$slug': { + id: '/_view/templates/$slug' + path: '/templates/$slug' + fullPath: '/templates/$slug' + preLoaderRoute: typeof ViewTemplatesSlugRouteImport + parentRoute: typeof ViewRouteRoute + } '/_view/solution/sales': { id: '/_view/solution/sales' path: '/solution/sales' @@ -1376,7 +1415,6 @@ interface ViewRouteRouteChildren { ViewPricingRoute: typeof ViewPricingRoute ViewRoadmapRoute: typeof ViewRoadmapRoute ViewSecurityRoute: typeof ViewSecurityRoute - ViewTemplatesRoute: typeof ViewTemplatesRoute ViewIndexRoute: typeof ViewIndexRoute ViewBlogSlugRoute: typeof ViewBlogSlugRoute ViewCallbackAuthRoute: typeof ViewCallbackAuthRoute @@ -1405,11 +1443,13 @@ interface ViewRouteRouteChildren { ViewSolutionProjectManagementRoute: typeof ViewSolutionProjectManagementRoute ViewSolutionRecruitingRoute: typeof ViewSolutionRecruitingRoute ViewSolutionSalesRoute: typeof ViewSolutionSalesRoute + ViewTemplatesSlugRoute: typeof ViewTemplatesSlugRoute ViewVsSlugRoute: typeof ViewVsSlugRoute ViewBlogIndexRoute: typeof ViewBlogIndexRoute ViewChangelogIndexRoute: typeof ViewChangelogIndexRoute ViewDownloadIndexRoute: typeof ViewDownloadIndexRoute ViewLegalIndexRoute: typeof ViewLegalIndexRoute + ViewTemplatesIndexRoute: typeof ViewTemplatesIndexRoute } const ViewRouteRouteChildren: ViewRouteRouteChildren = { @@ -1424,7 +1464,6 @@ const ViewRouteRouteChildren: ViewRouteRouteChildren = { ViewPricingRoute: ViewPricingRoute, ViewRoadmapRoute: ViewRoadmapRoute, ViewSecurityRoute: ViewSecurityRoute, - ViewTemplatesRoute: ViewTemplatesRoute, ViewIndexRoute: ViewIndexRoute, ViewBlogSlugRoute: ViewBlogSlugRoute, ViewCallbackAuthRoute: ViewCallbackAuthRoute, @@ -1453,11 +1492,13 @@ const ViewRouteRouteChildren: ViewRouteRouteChildren = { ViewSolutionProjectManagementRoute: ViewSolutionProjectManagementRoute, ViewSolutionRecruitingRoute: ViewSolutionRecruitingRoute, ViewSolutionSalesRoute: ViewSolutionSalesRoute, + ViewTemplatesSlugRoute: ViewTemplatesSlugRoute, ViewVsSlugRoute: ViewVsSlugRoute, ViewBlogIndexRoute: ViewBlogIndexRoute, ViewChangelogIndexRoute: ViewChangelogIndexRoute, ViewDownloadIndexRoute: ViewDownloadIndexRoute, ViewLegalIndexRoute: ViewLegalIndexRoute, + ViewTemplatesIndexRoute: ViewTemplatesIndexRoute, } const ViewRouteRouteWithChildren = ViewRouteRoute._addFileChildren( @@ -1478,6 +1519,7 @@ const rootRouteChildren: RootRouteChildren = { ApiTemplatesRoute: ApiTemplatesRoute, WebhookNangoRoute: WebhookNangoRoute, WebhookStripeRoute: WebhookStripeRoute, + ApiImagesSplatRoute: ApiImagesSplatRoute, ApiTweetIdRoute: ApiTweetIdRoute, } export const routeTree = rootRouteImport diff --git a/apps/web/src/routes/_view/templates.tsx b/apps/web/src/routes/_view/templates.tsx deleted file mode 100644 index fe56c31ac6..0000000000 --- a/apps/web/src/routes/_view/templates.tsx +++ /dev/null @@ -1,278 +0,0 @@ -import { Icon } from "@iconify-icon/react"; -import { createFileRoute } from "@tanstack/react-router"; -import { allTemplates } from "content-collections"; -import { useMemo, useState } from "react"; - -import { cn } from "@hypr/utils"; - -import { DownloadButton } from "@/components/download-button"; -import { SlashSeparator } from "@/components/slash-separator"; - -export const Route = createFileRoute("/_view/templates")({ - component: Component, - head: () => ({ - meta: [ - { title: "Meeting Templates - Hyprnote" }, - { - name: "description", - content: - "Discover our library of AI meeting templates. Get structured summaries for sprint planning, sales calls, 1:1s, and more. Create custom templates for your workflow.", - }, - { property: "og:title", content: "Meeting Templates - Hyprnote" }, - { - property: "og:description", - content: - "Browse our collection of AI meeting templates. From engineering standups to sales discovery calls, find the perfect template for your meeting type.", - }, - { property: "og:type", content: "website" }, - { property: "og:url", content: "https://hyprnote.com/templates" }, - ], - }), -}); - -function Component() { - const [searchQuery, setSearchQuery] = useState(""); - const [selectedCategory, setSelectedCategory] = useState(null); - - const templatesByCategory = getTemplatesByCategory(); - const categories = Object.keys(templatesByCategory); - - const filteredTemplates = useMemo(() => { - let templates = allTemplates; - - // Filter by category - if (selectedCategory) { - templates = templates.filter((t) => t.category === selectedCategory); - } - - // Filter by search query - if (searchQuery.trim()) { - const query = searchQuery.toLowerCase(); - templates = templates.filter( - (t) => - t.title.toLowerCase().includes(query) || - t.description.toLowerCase().includes(query) || - t.category.toLowerCase().includes(query), - ); - } - - return templates; - }, [searchQuery, selectedCategory]); - - const filteredByCategory = useMemo(() => { - return filteredTemplates.reduce( - (acc, template) => { - const category = template.category; - if (!acc[category]) { - acc[category] = []; - } - acc[category].push(template); - return acc; - }, - {} as Record, - ); - }, [filteredTemplates]); - - return ( -
-
- {/* Hero Section */} -
-
-
-

- Meeting templates for
- every conversation -

-

- Choose from {allTemplates.length} templates to structure your AI - summaries.
- From sprint planning to sales calls, find the perfect format. -

-
- - {/* Search Bar */} -
-
- setSearchQuery(e.target.value)} - className="flex-1 px-4 py-2.5 text-sm outline-none bg-white text-center placeholder:text-center" - /> -
-
- - {/* Category Chips */} -
- - {categories.map((category) => ( - - ))} -
-
-
- - - - {/* Templates List */} -
- {/* Templates List */} -
- {filteredTemplates.length === 0 ? ( -
- -

- No templates found matching your search. -

-
- ) : ( - Object.entries(filteredByCategory).map( - ([category, templates]) => ( -
-

- {category} -

-
- {templates.map((template) => ( - - ))} -
-
- ), - ) - )} -
-
- - - - {/* CTA Section */} -
-
-

- Ready to transform your meetings? -

-

- Download Hyprnote and start using these templates to capture - perfect meeting notes with AI. -

-
- -

- Free to use. No credit card required. -

-
-
-
-
-
- ); -} - -function getTemplatesByCategory() { - return allTemplates.reduce( - (acc, template) => { - const category = template.category; - if (!acc[category]) { - acc[category] = []; - } - acc[category].push(template); - return acc; - }, - {} as Record, - ); -} - -function getIconForTemplate(title: string): string { - const iconMap: Record = { - "Daily Standup": "mdi:run-fast", - "Sprint Planning": "mdi:calendar-star", - "Sprint Retrospective": "mdi:mirror", - "Product Roadmap Review": "mdi:road-variant", - "Customer Discovery Interview": "mdi:account-search", - "Sales Discovery Call": "mdi:phone", - "Technical Design Review": "mdi:draw", - "Executive Briefing": "mdi:tie", - "Board Meeting": "mdi:office-building", - "Performance Review": "mdi:chart-line", - "Client Kickoff Meeting": "mdi:rocket-launch", - "Brainstorming Session": "mdi:lightbulb-on", - "Incident Postmortem": "mdi:alert-circle", - "Lecture Notes": "mdi:school", - "Investor Pitch Meeting": "mdi:cash-multiple", - "1:1 Meeting": "mdi:account-multiple", - "Project Kickoff": "mdi:flag", - }; - return iconMap[title] || "mdi:file-document"; -} - -function TemplateCard({ template }: { template: (typeof allTemplates)[0] }) { - const icon = getIconForTemplate(template.title); - - return ( -
-
-
- -
-
-

- {template.title} -

-

- {template.description} -

-
-
-
-
- Sections -
-
- {template.sections.slice(0, 3).map((section) => ( - - {section.title} - - ))} - {template.sections.length > 3 && ( - - +{template.sections.length - 3} more - - )} -
-
-
- ); -} diff --git a/apps/web/src/routes/_view/templates/$slug.tsx b/apps/web/src/routes/_view/templates/$slug.tsx new file mode 100644 index 0000000000..9acb336298 --- /dev/null +++ b/apps/web/src/routes/_view/templates/$slug.tsx @@ -0,0 +1,277 @@ +import { MDXContent } from "@content-collections/mdx/react"; +import { Icon } from "@iconify-icon/react"; +import { createFileRoute, Link, notFound } from "@tanstack/react-router"; +import { allTemplates } from "content-collections"; + +import { cn } from "@hypr/utils"; + +import { DownloadButton } from "@/components/download-button"; + +export const Route = createFileRoute("/_view/templates/$slug")({ + component: Component, + loader: async ({ params }) => { + const template = allTemplates.find( + (template) => template.slug === params.slug, + ); + if (!template) { + throw notFound(); + } + return { template }; + }, + head: ({ loaderData }) => { + const { template } = loaderData!; + const url = `https://hyprnote.com/templates/${template.slug}`; + + return { + meta: [ + { title: `${template.title} Template - Hyprnote` }, + { name: "description", content: template.description }, + { property: "og:title", content: `${template.title} Template` }, + { property: "og:description", content: template.description }, + { property: "og:type", content: "website" }, + { property: "og:url", content: url }, + { name: "twitter:card", content: "summary" }, + { name: "twitter:title", content: `${template.title} Template` }, + { name: "twitter:description", content: template.description }, + ], + }; + }, +}); + +function Component() { + const { template } = Route.useLoaderData(); + + return ( +
+
+
+ + + +
+
+
+ ); +} + +function LeftSidebar({ template }: { template: (typeof allTemplates)[0] }) { + return ( + + ); +} + +function MainContent({ template }: { template: (typeof allTemplates)[0] }) { + return ( +
+
+ + + Back to templates + +
+ + + + + + +
+ ); +} + +function TemplateHeader({ template }: { template: (typeof allTemplates)[0] }) { + return ( +
+
+ {template.category} +
+

+ {template.title} +

+

+ {template.description} +

+ +
+ {template.targets.map((target) => ( + + {target} + + ))} +
+
+ ); +} + +function TemplateContent({ template }: { template: (typeof allTemplates)[0] }) { + return ( +
+

Structure

+
+
+ +
+
+
+ ); +} + +function ExamplesSection() { + return ( +
+

Examples

+
+

+ Examples coming soon +

+
+
+ ); +} + +function SuggestedTemplates({ + template, +}: { + template: (typeof allTemplates)[0]; +}) { + const suggestedTemplates = allTemplates.filter( + (t) => t.category === template.category && t.slug !== template.slug, + ); + + if (suggestedTemplates.length === 0) return null; + + return ( +
+

+ Other {template.category} templates +

+
+ {suggestedTemplates.map((t) => ( + +

+ {t.title} +

+

+ {t.description} +

+ + ))} +
+
+ ); +} + +function TemplateFooter() { + return ( +
+ + + View all templates + +
+ ); +} + +function RightSidebar() { + return ( + + ); +} diff --git a/apps/web/src/routes/_view/templates/index.tsx b/apps/web/src/routes/_view/templates/index.tsx new file mode 100644 index 0000000000..2d1caf3542 --- /dev/null +++ b/apps/web/src/routes/_view/templates/index.tsx @@ -0,0 +1,603 @@ +import { MDXContent } from "@content-collections/mdx/react"; +import { Icon } from "@iconify-icon/react"; +import { createFileRoute, useNavigate } from "@tanstack/react-router"; +import { allTemplates } from "content-collections"; +import { useCallback, useEffect, useMemo, useState } from "react"; + +import { cn } from "@hypr/utils"; + +import { DownloadButton } from "@/components/download-button"; +import { SlashSeparator } from "@/components/slash-separator"; + +type TemplatesSearch = { + category?: string; +}; + +export const Route = createFileRoute("/_view/templates/")({ + component: Component, + validateSearch: (search: Record): TemplatesSearch => { + return { + category: + typeof search.category === "string" ? search.category : undefined, + }; + }, + head: () => ({ + meta: [ + { title: "Meeting Templates - Hyprnote" }, + { + name: "description", + content: + "Discover our library of AI meeting templates. Get structured summaries for sprint planning, sales calls, 1:1s, and more. Create custom templates for your workflow.", + }, + { property: "og:title", content: "Meeting Templates - Hyprnote" }, + { + property: "og:description", + content: + "Browse our collection of AI meeting templates. From engineering standups to sales discovery calls, find the perfect template for your meeting type.", + }, + { property: "og:type", content: "website" }, + { property: "og:url", content: "https://hyprnote.com/templates" }, + ], + }), +}); + +function Component() { + const navigate = useNavigate({ from: Route.fullPath }); + const search = Route.useSearch(); + const [searchQuery, setSearchQuery] = useState(""); + const [selectedTemplate, setSelectedTemplate] = useState< + (typeof allTemplates)[0] | null + >(null); + + const selectedCategory = search.category || null; + + const setSelectedCategory = (category: string | null) => { + navigate({ search: category ? { category } : {}, resetScroll: false }); + }; + + const handleTemplateClick = (template: (typeof allTemplates)[0]) => { + setSelectedTemplate(template); + window.history.pushState({}, "", `/templates/${template.slug}`); + }; + + const handleModalClose = useCallback(() => { + setSelectedTemplate(null); + const url = selectedCategory + ? `/templates?category=${encodeURIComponent(selectedCategory)}` + : "/templates"; + window.history.pushState({}, "", url); + }, [selectedCategory]); + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape" && selectedTemplate) { + handleModalClose(); + } + }; + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [selectedTemplate, handleModalClose]); + + useEffect(() => { + if (selectedTemplate) { + document.body.style.overflow = "hidden"; + } else { + document.body.style.overflow = ""; + } + return () => { + document.body.style.overflow = ""; + }; + }, [selectedTemplate]); + + const templatesByCategory = getTemplatesByCategory(); + const categories = Object.keys(templatesByCategory); + + const filteredTemplates = useMemo(() => { + let templates = allTemplates; + + if (selectedCategory) { + templates = templates.filter((t) => t.category === selectedCategory); + } + + if (searchQuery.trim()) { + const query = searchQuery.toLowerCase(); + templates = templates.filter( + (t) => + t.title.toLowerCase().includes(query) || + t.description.toLowerCase().includes(query) || + t.category.toLowerCase().includes(query), + ); + } + + return templates; + }, [searchQuery, selectedCategory]); + + return ( +
+
+ + + + + + + +
+ + {selectedTemplate && ( + + )} +
+ ); +} + +function ContributeBanner() { + return ( + + + + Community-driven: Have a template idea?{" "} + + Contribute on GitHub + + + + ); +} + +function HeroSection({ + searchQuery, + setSearchQuery, +}: { + searchQuery: string; + setSearchQuery: (query: string) => void; +}) { + return ( +
+
+
+

+ Templates +

+

+ Different conversations need different approaches. Templates are AI + instructions that capture best practices for each meeting type — + plug them in and get structured notes instantly. +

+
+ +
+
+ setSearchQuery(e.target.value)} + className="flex-1 px-4 py-2.5 text-sm outline-none bg-white text-center placeholder:text-center" + /> +
+
+
+
+ ); +} + +function QuoteSection() { + return ( +
+

+ "Curated by Hyprnote and the community" +

+
+ ); +} + +function MobileCategoriesSection({ + categories, + selectedCategory, + setSelectedCategory, +}: { + categories: string[]; + selectedCategory: string | null; + setSelectedCategory: (category: string | null) => void; +}) { + return ( +
+
+ + {categories.map((category) => ( + + ))} +
+
+ ); +} + +function TemplatesSection({ + categories, + selectedCategory, + setSelectedCategory, + templatesByCategory, + filteredTemplates, + onTemplateClick, +}: { + categories: string[]; + selectedCategory: string | null; + setSelectedCategory: (category: string | null) => void; + templatesByCategory: Record; + filteredTemplates: typeof allTemplates; + onTemplateClick: (template: (typeof allTemplates)[0]) => void; +}) { + return ( +
+
+ + +
+
+ ); +} + +function DesktopSidebar({ + categories, + selectedCategory, + setSelectedCategory, + templatesByCategory, +}: { + categories: string[]; + selectedCategory: string | null; + setSelectedCategory: (category: string | null) => void; + templatesByCategory: Record; +}) { + return ( + + ); +} + +function TemplatesGrid({ + filteredTemplates, + onTemplateClick, +}: { + filteredTemplates: typeof allTemplates; + onTemplateClick: (template: (typeof allTemplates)[0]) => void; +}) { + if (filteredTemplates.length === 0) { + return ( +
+
+ +

+ No templates found matching your search. +

+
+
+ ); + } + + return ( +
+
+ {filteredTemplates.map((template) => ( + onTemplateClick(template)} + /> + ))} + +
+
+ ); +} + +function TemplateCard({ + template, + onClick, +}: { + template: (typeof allTemplates)[0]; + onClick: () => void; +}) { + return ( + + ); +} + +function ContributeCard() { + return ( +
+

+ Contribute a template +

+

+ Have a template idea? Submit a PR and help the community. +

+ + + Open on GitHub + +
+ ); +} + +function CTASection() { + return ( +
+
+

+ Ready to transform your meetings? +

+

+ Download Hyprnote and start using these templates to capture perfect + meeting notes with AI. +

+
+ +

+ Free to use. No credit card required. +

+
+
+
+ ); +} + +function TemplateModal({ + template, + onClose, +}: { + template: (typeof allTemplates)[0]; + onClose: () => void; +}) { + return ( +
+
+
+
e.stopPropagation()} + > +
+
+ + +
+
+ + {template.category} + +
+

+ {template.title} +

+

{template.description}

+ +
+ {template.targets.map((target) => ( + + {target} + + ))} +
+ +
+
+

+ Template Sections +

+
+ {template.sections.map((section, index) => ( +
+
+ + {index + 1} + +

+ {section.title} +

+
+

+ {section.description} +

+
+ ))} +
+
+ +
+ +
+
+ +
+
+ +

+ Download Hyprnote to use this template +

+
+
+
+
+
+
+
+ ); +} + +function getTemplatesByCategory() { + return allTemplates.reduce( + (acc, template) => { + const category = template.category; + if (!acc[category]) { + acc[category] = []; + } + acc[category].push(template); + return acc; + }, + {} as Record, + ); +}