diff --git a/apps/web/src/components/footer.tsx b/apps/web/src/components/footer.tsx index a6b9f3f064..11138b72e3 100644 --- a/apps/web/src/components/footer.tsx +++ b/apps/web/src/components/footer.tsx @@ -18,306 +18,341 @@ export function Footer() { className={`${maxWidthClass} mx-auto px-4 laptop:px-0 py-12 lg:py-16 border-x border-neutral-100`} >
-
- - Hyprnote - -

- Fastrepl © {currentYear} -

-

- Are you in back-to-back meetings?{" "} - - Get started - -

-

- - Terms - - {" · "} - - Privacy - -

-
+ + +
+ + + ); +} -
-
-

- Product -

- -
+function BrandSection({ currentYear }: { currentYear: number }) { + return ( +
+ + Hyprnote + +

Fastrepl © {currentYear}

+

+ Are you in back-to-back meetings?{" "} + + Get started + +

+

+ + Terms + + {" · "} + + Privacy + +

+
+ ); +} -
-

- Resources -

- -
+function LinksGrid() { + return ( +
+ + + + + +
+ ); +} -
-

- Company -

- -
+function ProductLinks() { + return ( +
+

+ Product +

+ +
+ ); +} -
-

- Tools -

- -
+function ResourcesLinks() { + return ( +
+

+ Resources +

+ +
+ ); +} -
-

- Social -

- -
-
- - - +function CompanyLinks() { + return ( +
+

+ Company +

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

+ Tools +

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

+ Social +

+ +
); } diff --git a/apps/web/src/components/header.tsx b/apps/web/src/components/header.tsx index a1ff1f1983..c4b73918c7 100644 --- a/apps/web/src/components/header.tsx +++ b/apps/web/src/components/header.tsx @@ -10,6 +10,7 @@ import { useState } from "react"; import { Search } from "@/components/search"; import { useDocsDrawer } from "@/hooks/use-docs-drawer"; +import { useHandbookDrawer } from "@/hooks/use-handbook-drawer"; import { getPlatformCTA, usePlatform } from "@/hooks/use-platform"; function scrollToHero() { @@ -49,7 +50,10 @@ export function Header() { const router = useRouterState(); const maxWidthClass = getMaxWidthClass(router.location.pathname); const isDocsPage = router.location.pathname.startsWith("/docs"); + const isHandbookPage = + router.location.pathname.startsWith("/company-handbook"); const docsDrawer = useDocsDrawer(); + const handbookDrawer = useHandbookDrawer(); return ( <> @@ -58,314 +62,570 @@ export function Header() { className={`${maxWidthClass} mx-auto px-4 laptop:px-0 border-x border-neutral-100 h-full`} >
-
- {isDocsPage && docsDrawer && ( - - )} - - Hyprnote - -
setIsProductOpen(true)} - onMouseLeave={() => setIsProductOpen(false)} - > - - {isProductOpen && ( -
-
-
-
-
- Products -
- {productsList.map((link) => ( - setIsProductOpen(false)} - className="py-2 text-sm text-neutral-700 flex items-center justify-between hover:underline decoration-dotted" - > - {link.label} - {link.badge && ( - - {link.badge} - - )} - - ))} -
-
-
- Features -
-
- {featuresList.map((link) => ( - setIsProductOpen(false)} - className="py-2 text-sm text-neutral-700 flex items-center justify-between hover:underline decoration-dotted" - > - {link.label} - {link.badge && ( - - {link.badge} - - )} - - ))} -
-
-
-
-
- )} -
- - Docs - - - Blog - - - Pricing - - - Enterprise - -
+ + + +
+ + + + + + ); +} + +function LeftNav({ + isDocsPage, + isHandbookPage, + docsDrawer, + handbookDrawer, + setIsMenuOpen, + isProductOpen, + setIsProductOpen, +}: { + isDocsPage: boolean; + isHandbookPage: boolean; + docsDrawer: ReturnType; + handbookDrawer: ReturnType; + setIsMenuOpen: (open: boolean) => void; + isProductOpen: boolean; + setIsProductOpen: (open: boolean) => void; +}) { + return ( +
+ + + + +
+ ); +} + +function DrawerButton({ + isDocsPage, + isHandbookPage, + docsDrawer, + handbookDrawer, + setIsMenuOpen, +}: { + isDocsPage: boolean; + isHandbookPage: boolean; + docsDrawer: ReturnType; + handbookDrawer: ReturnType; + setIsMenuOpen: (open: boolean) => void; +}) { + if (isDocsPage && docsDrawer) { + return ( + + ); + } - + if (isHandbookPage && handbookDrawer) { + return ( + + ); + } + + return null; +} -
- {platformCTA.action === "download" ? ( - - {platformCTA.label} - - ) : ( - - {platform === "mobile" ? "Get reminder" : platformCTA.label} - - )} - +function Logo() { + return ( + + Hyprnote + + ); +} + +function ProductDropdown({ + isProductOpen, + setIsProductOpen, +}: { + isProductOpen: boolean; + setIsProductOpen: (open: boolean) => void; +}) { + return ( +
setIsProductOpen(true)} + onMouseLeave={() => setIsProductOpen(false)} + > + + {isProductOpen && ( +
+
+
+ setIsProductOpen(false)} /> + setIsProductOpen(false)} />
- + )} +
+ ); +} + +function ProductsList({ onClose }: { onClose: () => void }) { + return ( +
+
+ Products +
+ {productsList.map((link) => ( + + {link.label} + {link.badge && ( + + {link.badge} + + )} + + ))} +
+ ); +} + +function FeaturesList({ onClose }: { onClose: () => void }) { + return ( +
+
+ Features +
+ {featuresList.map((link) => ( + + {link.label} + {link.badge && ( + + {link.badge} + + )} + + ))} +
+ ); +} - {isMenuOpen && ( - <> -
- +function DesktopNav({ + platformCTA, +}: { + platformCTA: ReturnType; +}) { + return ( + + ); +} + +function MobileNav({ + platform, + platformCTA, + isMenuOpen, + setIsMenuOpen, + docsDrawer, + handbookDrawer, +}: { + platform: string; + platformCTA: ReturnType; + isMenuOpen: boolean; + setIsMenuOpen: (open: boolean) => void; + docsDrawer: ReturnType; + handbookDrawer: ReturnType; +}) { + return ( +
+ + +
+ ); +} + +function CTAButton({ + platformCTA, + platform, + mobile = false, +}: { + platformCTA: ReturnType; + platform?: string; + mobile?: boolean; +}) { + const baseClass = mobile + ? "px-4 h-8 flex items-center text-sm bg-linear-to-t from-stone-600 to-stone-500 text-white rounded-full shadow-md active:scale-[98%] transition-all" + : "px-4 h-8 flex items-center text-sm bg-linear-to-t from-stone-600 to-stone-500 text-white rounded-full shadow-md hover:shadow-lg hover:scale-[102%] active:scale-[98%] transition-all"; + + if (platformCTA.action === "download") { + return ( + + {platformCTA.label} + + ); + } + + return ( + + {mobile && platform === "mobile" ? "Get reminder" : platformCTA.label} + + ); +} + +function MobileMenu({ + isMenuOpen, + setIsMenuOpen, + isProductOpen, + setIsProductOpen, + platform, + platformCTA, + maxWidthClass, +}: { + isMenuOpen: boolean; + setIsMenuOpen: (open: boolean) => void; + isProductOpen: boolean; + setIsProductOpen: (open: boolean) => void; + platform: string; + platformCTA: ReturnType; + maxWidthClass: string; +}) { + if (!isMenuOpen) return null; + + return ( + <> +
setIsMenuOpen(false)} + /> +
+ +
); } + +function MobileMenuLinks({ + isProductOpen, + setIsProductOpen, + setIsMenuOpen, +}: { + isProductOpen: boolean; + setIsProductOpen: (open: boolean) => void; + setIsMenuOpen: (open: boolean) => void; +}) { + return ( +
+ + setIsMenuOpen(false)} + className="block text-base text-neutral-700 hover:text-neutral-900 transition-colors" + > + Docs + + setIsMenuOpen(false)} + className="block text-base text-neutral-700 hover:text-neutral-900 transition-colors" + > + Blog + + setIsMenuOpen(false)} + className="block text-base text-neutral-700 hover:text-neutral-900 transition-colors" + > + Pricing + + setIsMenuOpen(false)} + className="block text-base text-neutral-700 hover:text-neutral-900 transition-colors" + > + Enterprise + +
+ ); +} + +function MobileProductSection({ + isProductOpen, + setIsProductOpen, + setIsMenuOpen, +}: { + isProductOpen: boolean; + setIsProductOpen: (open: boolean) => void; + setIsMenuOpen: (open: boolean) => void; +}) { + return ( +
+ + {isProductOpen && ( +
+ + +
+ )} +
+ ); +} + +function MobileProductsList({ + setIsMenuOpen, +}: { + setIsMenuOpen: (open: boolean) => void; +}) { + return ( +
+
+ Products +
+ {productsList.map((link) => ( + setIsMenuOpen(false)} + className="text-sm text-neutral-600 hover:text-neutral-900 transition-colors flex items-center justify-between py-1" + > + {link.label} + {link.badge && ( + + {link.badge} + + )} + + ))} +
+ ); +} + +function MobileFeaturesList({ + setIsMenuOpen, +}: { + setIsMenuOpen: (open: boolean) => void; +}) { + return ( +
+
+ Features +
+ {featuresList.map((link) => ( + setIsMenuOpen(false)} + className="text-sm text-neutral-600 hover:text-neutral-900 transition-colors flex items-center justify-between py-1" + > + {link.label} + {link.badge && ( + + {link.badge} + + )} + + ))} +
+ ); +} + +function MobileMenuCTAs({ + platform, + platformCTA, + setIsMenuOpen, +}: { + platform: string; + platformCTA: ReturnType; + setIsMenuOpen: (open: boolean) => void; +}) { + return ( +
+ setIsMenuOpen(false)} + className="block w-full px-4 py-3 text-center text-sm text-neutral-700 border border-neutral-200 rounded-lg hover:bg-neutral-50 transition-colors" + > + Get started + + {platformCTA.action === "download" ? ( + setIsMenuOpen(false)} + className="block w-full px-4 py-3 text-center text-sm bg-linear-to-t from-stone-600 to-stone-500 text-white rounded-lg shadow-md active:scale-[98%] transition-all" + > + {platformCTA.label} + + ) : ( + { + setIsMenuOpen(false); + scrollToHero(); + }} + className="block w-full px-4 py-3 text-center text-sm bg-linear-to-t from-stone-600 to-stone-500 text-white rounded-lg shadow-md active:scale-[98%] transition-all" + > + {platform === "mobile" ? "Get reminder" : platformCTA.label} + + )} +
+ ); +} diff --git a/apps/web/src/components/sidebar-navigation.tsx b/apps/web/src/components/sidebar-navigation.tsx new file mode 100644 index 0000000000..593c3ade7b --- /dev/null +++ b/apps/web/src/components/sidebar-navigation.tsx @@ -0,0 +1,71 @@ +import { Link } from "@tanstack/react-router"; +import { useEffect, useRef } from "react"; + +export function SidebarNavigation({ + sections, + currentSlug, + onLinkClick, + scrollContainerRef, + linkTo, +}: { + sections: { title: string; docs: T[] }[]; + currentSlug: string | undefined; + onLinkClick?: () => void; + scrollContainerRef?: React.RefObject; + linkTo: string; +}) { + const activeLinkRef = useRef(null); + + useEffect(() => { + if (activeLinkRef.current && scrollContainerRef?.current) { + const container = scrollContainerRef.current; + const activeLink = activeLinkRef.current; + + requestAnimationFrame(() => { + const containerRect = container.getBoundingClientRect(); + const linkRect = activeLink.getBoundingClientRect(); + + const scrollTop = + activeLink.offsetTop - + container.offsetTop - + containerRect.height / 2 + + linkRect.height / 2; + + container.scrollTop = scrollTop; + }); + } + }, [currentSlug, scrollContainerRef]); + + return ( + + ); +} diff --git a/apps/web/src/hooks/use-handbook-drawer.ts b/apps/web/src/hooks/use-handbook-drawer.ts new file mode 100644 index 0000000000..eb0540a1c0 --- /dev/null +++ b/apps/web/src/hooks/use-handbook-drawer.ts @@ -0,0 +1,13 @@ +import { createContext, useContext } from "react"; + +interface HandbookDrawerContextType { + isOpen: boolean; + setIsOpen: (open: boolean) => void; +} + +export const HandbookDrawerContext = + createContext(null); + +export function useHandbookDrawer() { + return useContext(HandbookDrawerContext); +} diff --git a/apps/web/src/routeTree.gen.ts b/apps/web/src/routeTree.gen.ts index 0dd2a7b37c..5fd76a3fa6 100644 --- a/apps/web/src/routeTree.gen.ts +++ b/apps/web/src/routeTree.gen.ts @@ -31,7 +31,6 @@ import { Route as ViewOssFriendsRouteImport } from './routes/_view/oss-friends' import { Route as ViewOpensourceRouteImport } from './routes/_view/opensource' import { Route as ViewFreeRouteImport } from './routes/_view/free' import { Route as ViewFileTranscriptionRouteImport } from './routes/_view/file-transcription' -import { Route as ViewFaqRouteImport } from './routes/_view/faq' import { Route as ViewEnterpriseRouteImport } from './routes/_view/enterprise' import { Route as ViewBrandRouteImport } from './routes/_view/brand' import { Route as ViewAboutRouteImport } from './routes/_view/about' @@ -199,11 +198,6 @@ const ViewFileTranscriptionRoute = ViewFileTranscriptionRouteImport.update({ path: '/file-transcription', getParentRoute: () => ViewRouteRoute, } as any) -const ViewFaqRoute = ViewFaqRouteImport.update({ - id: '/faq', - path: '/faq', - getParentRoute: () => ViewRouteRoute, -} as any) const ViewEnterpriseRoute = ViewEnterpriseRouteImport.update({ id: '/enterprise', path: '/enterprise', @@ -514,7 +508,6 @@ export interface FileRoutesByFullPath { '/about': typeof ViewAboutRoute '/brand': typeof ViewBrandRoute '/enterprise': typeof ViewEnterpriseRoute - '/faq': typeof ViewFaqRoute '/file-transcription': typeof ViewFileTranscriptionRoute '/free': typeof ViewFreeRoute '/opensource': typeof ViewOpensourceRoute @@ -592,7 +585,6 @@ export interface FileRoutesByTo { '/about': typeof ViewAboutRoute '/brand': typeof ViewBrandRoute '/enterprise': typeof ViewEnterpriseRoute - '/faq': typeof ViewFaqRoute '/file-transcription': typeof ViewFileTranscriptionRoute '/free': typeof ViewFreeRoute '/opensource': typeof ViewOpensourceRoute @@ -675,7 +667,6 @@ export interface FileRoutesById { '/_view/about': typeof ViewAboutRoute '/_view/brand': typeof ViewBrandRoute '/_view/enterprise': typeof ViewEnterpriseRoute - '/_view/faq': typeof ViewFaqRoute '/_view/file-transcription': typeof ViewFileTranscriptionRoute '/_view/free': typeof ViewFreeRoute '/_view/opensource': typeof ViewOpensourceRoute @@ -758,7 +749,6 @@ export interface FileRouteTypes { | '/about' | '/brand' | '/enterprise' - | '/faq' | '/file-transcription' | '/free' | '/opensource' @@ -836,7 +826,6 @@ export interface FileRouteTypes { | '/about' | '/brand' | '/enterprise' - | '/faq' | '/file-transcription' | '/free' | '/opensource' @@ -918,7 +907,6 @@ export interface FileRouteTypes { | '/_view/about' | '/_view/brand' | '/_view/enterprise' - | '/_view/faq' | '/_view/file-transcription' | '/_view/free' | '/_view/opensource' @@ -1158,13 +1146,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ViewFileTranscriptionRouteImport parentRoute: typeof ViewRouteRoute } - '/_view/faq': { - id: '/_view/faq' - path: '/faq' - fullPath: '/faq' - preLoaderRoute: typeof ViewFaqRouteImport - parentRoute: typeof ViewRouteRoute - } '/_view/enterprise': { id: '/_view/enterprise' path: '/enterprise' @@ -1636,7 +1617,6 @@ interface ViewRouteRouteChildren { ViewAboutRoute: typeof ViewAboutRoute ViewBrandRoute: typeof ViewBrandRoute ViewEnterpriseRoute: typeof ViewEnterpriseRoute - ViewFaqRoute: typeof ViewFaqRoute ViewFileTranscriptionRoute: typeof ViewFileTranscriptionRoute ViewFreeRoute: typeof ViewFreeRoute ViewOpensourceRoute: typeof ViewOpensourceRoute @@ -1694,7 +1674,6 @@ const ViewRouteRouteChildren: ViewRouteRouteChildren = { ViewAboutRoute: ViewAboutRoute, ViewBrandRoute: ViewBrandRoute, ViewEnterpriseRoute: ViewEnterpriseRoute, - ViewFaqRoute: ViewFaqRoute, ViewFileTranscriptionRoute: ViewFileTranscriptionRoute, ViewFreeRoute: ViewFreeRoute, ViewOpensourceRoute: ViewOpensourceRoute, diff --git a/apps/web/src/routes/_view/company-handbook/route.tsx b/apps/web/src/routes/_view/company-handbook/route.tsx index 36ef19c14c..f6c71b0fc1 100644 --- a/apps/web/src/routes/_view/company-handbook/route.tsx +++ b/apps/web/src/routes/_view/company-handbook/route.tsx @@ -1,11 +1,8 @@ -import { - createFileRoute, - Link, - Outlet, - useMatchRoute, -} from "@tanstack/react-router"; +import { createFileRoute, Outlet, useMatchRoute } from "@tanstack/react-router"; import { allHandbooks } from "content-collections"; -import { useMemo } from "react"; +import { useMemo, useRef } from "react"; + +import { SidebarNavigation } from "@/components/sidebar-navigation"; import { handbookStructure } from "./-structure"; @@ -72,34 +69,20 @@ function LeftSidebar() { return { sections }; }, []); + const scrollContainerRef = useRef(null); + return ( ); diff --git a/apps/web/src/routes/_view/docs/-structure.ts b/apps/web/src/routes/_view/docs/-structure.ts index 445dc29875..ad43fea4da 100644 --- a/apps/web/src/routes/_view/docs/-structure.ts +++ b/apps/web/src/routes/_view/docs/-structure.ts @@ -1,3 +1,5 @@ +import { allDocs } from "content-collections"; + export const docsStructure = { sections: ["about", "developers", "pro", "faq"], defaultPages: { @@ -7,3 +9,44 @@ export const docsStructure = { faq: "faq/general", } as Record, }; + +export function getDocsBySection() { + const sectionGroups: Record< + string, + { title: string; docs: (typeof allDocs)[0][] } + > = {}; + + allDocs.forEach((doc) => { + if (doc.slug === "index" || doc.isIndex) { + return; + } + + const sectionName = doc.section; + + if (!sectionGroups[sectionName]) { + sectionGroups[sectionName] = { + title: sectionName, + docs: [], + }; + } + + sectionGroups[sectionName].docs.push(doc); + }); + + Object.keys(sectionGroups).forEach((sectionName) => { + sectionGroups[sectionName].docs.sort((a, b) => a.order - b.order); + }); + + const sections = docsStructure.sections + .map((sectionId) => { + return Object.values(sectionGroups).find( + (group) => group.title.toLowerCase() === sectionId.toLowerCase(), + ); + }) + .filter( + (section): section is NonNullable => + section !== undefined, + ); + + return { sections }; +} diff --git a/apps/web/src/routes/_view/docs/route.tsx b/apps/web/src/routes/_view/docs/route.tsx index 8b9304bbc4..9c4ac1cc1e 100644 --- a/apps/web/src/routes/_view/docs/route.tsx +++ b/apps/web/src/routes/_view/docs/route.tsx @@ -1,13 +1,9 @@ -import { - createFileRoute, - Link, - Outlet, - useMatchRoute, -} from "@tanstack/react-router"; -import { allDocs } from "content-collections"; -import { useMemo } from "react"; +import { createFileRoute, Outlet, useMatchRoute } from "@tanstack/react-router"; +import { useRef } from "react"; -import { docsStructure } from "./-structure"; +import { SidebarNavigation } from "@/components/sidebar-navigation"; + +import { getDocsBySection } from "./-structure"; export const Route = createFileRoute("/_view/docs")({ component: Component, @@ -34,94 +30,22 @@ function LeftSidebar() { match && typeof match !== "boolean" ? match._splat : undefined ) as string | undefined; - const docsBySection = useMemo(() => { - const sectionGroups: Record< - string, - { title: string; docs: (typeof allDocs)[0][] } - > = {}; - - allDocs.forEach((doc) => { - if (doc.slug === "index" || doc.isIndex) { - return; - } - - const sectionName = doc.section; - - if (!sectionGroups[sectionName]) { - sectionGroups[sectionName] = { - title: sectionName, - docs: [], - }; - } - - sectionGroups[sectionName].docs.push(doc); - }); - - Object.keys(sectionGroups).forEach((sectionName) => { - sectionGroups[sectionName].docs.sort((a, b) => a.order - b.order); - }); - - const sections = docsStructure.sections - .map((sectionId) => { - return Object.values(sectionGroups).find( - (group) => group.title.toLowerCase() === sectionId.toLowerCase(), - ); - }) - .filter( - (section): section is NonNullable => - section !== undefined, - ); - - return { sections }; - }, []); + const { sections } = getDocsBySection(); + const scrollContainerRef = useRef(null); return ( ); } - -function DocsNavigation({ - sections, - currentSlug, - onLinkClick, -}: { - sections: { title: string; docs: (typeof allDocs)[0][] }[]; - currentSlug: string | undefined; - onLinkClick?: () => void; -}) { - return ( - - ); -} diff --git a/apps/web/src/routes/_view/faq.tsx b/apps/web/src/routes/_view/faq.tsx deleted file mode 100644 index b3b069a63b..0000000000 --- a/apps/web/src/routes/_view/faq.tsx +++ /dev/null @@ -1,272 +0,0 @@ -import { Icon } from "@iconify-icon/react"; -import { createFileRoute } from "@tanstack/react-router"; -import { useState } from "react"; - -import { cn } from "@hypr/utils"; - -export const Route = createFileRoute("/_view/faq")({ - component: Component, - head: () => ({ - meta: [ - { title: "Frequently Asked Questions - Hyprnote" }, - { - name: "description", - content: - "Find answers to common questions about Hyprnote, including features, privacy, pricing, and technical support.", - }, - ], - }), -}); - -interface FAQItem { - question: string; - answer: string; - category: string; -} - -const faqs: FAQItem[] = [ - { - category: "General", - question: "What is Hyprnote?", - answer: - "Hyprnote is a desktop notetaking application that captures both your microphone and system audio. It uses local AI to transcribe conversations, generate summaries, and help you organize your notes - all while keeping your data private on your device.", - }, - { - category: "General", - question: "How is Hyprnote different from other meeting recorders?", - answer: - "Unlike bot-based recorders that join your meetings, Hyprnote runs locally on your computer and captures audio directly from your system. This means it works with any application, doesn't require meeting permissions, and keeps all your data private.", - }, - { - category: "Features", - question: "What apps does Hyprnote work with?", - answer: - "Hyprnote works with any application on your computer - Zoom, Google Meet, Microsoft Teams, Slack, Discord, and more. It captures system audio, so it doesn't depend on specific integrations.", - }, - { - category: "Features", - question: "Can I record in-person conversations?", - answer: - "Yes! Hyprnote captures your microphone input, so you can record in-person meetings, phone calls, or any conversation where you're using your computer's microphone.", - }, - { - category: "Features", - question: "What languages does Hyprnote support?", - answer: - "Currently, Hyprnote supports English transcription. We're working on adding support for more languages in upcoming releases.", - }, - { - category: "Privacy", - question: "Where is my data stored?", - answer: - "All your recordings and notes are stored locally on your device. Hyprnote doesn't send your audio or transcripts to external servers. Everything is processed on-device using local AI.", - }, - { - category: "Privacy", - question: "Is my data encrypted?", - answer: - "Yes, all your data is encrypted at rest on your device. Your recordings, transcripts, and notes are protected with industry-standard encryption.", - }, - { - category: "Privacy", - question: "Can my employer see my Hyprnote recordings?", - answer: - "No. Hyprnote stores everything locally on your device. Unless you explicitly share your notes or recordings, they remain private to you.", - }, - { - category: "Technical", - question: "What are the system requirements?", - answer: - "Hyprnote requires macOS 12.0 or later with Apple Silicon (M1/M2/M3) or Intel processor. We recommend at least 8GB of RAM for optimal performance with local AI features.", - }, - { - category: "Technical", - question: "How much storage space does Hyprnote need?", - answer: - "The app itself is about 200MB. Recording storage depends on your usage - a 1-hour meeting uses approximately 50-100MB. We recommend having at least 5GB of free space.", - }, - { - category: "Technical", - question: "Does Hyprnote work offline?", - answer: - "Yes! Since Hyprnote uses local AI, it works completely offline. You don't need an internet connection to record, transcribe, or generate summaries.", - }, - { - category: "Pricing", - question: "Is Hyprnote free?", - answer: - "Hyprnote offers a free tier with core features. Pro features like unlimited recording time, advanced templates, and priority support are available through our paid plans.", - }, - { - category: "Pricing", - question: "Can I try Pro features before subscribing?", - answer: - "Yes! New users get a 14-day free trial of Hyprnote Pro to try all features before committing to a subscription.", - }, - { - category: "Support", - question: "How do I get help if something isn't working?", - answer: - "You can reach our support team at support@hyprnote.com or join our Discord community for quick help. We also have comprehensive documentation at docs.hyprnote.com.", - }, - { - category: "Support", - question: "Do you offer refunds?", - answer: - "Yes, we offer a 30-day money-back guarantee. If you're not satisfied with Hyprnote Pro, contact us within 30 days of purchase for a full refund.", - }, -]; - -const categories = Array.from(new Set(faqs.map((faq) => faq.category))); - -function Component() { - const [selectedCategory, setSelectedCategory] = useState("All"); - const [openIndex, setOpenIndex] = useState(null); - - const filteredFAQs = - selectedCategory === "All" - ? faqs - : faqs.filter((faq) => faq.category === selectedCategory); - - return ( -
-
-
-
-

- Frequently Asked Questions -

-

- Find answers to common questions about Hyprnote. Can't find what - you're looking for?{" "} - - Contact us - - . -

-
- -
- - {categories.map((category) => ( - - ))} -
- -
- {filteredFAQs.map((faq, index) => ( - setOpenIndex(openIndex === index ? null : index)} - /> - ))} -
- -
- -

- Still have questions? -

-

- Our team is here to help. Reach out and we'll get back to you as - soon as possible. -

- -
-
-
-
- ); -} - -function FAQItem({ - faq, - isOpen, - onClick, -}: { - faq: FAQItem; - isOpen: boolean; - onClick: () => void; -}) { - return ( -
- - {isOpen && ( -
- {faq.answer} -
- )} -
- ); -} diff --git a/apps/web/src/routes/_view/route.tsx b/apps/web/src/routes/_view/route.tsx index 9e8d40ec5b..f65e895b07 100644 --- a/apps/web/src/routes/_view/route.tsx +++ b/apps/web/src/routes/_view/route.tsx @@ -1,18 +1,20 @@ import { createFileRoute, - Link, Outlet, useMatchRoute, useRouterState, } from "@tanstack/react-router"; -import { allDocs } from "content-collections"; -import { createContext, useContext, useMemo, useState } from "react"; +import { allHandbooks } from "content-collections"; +import { createContext, useContext, useMemo, useRef, useState } from "react"; import { Footer } from "@/components/footer"; import { Header } from "@/components/header"; +import { SidebarNavigation } from "@/components/sidebar-navigation"; import { DocsDrawerContext } from "@/hooks/use-docs-drawer"; +import { HandbookDrawerContext } from "@/hooks/use-handbook-drawer"; -import { docsStructure } from "./docs/-structure"; +import { handbookStructure } from "./company-handbook/-structure"; +import { getDocsBySection } from "./docs/-structure"; export const Route = createFileRoute("/_view")({ component: Component, @@ -32,8 +34,11 @@ export function useHeroContext() { function Component() { const router = useRouterState(); const isDocsPage = router.location.pathname.startsWith("/docs"); + const isHandbookPage = + router.location.pathname.startsWith("/company-handbook"); const [onTrigger, setOnTrigger] = useState<(() => void) | null>(null); const [isDocsDrawerOpen, setIsDocsDrawerOpen] = useState(false); + const [isHandbookDrawerOpen, setIsHandbookDrawerOpen] = useState(false); return ( -
-
-
- -
-
- {isDocsPage && ( - setIsDocsDrawerOpen(false)} - /> - )} -
+ +
+
+
+ +
+
+ {isDocsPage && ( + setIsDocsDrawerOpen(false)} + /> + )} + {isHandbookPage && ( + setIsHandbookDrawerOpen(false)} + /> + )} +
+
); @@ -77,13 +95,55 @@ function MobileDocsDrawer({ match && typeof match !== "boolean" ? match._splat : undefined ) as string | undefined; - const docsBySection = useMemo(() => { + const { sections } = getDocsBySection(); + const scrollContainerRef = useRef(null); + + return ( +
+
+ +
+
+ ); +} + +function MobileHandbookDrawer({ + isOpen, + onClose, +}: { + isOpen: boolean; + onClose: () => void; +}) { + const matchRoute = useMatchRoute(); + const match = matchRoute({ to: "/company-handbook/$", fuzzy: true }); + + const currentSlug = ( + match && typeof match !== "boolean" ? match._splat : undefined + ) as string | undefined; + + const handbooksBySection = useMemo(() => { const sectionGroups: Record< string, - { title: string; docs: (typeof allDocs)[0][] } + { title: string; docs: (typeof allHandbooks)[0][] } > = {}; - allDocs.forEach((doc) => { + allHandbooks.forEach((doc) => { if (doc.slug === "index" || doc.isIndex) { return; } @@ -104,7 +164,7 @@ function MobileDocsDrawer({ sectionGroups[sectionName].docs.sort((a, b) => a.order - b.order); }); - const sections = docsStructure.sections + const sections = handbookStructure.sections .map((sectionId) => { const sectionName = sectionId.charAt(0).toUpperCase() + sectionId.slice(1); @@ -115,58 +175,29 @@ function MobileDocsDrawer({ return { sections }; }, []); + const scrollContainerRef = useRef(null); + return (
-
- +
); } - -function DocsNavigation({ - sections, - currentSlug, - onLinkClick, -}: { - sections: { title: string; docs: (typeof allDocs)[0][] }[]; - currentSlug: string | undefined; - onLinkClick?: () => void; -}) { - return ( - - ); -} diff --git a/apps/web/src/utils/sitemap.ts b/apps/web/src/utils/sitemap.ts index 0077a4a5a6..8577a57a9d 100644 --- a/apps/web/src/utils/sitemap.ts +++ b/apps/web/src/utils/sitemap.ts @@ -134,10 +134,6 @@ export function getSitemap(): Sitemap { priority: 0.6, changeFrequency: "weekly", }, - "/faq": { - priority: 0.7, - changeFrequency: "monthly", - }, "/file-transcription": { priority: 0.7, changeFrequency: "monthly",