From 665dd96c92924d48f8c1167b0cf630a75c07e402 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 29 Nov 2025 01:59:26 +0000 Subject: [PATCH 01/12] feat(web): add unified /gallery route and /shortcuts SEO route - Add /gallery route with type filter (template/shortcut) and category filter - Add /gallery/$type/$slug detail route for both templates and shortcuts - Add /shortcuts route for SEO with dedicated index and detail pages - Update header and footer navigation to use /gallery instead of /templates - Keep existing /templates route for SEO backward compatibility Co-Authored-By: john@hyprnote.com --- apps/web/src/components/footer.tsx | 4 +- apps/web/src/components/header.tsx | 2 +- .../src/routes/_view/gallery/$type.$slug.tsx | 385 +++++++++ apps/web/src/routes/_view/gallery/index.tsx | 756 ++++++++++++++++++ apps/web/src/routes/_view/shortcuts/$slug.tsx | 259 ++++++ apps/web/src/routes/_view/shortcuts/index.tsx | 555 +++++++++++++ 6 files changed, 1958 insertions(+), 3 deletions(-) create mode 100644 apps/web/src/routes/_view/gallery/$type.$slug.tsx create mode 100644 apps/web/src/routes/_view/gallery/index.tsx create mode 100644 apps/web/src/routes/_view/shortcuts/$slug.tsx create mode 100644 apps/web/src/routes/_view/shortcuts/index.tsx diff --git a/apps/web/src/components/footer.tsx b/apps/web/src/components/footer.tsx index 481f05b66b..b8fdec0c8c 100644 --- a/apps/web/src/components/footer.tsx +++ b/apps/web/src/components/footer.tsx @@ -156,10 +156,10 @@ export function Footer() {
  • - Templates + Gallery
  • diff --git a/apps/web/src/components/header.tsx b/apps/web/src/components/header.tsx index 48b0443f60..574baa617b 100644 --- a/apps/web/src/components/header.tsx +++ b/apps/web/src/components/header.tsx @@ -36,7 +36,7 @@ const featuresList = [ { to: "/product/ai-assistant", label: "AI Assistant" }, { to: "/product/mini-apps", label: "Mini Apps" }, { to: "/product/workflows", label: "Workflows", badge: "Coming Soon" }, - { to: "/templates", label: "Templates" }, + { to: "/gallery", label: "Gallery" }, ]; export function Header() { diff --git a/apps/web/src/routes/_view/gallery/$type.$slug.tsx b/apps/web/src/routes/_view/gallery/$type.$slug.tsx new file mode 100644 index 0000000000..ace008a10c --- /dev/null +++ b/apps/web/src/routes/_view/gallery/$type.$slug.tsx @@ -0,0 +1,385 @@ +import { MDXContent } from "@content-collections/mdx/react"; +import { Icon } from "@iconify-icon/react"; +import { createFileRoute, Link, notFound } from "@tanstack/react-router"; +import { allShortcuts, allTemplates } from "content-collections"; + +import { cn } from "@hypr/utils"; + +import { DownloadButton } from "@/components/download-button"; + +type GalleryType = "template" | "shortcut"; + +export const Route = createFileRoute("/_view/gallery/$type/$slug")({ + component: Component, + loader: async ({ params }) => { + const { type, slug } = params; + + if (type !== "template" && type !== "shortcut") { + throw notFound(); + } + + if (type === "template") { + const template = allTemplates.find((t) => t.slug === slug); + if (!template) { + throw notFound(); + } + return { type: "template" as const, item: template }; + } else { + const shortcut = allShortcuts.find((s) => s.slug === slug); + if (!shortcut) { + throw notFound(); + } + return { type: "shortcut" as const, item: shortcut }; + } + }, + head: ({ loaderData }) => { + if (!loaderData) return { meta: [] }; + + const { type, item } = loaderData; + const typeLabel = type === "template" ? "Template" : "Shortcut"; + const url = `https://hyprnote.com/gallery/${type}/${item.slug}`; + + const ogImageUrl = `https://hyprnote.com/og?type=gallery&title=${encodeURIComponent(item.title)}&category=${encodeURIComponent(item.category)}${item.description ? `&description=${encodeURIComponent(item.description)}` : ""}`; + + return { + meta: [ + { title: `${item.title} - ${typeLabel} - Hyprnote` }, + { name: "description", content: item.description }, + { + property: "og:title", + content: `${item.title} - ${typeLabel}`, + }, + { property: "og:description", content: item.description }, + { property: "og:type", content: "article" }, + { property: "og:url", content: url }, + { property: "og:image", content: ogImageUrl }, + { name: "twitter:card", content: "summary_large_image" }, + { + name: "twitter:title", + content: `${item.title} - ${typeLabel}`, + }, + { name: "twitter:description", content: item.description }, + { name: "twitter:image", content: ogImageUrl }, + ], + }; + }, +}); + +function Component() { + const data = Route.useLoaderData(); + const { type, item } = data; + const isTemplate = type === "template"; + + return ( +
    +
    +
    + + + +
    +
    +
    + ); +} + +function LeftSidebar({ + type, + item, +}: { + type: GalleryType; + item: (typeof allTemplates)[0] | (typeof allShortcuts)[0]; +}) { + return ( + + ); +} + +function MainContent({ + type, + item, +}: { + type: GalleryType; + item: (typeof allTemplates)[0] | (typeof allShortcuts)[0]; +}) { + const isTemplate = type === "template"; + + return ( +
    +
    + + + Back to gallery + +
    + + + + + +
    + ); +} + +function ItemHeader({ + type, + item, +}: { + type: GalleryType; + item: (typeof allTemplates)[0] | (typeof allShortcuts)[0]; +}) { + const isTemplate = type === "template"; + + return ( +
    +
    + + {isTemplate ? "Template" : "Shortcut"} + + {item.category} +
    +

    + {item.title} +

    +

    + {item.description} +

    + + {isTemplate && "targets" in item && ( +
    + {item.targets.map((target) => ( + + {target} + + ))} +
    + )} +
    + ); +} + +function ItemContent({ + type, + item, +}: { + type: GalleryType; + item: (typeof allTemplates)[0] | (typeof allShortcuts)[0]; +}) { + const isTemplate = type === "template"; + + return ( +
    +

    + {isTemplate ? "Structure" : "Details"} +

    +
    + {isTemplate && "sections" in item && ( +
    +

    + Template Sections +

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

    + {section.title} +

    +
    +

    + {section.description} +

    +
    + ))} +
    +
    + )} + +
    + +
    +
    +
    + ); +} + +function SuggestedItems({ + type, + item, +}: { + type: GalleryType; + item: (typeof allTemplates)[0] | (typeof allShortcuts)[0]; +}) { + const suggestedItems = + type === "template" + ? allTemplates.filter( + (t) => t.category === item.category && t.slug !== item.slug, + ) + : allShortcuts.filter( + (s) => s.category === item.category && s.slug !== item.slug, + ); + + if (suggestedItems.length === 0) return null; + + return ( +
    +

    + Other {item.category} {type === "template" ? "templates" : "shortcuts"} +

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

    + {t.title} +

    +

    + {t.description} +

    + + ))} +
    +
    + ); +} + +function ItemFooter({ type }: { type: GalleryType }) { + return ( + + ); +} + +function RightSidebar({ type }: { type: GalleryType }) { + const isTemplate = type === "template"; + const contentDir = isTemplate ? "templates" : "shortcuts"; + + return ( + + ); +} diff --git a/apps/web/src/routes/_view/gallery/index.tsx b/apps/web/src/routes/_view/gallery/index.tsx new file mode 100644 index 0000000000..8345161339 --- /dev/null +++ b/apps/web/src/routes/_view/gallery/index.tsx @@ -0,0 +1,756 @@ +import { MDXContent } from "@content-collections/mdx/react"; +import { Icon } from "@iconify-icon/react"; +import { createFileRoute, useNavigate } from "@tanstack/react-router"; +import { allShortcuts, 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 GalleryType = "template" | "shortcut"; + +type GallerySearch = { + type?: GalleryType; + category?: string; +}; + +export const Route = createFileRoute("/_view/gallery/")({ + component: Component, + validateSearch: (search: Record): GallerySearch => { + return { + type: + search.type === "template" || search.type === "shortcut" + ? search.type + : undefined, + category: + typeof search.category === "string" ? search.category : undefined, + }; + }, + head: ({ search }) => { + const typeLabel = + search.type === "shortcut" + ? "Shortcuts" + : search.type === "template" + ? "Templates" + : "Templates & Shortcuts"; + return { + meta: [ + { title: `${typeLabel} Gallery - Hyprnote` }, + { + name: "description", + content: + "Discover our library of AI meeting templates and shortcuts. Get structured summaries, extract action items, and more with Hyprnote's AI-powered tools.", + }, + { property: "og:title", content: `${typeLabel} Gallery - Hyprnote` }, + { + property: "og:description", + content: + "Browse our collection of AI meeting templates and shortcuts. From engineering standups to sales discovery calls, find the perfect tool for your workflow.", + }, + { property: "og:type", content: "website" }, + { property: "og:url", content: "https://hyprnote.com/gallery" }, + ], + }; + }, +}); + +type GalleryItem = + | { type: "template"; item: (typeof allTemplates)[0] } + | { type: "shortcut"; item: (typeof allShortcuts)[0] }; + +function Component() { + const navigate = useNavigate({ from: Route.fullPath }); + const search = Route.useSearch(); + const [searchQuery, setSearchQuery] = useState(""); + const [selectedItem, setSelectedItem] = useState(null); + + const selectedType = search.type || null; + const selectedCategory = search.category || null; + + const setSelectedType = (type: GalleryType | null) => { + navigate({ + search: { + type: type || undefined, + category: selectedCategory || undefined, + }, + resetScroll: false, + }); + }; + + const setSelectedCategory = (category: string | null) => { + navigate({ + search: { + type: selectedType || undefined, + category: category || undefined, + }, + resetScroll: false, + }); + }; + + const handleItemClick = (item: GalleryItem) => { + setSelectedItem(item); + window.history.pushState( + {}, + "", + `/gallery/${item.type}/${item.type === "template" ? item.item.slug : item.item.slug}`, + ); + }; + + const handleModalClose = useCallback(() => { + setSelectedItem(null); + const params = new URLSearchParams(); + if (selectedType) params.set("type", selectedType); + if (selectedCategory) params.set("category", selectedCategory); + const queryString = params.toString(); + window.history.pushState( + {}, + "", + `/gallery${queryString ? `?${queryString}` : ""}`, + ); + }, [selectedType, selectedCategory]); + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape" && selectedItem) { + handleModalClose(); + } + }; + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [selectedItem, handleModalClose]); + + useEffect(() => { + if (selectedItem) { + document.body.style.overflow = "hidden"; + } else { + document.body.style.overflow = ""; + } + return () => { + document.body.style.overflow = ""; + }; + }, [selectedItem]); + + const allItems: GalleryItem[] = useMemo(() => { + const templates: GalleryItem[] = allTemplates.map((t) => ({ + type: "template" as const, + item: t, + })); + const shortcuts: GalleryItem[] = allShortcuts.map((s) => ({ + type: "shortcut" as const, + item: s, + })); + return [...templates, ...shortcuts]; + }, []); + + const itemsByCategory = useMemo(() => { + return allItems.reduce( + (acc, item) => { + const category = item.item.category; + if (!acc[category]) { + acc[category] = []; + } + acc[category].push(item); + return acc; + }, + {} as Record, + ); + }, [allItems]); + + const categories = Object.keys(itemsByCategory).sort(); + + const filteredItems = useMemo(() => { + let items = allItems; + + if (selectedType) { + items = items.filter((i) => i.type === selectedType); + } + + if (selectedCategory) { + items = items.filter((i) => i.item.category === selectedCategory); + } + + if (searchQuery.trim()) { + const query = searchQuery.toLowerCase(); + items = items.filter( + (i) => + i.item.title.toLowerCase().includes(query) || + i.item.description.toLowerCase().includes(query) || + i.item.category.toLowerCase().includes(query), + ); + } + + return items; + }, [allItems, searchQuery, selectedType, selectedCategory]); + + const filteredCategories = useMemo(() => { + if (!selectedType) return categories; + const items = allItems.filter((i) => i.type === selectedType); + const cats = new Set(items.map((i) => i.item.category)); + return Array.from(cats).sort(); + }, [allItems, selectedType, categories]); + + const filteredItemsByCategory = useMemo(() => { + return filteredItems.reduce( + (acc, item) => { + const category = item.item.category; + if (!acc[category]) { + acc[category] = []; + } + acc[category].push(item); + return acc; + }, + {} as Record, + ); + }, [filteredItems]); + + return ( +
    +
    + + + + + + + +
    + + {selectedItem && ( + + )} +
    + ); +} + +function ContributeBanner() { + return ( + + + + Community-driven: Have an idea?{" "} + + Contribute on GitHub + + + + ); +} + +function HeroSection({ + searchQuery, + setSearchQuery, + selectedType, + setSelectedType, +}: { + searchQuery: string; + setSearchQuery: (query: string) => void; + selectedType: GalleryType | null; + setSelectedType: (type: GalleryType | null) => void; +}) { + return ( +
    +
    +
    +

    + Gallery +

    +

    + Templates are AI instructions for summarizing meetings. Shortcuts + are quick commands for the AI chat assistant. Browse and discover + tools for your workflow. +

    +
    + +
    + + + +
    + +
    +
    + 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 GallerySection({ + categories, + selectedCategory, + setSelectedCategory, + itemsByCategory, + filteredItems, + onItemClick, +}: { + categories: string[]; + selectedCategory: string | null; + setSelectedCategory: (category: string | null) => void; + itemsByCategory: Record; + filteredItems: GalleryItem[]; + onItemClick: (item: GalleryItem) => void; +}) { + return ( +
    +
    + + +
    +
    + ); +} + +function DesktopSidebar({ + categories, + selectedCategory, + setSelectedCategory, + itemsByCategory, + totalCount, +}: { + categories: string[]; + selectedCategory: string | null; + setSelectedCategory: (category: string | null) => void; + itemsByCategory: Record; + totalCount: number; +}) { + return ( + + ); +} + +function GalleryGrid({ + filteredItems, + onItemClick, +}: { + filteredItems: GalleryItem[]; + onItemClick: (item: GalleryItem) => void; +}) { + if (filteredItems.length === 0) { + return ( +
    +
    + +

    + No items found matching your search. +

    +
    +
    + ); + } + + return ( +
    +
    + {filteredItems.map((item) => ( + onItemClick(item)} + /> + ))} + +
    +
    + ); +} + +function ItemCard({ + item, + onClick, +}: { + item: GalleryItem; + onClick: () => void; +}) { + const isTemplate = item.type === "template"; + + return ( + + ); +} + +function ContributeCard() { + return ( +
    +

    Contribute

    +

    + Have an 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 and shortcuts to + capture perfect meeting notes with AI. +

    +
    + +

    + Free to use. No credit card required. +

    +
    +
    +
    + ); +} + +function ItemModal({ + item, + onClose, +}: { + item: GalleryItem; + onClose: () => void; +}) { + const isTemplate = item.type === "template"; + + return ( +
    +
    +
    +
    e.stopPropagation()} + > +
    +
    + + +
    +
    + + {isTemplate ? "Template" : "Shortcut"} + + + {item.item.category} + +
    +

    + {item.item.title} +

    +

    {item.item.description}

    + + {isTemplate && "targets" in item.item && ( +
    + {item.item.targets.map((target) => ( + + {target} + + ))} +
    + )} + +
    + {isTemplate && "sections" in item.item && ( +
    +

    + Template Sections +

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

    + {section.title} +

    +
    +

    + {section.description} +

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

    + Download Hyprnote to use this{" "} + {isTemplate ? "template" : "shortcut"} +

    +
    +
    +
    +
    +
    +
    +
    + ); +} diff --git a/apps/web/src/routes/_view/shortcuts/$slug.tsx b/apps/web/src/routes/_view/shortcuts/$slug.tsx new file mode 100644 index 0000000000..baf79265f3 --- /dev/null +++ b/apps/web/src/routes/_view/shortcuts/$slug.tsx @@ -0,0 +1,259 @@ +import { MDXContent } from "@content-collections/mdx/react"; +import { Icon } from "@iconify-icon/react"; +import { createFileRoute, Link, notFound } from "@tanstack/react-router"; +import { allShortcuts } from "content-collections"; + +import { cn } from "@hypr/utils"; + +import { DownloadButton } from "@/components/download-button"; + +export const Route = createFileRoute("/_view/shortcuts/$slug")({ + component: Component, + loader: async ({ params }) => { + const shortcut = allShortcuts.find( + (shortcut) => shortcut.slug === params.slug, + ); + if (!shortcut) { + throw notFound(); + } + return { shortcut }; + }, + head: ({ loaderData }) => { + const { shortcut } = loaderData!; + const url = `https://hyprnote.com/shortcuts/${shortcut.slug}`; + + const ogImageUrl = `https://hyprnote.com/og?type=shortcuts&title=${encodeURIComponent(shortcut.title)}&category=${encodeURIComponent(shortcut.category)}${shortcut.description ? `&description=${encodeURIComponent(shortcut.description)}` : ""}`; + + return { + meta: [ + { title: `${shortcut.title} - AI Shortcut - Hyprnote` }, + { name: "description", content: shortcut.description }, + { + property: "og:title", + content: `${shortcut.title} - AI Shortcut`, + }, + { property: "og:description", content: shortcut.description }, + { property: "og:type", content: "article" }, + { property: "og:url", content: url }, + { property: "og:image", content: ogImageUrl }, + { name: "twitter:card", content: "summary_large_image" }, + { + name: "twitter:title", + content: `${shortcut.title} - AI Shortcut`, + }, + { name: "twitter:description", content: shortcut.description }, + { name: "twitter:image", content: ogImageUrl }, + ], + }; + }, +}); + +function Component() { + const { shortcut } = Route.useLoaderData(); + + return ( +
    +
    +
    + + + +
    +
    +
    + ); +} + +function LeftSidebar({ shortcut }: { shortcut: (typeof allShortcuts)[0] }) { + return ( + + ); +} + +function MainContent({ shortcut }: { shortcut: (typeof allShortcuts)[0] }) { + return ( +
    +
    + + + Back to shortcuts + +
    + + + + + +
    + ); +} + +function ShortcutHeader({ shortcut }: { shortcut: (typeof allShortcuts)[0] }) { + return ( +
    +
    + + Shortcut + + {shortcut.category} +
    +

    + {shortcut.title} +

    +

    + {shortcut.description} +

    +
    + ); +} + +function ShortcutContent({ shortcut }: { shortcut: (typeof allShortcuts)[0] }) { + return ( +
    +

    Details

    +
    +
    + +
    +
    +
    + ); +} + +function SuggestedShortcuts({ + shortcut, +}: { + shortcut: (typeof allShortcuts)[0]; +}) { + const suggestedShortcuts = allShortcuts.filter( + (s) => s.category === shortcut.category && s.slug !== shortcut.slug, + ); + + if (suggestedShortcuts.length === 0) return null; + + return ( +
    +

    + Other {shortcut.category} shortcuts +

    +
    + {suggestedShortcuts.map((s) => ( + +

    + {s.title} +

    +

    + {s.description} +

    + + ))} +
    +
    + ); +} + +function ShortcutFooter() { + return ( +
    + + + View all shortcuts + +
    + ); +} + +function RightSidebar() { + return ( + + ); +} diff --git a/apps/web/src/routes/_view/shortcuts/index.tsx b/apps/web/src/routes/_view/shortcuts/index.tsx new file mode 100644 index 0000000000..4d473914bc --- /dev/null +++ b/apps/web/src/routes/_view/shortcuts/index.tsx @@ -0,0 +1,555 @@ +import { MDXContent } from "@content-collections/mdx/react"; +import { Icon } from "@iconify-icon/react"; +import { createFileRoute, useNavigate } from "@tanstack/react-router"; +import { allShortcuts } 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 ShortcutsSearch = { + category?: string; +}; + +export const Route = createFileRoute("/_view/shortcuts/")({ + component: Component, + validateSearch: (search: Record): ShortcutsSearch => { + return { + category: + typeof search.category === "string" ? search.category : undefined, + }; + }, + head: () => ({ + meta: [ + { title: "AI Shortcuts - Hyprnote" }, + { + name: "description", + content: + "Discover our library of AI shortcuts for meeting conversations. Extract action items, draft follow-up emails, get meeting insights, and more with quick chat commands.", + }, + { property: "og:title", content: "AI Shortcuts - Hyprnote" }, + { + property: "og:description", + content: + "Browse our collection of AI shortcuts. Quick commands for extracting insights, drafting emails, and analyzing your meeting conversations.", + }, + { property: "og:type", content: "website" }, + { property: "og:url", content: "https://hyprnote.com/shortcuts" }, + ], + }), +}); + +function Component() { + const navigate = useNavigate({ from: Route.fullPath }); + const search = Route.useSearch(); + const [searchQuery, setSearchQuery] = useState(""); + const [selectedShortcut, setSelectedShortcut] = useState< + (typeof allShortcuts)[0] | null + >(null); + + const selectedCategory = search.category || null; + + const setSelectedCategory = (category: string | null) => { + navigate({ search: category ? { category } : {}, resetScroll: false }); + }; + + const handleShortcutClick = (shortcut: (typeof allShortcuts)[0]) => { + setSelectedShortcut(shortcut); + window.history.pushState({}, "", `/shortcuts/${shortcut.slug}`); + }; + + const handleModalClose = useCallback(() => { + setSelectedShortcut(null); + const url = selectedCategory + ? `/shortcuts?category=${encodeURIComponent(selectedCategory)}` + : "/shortcuts"; + window.history.pushState({}, "", url); + }, [selectedCategory]); + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape" && selectedShortcut) { + handleModalClose(); + } + }; + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [selectedShortcut, handleModalClose]); + + useEffect(() => { + if (selectedShortcut) { + document.body.style.overflow = "hidden"; + } else { + document.body.style.overflow = ""; + } + return () => { + document.body.style.overflow = ""; + }; + }, [selectedShortcut]); + + const shortcutsByCategory = getShortcutsByCategory(); + const categories = Object.keys(shortcutsByCategory); + + const filteredShortcuts = useMemo(() => { + let shortcuts = allShortcuts; + + if (selectedCategory) { + shortcuts = shortcuts.filter((s) => s.category === selectedCategory); + } + + if (searchQuery.trim()) { + const query = searchQuery.toLowerCase(); + shortcuts = shortcuts.filter( + (s) => + s.title.toLowerCase().includes(query) || + s.description.toLowerCase().includes(query) || + s.category.toLowerCase().includes(query), + ); + } + + return shortcuts; + }, [searchQuery, selectedCategory]); + + return ( +
    +
    + + + + + + + +
    + + {selectedShortcut && ( + + )} +
    + ); +} + +function ContributeBanner() { + return ( + + + + Community-driven: Have a shortcut idea?{" "} + + Contribute on GitHub + + + + ); +} + +function HeroSection({ + searchQuery, + setSearchQuery, +}: { + searchQuery: string; + setSearchQuery: (query: string) => void; +}) { + return ( +
    +
    +
    +

    + Shortcuts +

    +

    + Quick AI commands for your meeting conversations. Use shortcuts in + the chat assistant to extract insights, draft emails, and analyze + discussions 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 ShortcutsSection({ + categories, + selectedCategory, + setSelectedCategory, + shortcutsByCategory, + filteredShortcuts, + onShortcutClick, +}: { + categories: string[]; + selectedCategory: string | null; + setSelectedCategory: (category: string | null) => void; + shortcutsByCategory: Record; + filteredShortcuts: typeof allShortcuts; + onShortcutClick: (shortcut: (typeof allShortcuts)[0]) => void; +}) { + return ( +
    +
    + + +
    +
    + ); +} + +function DesktopSidebar({ + categories, + selectedCategory, + setSelectedCategory, + shortcutsByCategory, +}: { + categories: string[]; + selectedCategory: string | null; + setSelectedCategory: (category: string | null) => void; + shortcutsByCategory: Record; +}) { + return ( + + ); +} + +function ShortcutsGrid({ + filteredShortcuts, + onShortcutClick, +}: { + filteredShortcuts: typeof allShortcuts; + onShortcutClick: (shortcut: (typeof allShortcuts)[0]) => void; +}) { + if (filteredShortcuts.length === 0) { + return ( +
    +
    + +

    + No shortcuts found matching your search. +

    +
    +
    + ); + } + + return ( +
    +
    + {filteredShortcuts.map((shortcut) => ( + onShortcutClick(shortcut)} + /> + ))} + +
    +
    + ); +} + +function ShortcutCard({ + shortcut, + onClick, +}: { + shortcut: (typeof allShortcuts)[0]; + onClick: () => void; +}) { + return ( + + ); +} + +function ContributeCard() { + return ( +
    +

    + Contribute a shortcut +

    +

    + Have a shortcut 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 shortcuts to get instant + insights from your meeting conversations. +

    +
    + +

    + Free to use. No credit card required. +

    +
    +
    +
    + ); +} + +function ShortcutModal({ + shortcut, + onClose, +}: { + shortcut: (typeof allShortcuts)[0]; + onClose: () => void; +}) { + return ( +
    +
    +
    +
    e.stopPropagation()} + > +
    +
    + + +
    +
    + + Shortcut + + + {shortcut.category} + +
    +

    + {shortcut.title} +

    +

    {shortcut.description}

    + +
    +
    + +
    +
    + +
    +
    + +

    + Download Hyprnote to use this shortcut +

    +
    +
    +
    +
    +
    +
    +
    + ); +} + +function getShortcutsByCategory() { + return allShortcuts.reduce( + (acc, shortcut) => { + const category = shortcut.category; + if (!acc[category]) { + acc[category] = []; + } + acc[category].push(shortcut); + return acc; + }, + {} as Record, + ); +} From d43fd91582219992fd4958f702bb73a285c7ab4d Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 29 Nov 2025 02:10:58 +0000 Subject: [PATCH 02/12] fix: resolve TypeScript errors and add raw MDX link button - Fix head function in gallery/index.tsx (use static title instead of search params) - Remove unused isTemplate variables in gallery/$type.$slug.tsx - Rename 'Gallery' to 'Prompt Gallery' in header navigation - Add 'View raw MDX source' button to gallery and shortcuts detail pages Co-Authored-By: john@hyprnote.com --- apps/web/src/components/header.tsx | 2 +- .../src/routes/_view/gallery/$type.$slug.tsx | 24 ++++++++-- apps/web/src/routes/_view/gallery/index.tsx | 47 +++++++++---------- apps/web/src/routes/_view/shortcuts/$slug.tsx | 16 ++++++- 4 files changed, 55 insertions(+), 34 deletions(-) diff --git a/apps/web/src/components/header.tsx b/apps/web/src/components/header.tsx index 574baa617b..3e46654f48 100644 --- a/apps/web/src/components/header.tsx +++ b/apps/web/src/components/header.tsx @@ -36,7 +36,7 @@ const featuresList = [ { to: "/product/ai-assistant", label: "AI Assistant" }, { to: "/product/mini-apps", label: "Mini Apps" }, { to: "/product/workflows", label: "Workflows", badge: "Coming Soon" }, - { to: "/gallery", label: "Gallery" }, + { to: "/gallery", label: "Prompt Gallery" }, ]; export function Header() { diff --git a/apps/web/src/routes/_view/gallery/$type.$slug.tsx b/apps/web/src/routes/_view/gallery/$type.$slug.tsx index ace008a10c..7a1aebdddf 100644 --- a/apps/web/src/routes/_view/gallery/$type.$slug.tsx +++ b/apps/web/src/routes/_view/gallery/$type.$slug.tsx @@ -68,7 +68,6 @@ export const Route = createFileRoute("/_view/gallery/$type/$slug")({ function Component() { const data = Route.useLoaderData(); const { type, item } = data; - const isTemplate = type === "template"; return (
    @@ -76,7 +75,7 @@ function Component() {
    - +
    @@ -158,8 +157,6 @@ function MainContent({ type: GalleryType; item: (typeof allTemplates)[0] | (typeof allShortcuts)[0]; }) { - const isTemplate = type === "template"; - return (
    @@ -338,9 +335,16 @@ function ItemFooter({ type }: { type: GalleryType }) { ); } -function RightSidebar({ type }: { type: GalleryType }) { +function RightSidebar({ + type, + item, +}: { + type: GalleryType; + item: (typeof allTemplates)[0] | (typeof allShortcuts)[0]; +}) { const isTemplate = type === "template"; const contentDir = isTemplate ? "templates" : "shortcuts"; + const rawMdxUrl = `https://github.com/fastrepl/hyprnote/blob/main/apps/web/content/${contentDir}/${item.slug}.mdx?plain=1`; return (
    + + + View raw MDX source + +

    Contribute

    diff --git a/apps/web/src/routes/_view/gallery/index.tsx b/apps/web/src/routes/_view/gallery/index.tsx index 8345161339..5c261a7ef2 100644 --- a/apps/web/src/routes/_view/gallery/index.tsx +++ b/apps/web/src/routes/_view/gallery/index.tsx @@ -28,32 +28,27 @@ export const Route = createFileRoute("/_view/gallery/")({ typeof search.category === "string" ? search.category : undefined, }; }, - head: ({ search }) => { - const typeLabel = - search.type === "shortcut" - ? "Shortcuts" - : search.type === "template" - ? "Templates" - : "Templates & Shortcuts"; - return { - meta: [ - { title: `${typeLabel} Gallery - Hyprnote` }, - { - name: "description", - content: - "Discover our library of AI meeting templates and shortcuts. Get structured summaries, extract action items, and more with Hyprnote's AI-powered tools.", - }, - { property: "og:title", content: `${typeLabel} Gallery - Hyprnote` }, - { - property: "og:description", - content: - "Browse our collection of AI meeting templates and shortcuts. From engineering standups to sales discovery calls, find the perfect tool for your workflow.", - }, - { property: "og:type", content: "website" }, - { property: "og:url", content: "https://hyprnote.com/gallery" }, - ], - }; - }, + head: () => ({ + meta: [ + { title: "Templates & Shortcuts Gallery - Hyprnote" }, + { + name: "description", + content: + "Discover our library of AI meeting templates and shortcuts. Get structured summaries, extract action items, and more with Hyprnote's AI-powered tools.", + }, + { + property: "og:title", + content: "Templates & Shortcuts Gallery - Hyprnote", + }, + { + property: "og:description", + content: + "Browse our collection of AI meeting templates and shortcuts. From engineering standups to sales discovery calls, find the perfect tool for your workflow.", + }, + { property: "og:type", content: "website" }, + { property: "og:url", content: "https://hyprnote.com/gallery" }, + ], + }), }); type GalleryItem = diff --git a/apps/web/src/routes/_view/shortcuts/$slug.tsx b/apps/web/src/routes/_view/shortcuts/$slug.tsx index baf79265f3..b5a67660a8 100644 --- a/apps/web/src/routes/_view/shortcuts/$slug.tsx +++ b/apps/web/src/routes/_view/shortcuts/$slug.tsx @@ -57,7 +57,7 @@ function Component() {

    - +
    @@ -213,7 +213,9 @@ function ShortcutFooter() { ); } -function RightSidebar() { +function RightSidebar({ shortcut }: { shortcut: (typeof allShortcuts)[0] }) { + const rawMdxUrl = `https://github.com/fastrepl/hyprnote/blob/main/apps/web/content/shortcuts/${shortcut.slug}.mdx?plain=1`; + return (
    @@ -398,14 +381,12 @@ function GallerySection({ setSelectedCategory, itemsByCategory, filteredItems, - onItemClick, }: { categories: string[]; selectedCategory: string | null; setSelectedCategory: (category: string | null) => void; itemsByCategory: Record; filteredItems: GalleryItem[]; - onItemClick: (item: GalleryItem) => void; }) { return (
    @@ -417,7 +398,7 @@ function GallerySection({ itemsByCategory={itemsByCategory} totalCount={filteredItems.length} /> - +
    ); @@ -480,13 +461,7 @@ function DesktopSidebar({ ); } -function GalleryGrid({ - filteredItems, - onItemClick, -}: { - filteredItems: GalleryItem[]; - onItemClick: (item: GalleryItem) => void; -}) { +function GalleryGrid({ filteredItems }: { filteredItems: GalleryItem[] }) { if (filteredItems.length === 0) { return (
    @@ -507,11 +482,7 @@ function GalleryGrid({
    {filteredItems.map((item) => ( - onItemClick(item)} - /> + ))}
    @@ -519,18 +490,12 @@ function GalleryGrid({ ); } -function ItemCard({ - item, - onClick, -}: { - item: GalleryItem; - onClick: () => void; -}) { +function ItemCard({ item }: { item: GalleryItem }) { const isTemplate = item.type === "template"; return ( - + )} + ); } @@ -626,126 +581,3 @@ function CTASection() {
    ); } - -function ItemModal({ - item, - onClose, -}: { - item: GalleryItem; - onClose: () => void; -}) { - const isTemplate = item.type === "template"; - - return ( -
    -
    -
    -
    e.stopPropagation()} - > -
    -
    - - -
    -
    - - {isTemplate ? "Template" : "Shortcut"} - - - {item.item.category} - -
    -

    - {item.item.title} -

    -

    {item.item.description}

    - - {isTemplate && "targets" in item.item && ( -
    - {item.item.targets.map((target) => ( - - {target} - - ))} -
    - )} - -
    - {isTemplate && "sections" in item.item && ( -
    -

    - Template Sections -

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

    - {section.title} -

    -
    -

    - {section.description} -

    -
    - ))} -
    -
    - )} - -
    - -
    -
    - -
    -
    - -

    - Download Hyprnote to use this{" "} - {isTemplate ? "template" : "shortcut"} -

    -
    -
    -
    -
    -
    -
    -
    - ); -} From 578c6e0382e233a83b802105ee99ee71ea51fa9f Mon Sep 17 00:00:00 2001 From: ComputelessComputer Date: Sun, 30 Nov 2025 12:23:56 +0900 Subject: [PATCH 10/12] refactor: simplify template targets display with comma-separated list --- apps/web/src/routes/_view/templates/index.tsx | 32 ++++++------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/apps/web/src/routes/_view/templates/index.tsx b/apps/web/src/routes/_view/templates/index.tsx index 2d1caf3542..a844bc6837 100644 --- a/apps/web/src/routes/_view/templates/index.tsx +++ b/apps/web/src/routes/_view/templates/index.tsx @@ -413,20 +413,8 @@ function TemplateCard({
    For
    -
    - {template.targets.slice(0, 3).map((target) => ( - - {target} - - ))} - {template.targets.length > 3 && ( - - +{template.targets.length - 3} more - - )} +
    + {template.targets.join(", ")}
    @@ -529,15 +517,13 @@ function TemplateModal({

    {template.description}

    -
    - {template.targets.map((target) => ( - - {target} - - ))} +
    + + For:{" "} + + + {template.targets.join(", ")} +
    From ad5eb769750965678fcb2e178398a3d9f24142c5 Mon Sep 17 00:00:00 2001 From: ComputelessComputer Date: Sun, 30 Nov 2025 12:24:16 +0900 Subject: [PATCH 11/12] feat: update gallery navigation label for clarity --- apps/web/src/components/header.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/components/header.tsx b/apps/web/src/components/header.tsx index 878daa77d3..bfcde3d2a0 100644 --- a/apps/web/src/components/header.tsx +++ b/apps/web/src/components/header.tsx @@ -35,7 +35,7 @@ const featuresList = [ { to: "/product/ai-notetaking", label: "AI Notetaking" }, { to: "/product/ai-assistant", label: "AI Assistant" }, { to: "/product/mini-apps", label: "Mini Apps" }, - { to: "/gallery", label: "Prompt Gallery" }, + { to: "/gallery", label: "Templates & Shortcuts" }, { to: "/product/workflows", label: "Workflows", badge: "Coming Soon" }, ]; From 5a3ee8126196c99c64bac0f77d157f7f6aedd94f Mon Sep 17 00:00:00 2001 From: ComputelessComputer Date: Sun, 30 Nov 2025 12:45:31 +0900 Subject: [PATCH 12/12] refactor(ui): Simplify category labels with consistent styling --- apps/web/src/routes/_view/gallery/index.tsx | 32 +++++++++---------- apps/web/src/routes/_view/shortcuts/index.tsx | 11 +++---- apps/web/src/routes/_view/templates/index.tsx | 7 +++- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/apps/web/src/routes/_view/gallery/index.tsx b/apps/web/src/routes/_view/gallery/index.tsx index 1f35c54600..34cacf1466 100644 --- a/apps/web/src/routes/_view/gallery/index.tsx +++ b/apps/web/src/routes/_view/gallery/index.tsx @@ -1,5 +1,5 @@ import { Icon } from "@iconify-icon/react"; -import { createFileRoute, useNavigate } from "@tanstack/react-router"; +import { createFileRoute, Link, useNavigate } from "@tanstack/react-router"; import { allShortcuts, allTemplates } from "content-collections"; import { CircleHelp } from "lucide-react"; import { useMemo, useState } from "react"; @@ -243,10 +243,13 @@ function HeroSection({ - + templates - + AI instructions for summarizing meetings @@ -257,10 +260,13 @@ function HeroSection({ - + shortcuts - + Quick commands for the AI chat assistant @@ -499,19 +505,13 @@ function ItemCard({ item }: { item: GalleryItem }) { className="group p-4 border border-neutral-200 rounded-sm bg-white hover:shadow-md hover:border-neutral-300 transition-all text-left cursor-pointer flex flex-col items-start" >
    -
    - +

    + {isTemplate ? "Template" : "Shortcut"} - {item.item.category} -

    + / + {item.item.category} +

    {item.item.title}

    diff --git a/apps/web/src/routes/_view/shortcuts/index.tsx b/apps/web/src/routes/_view/shortcuts/index.tsx index 4d473914bc..163c82893c 100644 --- a/apps/web/src/routes/_view/shortcuts/index.tsx +++ b/apps/web/src/routes/_view/shortcuts/index.tsx @@ -402,12 +402,11 @@ function ShortcutCard({ className="group p-4 border border-neutral-200 rounded-sm bg-white hover:shadow-md hover:border-neutral-300 transition-all text-left cursor-pointer flex flex-col items-start" >
    -
    - - Shortcut - - {shortcut.category} -
    +

    + Shortcut + / + {shortcut.category} +

    {shortcut.title}

    diff --git a/apps/web/src/routes/_view/templates/index.tsx b/apps/web/src/routes/_view/templates/index.tsx index a844bc6837..f3ab63623b 100644 --- a/apps/web/src/routes/_view/templates/index.tsx +++ b/apps/web/src/routes/_view/templates/index.tsx @@ -401,7 +401,12 @@ function TemplateCard({ onClick={onClick} className="group p-4 border border-neutral-200 rounded-sm bg-white hover:shadow-md hover:border-neutral-300 transition-all text-left cursor-pointer flex flex-col items-start" > -
    +
    +

    + Template + / + {template.category} +

    {template.title}