diff --git a/apps/web/package.json b/apps/web/package.json index 55bdb07ee..9c37d4616 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -14,6 +14,7 @@ "@hypr/ui": "workspace:*", "@hypr/utils": "workspace:*", "@iconify-icon/react": "^3.0.1", + "@mux/mux-player-react": "^3.7.0", "@nangohq/frontend": "^0.69.5", "@nangohq/node": "^0.69.5", "@sentry/tanstackstart-react": "^10.22.0", diff --git a/apps/web/public/favicon.ico b/apps/web/public/favicon.ico index a11777cc4..7ce4a1bd6 100644 Binary files a/apps/web/public/favicon.ico and b/apps/web/public/favicon.ico differ diff --git a/apps/web/public/hyprnote_with_noise.png b/apps/web/public/hyprnote/icon.png similarity index 100% rename from apps/web/public/hyprnote_with_noise.png rename to apps/web/public/hyprnote/icon.png diff --git a/apps/web/public/hyprnote/logo.svg b/apps/web/public/hyprnote/logo.svg new file mode 100644 index 000000000..bd69f4871 --- /dev/null +++ b/apps/web/public/hyprnote/logo.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/apps/web/public/hyprnote/signature.svg b/apps/web/public/hyprnote/signature.svg new file mode 100644 index 000000000..19bcf4469 --- /dev/null +++ b/apps/web/public/hyprnote/signature.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/public/icons/adobe.svg b/apps/web/public/icons/adobe.svg index 122fe18f6..2ebd6f2c4 100644 --- a/apps/web/public/icons/adobe.svg +++ b/apps/web/public/icons/adobe.svg @@ -1,10 +1 @@ - - - - - - \ No newline at end of file + diff --git a/apps/web/public/icons/yc_stone.svg b/apps/web/public/icons/yc_stone.svg new file mode 100644 index 000000000..473c62784 --- /dev/null +++ b/apps/web/public/icons/yc_stone.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/apps/web/public/patterns/paper.png b/apps/web/public/patterns/paper.png new file mode 100644 index 000000000..f90fab192 Binary files /dev/null and b/apps/web/public/patterns/paper.png differ diff --git a/apps/web/public/patterns/white_leather.png b/apps/web/public/patterns/white_leather.png new file mode 100644 index 000000000..e7701de5e Binary files /dev/null and b/apps/web/public/patterns/white_leather.png differ diff --git a/apps/web/public/static.gif b/apps/web/public/static.gif new file mode 100644 index 000000000..5329fc62c Binary files /dev/null and b/apps/web/public/static.gif differ diff --git a/apps/web/public/team/john.png b/apps/web/public/team/john.png new file mode 100644 index 000000000..64839f6da Binary files /dev/null and b/apps/web/public/team/john.png differ diff --git a/apps/web/public/team/yujong.png b/apps/web/public/team/yujong.png new file mode 100644 index 000000000..fbdd02b5e Binary files /dev/null and b/apps/web/public/team/yujong.png differ diff --git a/apps/web/src/components/download-button.tsx b/apps/web/src/components/download-button.tsx index 939cb81c5..c84fda54f 100644 --- a/apps/web/src/components/download-button.tsx +++ b/apps/web/src/components/download-button.tsx @@ -1,10 +1,11 @@ import { cn } from "@hypr/utils"; + import { Link } from "@tanstack/react-router"; export function DownloadButton() { return ( n > 1000 ? `${(n / 1000).toFixed(1)}k` : n; + + return ( +
+

+ {type === "stars" ? "Stars" : "Forks"} +

+

{renderCount(count)}

+
+ ); +} + +function OpenSourceButton({ showStars = false, starCount }: { showStars?: boolean; starCount?: number }) { + const renderCount = (n: number) => n > 1000 ? `${(n / 1000).toFixed(1)}k` : n; + + return ( +
+

Open source

+

+ {"Hyprnote values privacy and community, so it's been transparent from day one"} +

+ + + View on GitHub + {showStars && starCount && ( + <> + +
+ + {renderCount(starCount)} +
+ + )} +
+
+ ); +} + +function Avatar({ username, avatar }: { username: string; avatar: string }) { + return ( + + {`${username}'s + + ); +} + +function GridRow({ + children, +}: { + children: React.ReactNode; +}) { + return
{children}
; +} + +export function GitHubOpenSource() { + const STARS_COUNT = 6419; + const FORKS_COUNT = 396; + + return ( +
+
+
+ +
+ +
+
+
+ + {CURATED_PROFILES.slice(0, 2).map((profile) => ( + + ))} + + + {CURATED_PROFILES.slice(2, 4).map((profile) => ( + + ))} + + + {CURATED_PROFILES.slice(4, 6).map((profile) => ( + + ))} + + + {CURATED_PROFILES.slice(6, 8).map((profile) => ( + + ))} + +
+ +
+ + {CURATED_PROFILES.slice(8, 10).map((profile) => ( + + ))} + + + + {CURATED_PROFILES.slice(10, 12).map((profile) => ( + + ))} + +
+ +
+ + {CURATED_PROFILES.slice(12, 15).map((profile) => ( + + ))} + + + {CURATED_PROFILES.slice(15, 18).map((profile) => ( + + ))} + + + {CURATED_PROFILES.slice(18, 21).map((profile) => ( + + ))} + + + {CURATED_PROFILES.slice(21, 24).map((profile) => ( + + ))} + +
+
+ +
+ +
+ +
+
+ + {CURATED_PROFILES.slice(24, 27).map((profile) => ( + + ))} + + + {CURATED_PROFILES.slice(27, 30).map((profile) => ( + + ))} + + + {CURATED_PROFILES.slice(30, 33).map((profile) => ( + + ))} + + + {CURATED_PROFILES.slice(33, 36).map((profile) => ( + + ))} + +
+ +
+ + {CURATED_PROFILES.slice(36, 38).map((profile) => ( + + ))} + + + + {CURATED_PROFILES.slice(38, 40).map((profile) => ( + + ))} + +
+ +
+ + {CURATED_PROFILES.slice(40, 42).map((profile) => ( + + ))} + + + {CURATED_PROFILES.slice(42, 44).map((profile) => ( + + ))} + + + {CURATED_PROFILES.slice(44, 46).map((profile) => ( + + ))} + + + {CURATED_PROFILES.slice(46, 48).map((profile) => ( + + ))} + +
+
+
+
+
+ ); +} diff --git a/apps/web/src/components/github-stars.tsx b/apps/web/src/components/github-stars.tsx index c245f32d5..1b290715b 100644 --- a/apps/web/src/components/github-stars.tsx +++ b/apps/web/src/components/github-stars.tsx @@ -1,4 +1,5 @@ import { cn } from "@hypr/utils"; + import { Icon } from "@iconify-icon/react"; import { useQuery } from "@tanstack/react-query"; diff --git a/apps/web/src/components/join-waitlist-button.tsx b/apps/web/src/components/join-waitlist-button.tsx new file mode 100644 index 000000000..ef26ccc05 --- /dev/null +++ b/apps/web/src/components/join-waitlist-button.tsx @@ -0,0 +1,33 @@ +import { cn } from "@hypr/utils"; + +export function JoinWaitlistButton() { + return ( + + Join waitlist + + + + + ); +} diff --git a/apps/web/src/components/logo-cloud.tsx b/apps/web/src/components/logo-cloud.tsx index c6859fc9b..875a7f660 100644 --- a/apps/web/src/components/logo-cloud.tsx +++ b/apps/web/src/components/logo-cloud.tsx @@ -15,14 +15,14 @@ function LogoCard({ logo, className, children, ...props }: LogoCardProps) { return (
{logo.alt} Download diff --git a/apps/web/src/components/social-card.tsx b/apps/web/src/components/social-card.tsx index 9ba259238..82e6c526b 100644 --- a/apps/web/src/components/social-card.tsx +++ b/apps/web/src/components/social-card.tsx @@ -8,7 +8,6 @@ export interface SocialCardProps { body: string; url: string; className?: string; - // Platform-specific metadata username?: string; // for Twitter subreddit?: string; // for Reddit role?: string; // for LinkedIn @@ -75,7 +74,7 @@ export function SocialCard({
-

{body}

+

{body}

); diff --git a/apps/web/src/components/testimonial-card.tsx b/apps/web/src/components/testimonial-card.tsx deleted file mode 100644 index 585156fa2..000000000 --- a/apps/web/src/components/testimonial-card.tsx +++ /dev/null @@ -1,22 +0,0 @@ -export type TestimonialCardProps = { - quote: string; - author: string; - role: string; - company: string; -}; - -export function TestimonialCard({ quote, author, role, company }: TestimonialCardProps) { - return ( -
-
-

"{quote}"

-
-

{author}

-

- {role} at {company} -

-
-
-
- ); -} diff --git a/apps/web/src/components/video-modal.tsx b/apps/web/src/components/video-modal.tsx new file mode 100644 index 000000000..effdab3db --- /dev/null +++ b/apps/web/src/components/video-modal.tsx @@ -0,0 +1,89 @@ +import { cn } from "@hypr/utils"; + +import { Icon } from "@iconify-icon/react"; +import MuxPlayer, { type MuxPlayerRefAttributes } from "@mux/mux-player-react"; +import { useEffect, useRef } from "react"; + +interface VideoModalProps { + playbackId: string; + isOpen: boolean; + onClose: () => void; +} + +export function VideoModal({ playbackId, isOpen, onClose }: VideoModalProps) { + const playerRef = useRef(null); + const modalRef = useRef(null); + + useEffect(() => { + if (isOpen && playerRef.current) { + playerRef.current.play().catch(() => { + // Ignore autoplay errors + }); + } + }, [isOpen]); + + useEffect(() => { + const handleEscape = (e: KeyboardEvent) => { + if (e.key === "Escape" && isOpen) { + onClose(); + } + }; + + document.addEventListener("keydown", handleEscape); + return () => document.removeEventListener("keydown", handleEscape); + }, [isOpen, onClose]); + + useEffect(() => { + if (isOpen) { + document.body.style.overflow = "hidden"; + } else { + document.body.style.overflow = ""; + } + return () => { + document.body.style.overflow = ""; + }; + }, [isOpen]); + + if (!isOpen) { + return null; + } + + return ( +
+
e.stopPropagation()} + > + {/* Close button */} + + + {/* Video */} + +
+
+ ); +} diff --git a/apps/web/src/components/video-player.tsx b/apps/web/src/components/video-player.tsx new file mode 100644 index 000000000..768871a52 --- /dev/null +++ b/apps/web/src/components/video-player.tsx @@ -0,0 +1,104 @@ +import { cn } from "@hypr/utils"; + +import MuxPlayer, { type MuxPlayerRefAttributes } from "@mux/mux-player-react"; +import { useEffect, useRef, useState } from "react"; + +interface VideoPlayerProps { + playbackId: string; + className?: string; + onLearnMore?: () => void; + showButtons?: boolean; + onExpandVideo?: () => void; +} + +export function VideoPlayer({ + playbackId, + className, + onLearnMore, + showButtons = true, + onExpandVideo, +}: VideoPlayerProps) { + const [isHovered, setIsHovered] = useState(false); + const [showControls, setShowControls] = useState(false); + const playerRef = useRef(null); + + useEffect(() => { + if (playerRef.current) { + if (isHovered) { + playerRef.current.play().catch(() => {}); + setShowControls(true); + } else { + playerRef.current.pause(); + playerRef.current.currentTime = 0; + setShowControls(false); + } + } + }, [isHovered]); + + return ( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + + + {showButtons && showControls && ( +
+ + +
+ )} +
+ ); +} diff --git a/apps/web/src/components/video-thumbnail.tsx b/apps/web/src/components/video-thumbnail.tsx new file mode 100644 index 000000000..086fce60a --- /dev/null +++ b/apps/web/src/components/video-thumbnail.tsx @@ -0,0 +1,49 @@ +import { cn } from "@hypr/utils"; + +import { Icon } from "@iconify-icon/react"; +import MuxPlayer from "@mux/mux-player-react"; + +interface VideoThumbnailProps { + playbackId: string; + className?: string; + onPlay?: () => void; +} + +export function VideoThumbnail({ + playbackId, + className, + onPlay, +}: VideoThumbnailProps) { + return ( +
+ + +
+ +
+
+ ); +} diff --git a/apps/web/src/routeTree.gen.ts b/apps/web/src/routeTree.gen.ts index ef93f082e..f82d7e18a 100644 --- a/apps/web/src/routeTree.gen.ts +++ b/apps/web/src/routeTree.gen.ts @@ -9,13 +9,14 @@ // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. import { Route as rootRouteImport } from './routes/__root' +import { Route as CalRouteImport } from './routes/cal' import { Route as AuthRouteImport } from './routes/auth' import { Route as ViewRouteRouteImport } from './routes/_view/route' 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 ViewPricingRouteImport } from './routes/_view/pricing' -import { Route as ViewDownloadsRouteImport } from './routes/_view/downloads' +import { Route as ViewDownloadRouteImport } from './routes/_view/download' import { Route as ViewAppRouteRouteImport } from './routes/_view/app/route' import { Route as ViewChangelogIndexRouteImport } from './routes/_view/changelog/index' import { Route as ViewBlogIndexRouteImport } from './routes/_view/blog/index' @@ -28,6 +29,11 @@ import { Route as ViewBlogSlugRouteImport } from './routes/_view/blog/$slug' import { Route as ViewAppIntegrationRouteImport } from './routes/_view/app/integration' import { Route as ViewAppAccountRouteImport } from './routes/_view/app/account' +const CalRoute = CalRouteImport.update({ + id: '/cal', + path: '/cal', + getParentRoute: () => rootRouteImport, +} as any) const AuthRoute = AuthRouteImport.update({ id: '/auth', path: '/auth', @@ -57,9 +63,9 @@ const ViewPricingRoute = ViewPricingRouteImport.update({ path: '/pricing', getParentRoute: () => ViewRouteRoute, } as any) -const ViewDownloadsRoute = ViewDownloadsRouteImport.update({ - id: '/downloads', - path: '/downloads', +const ViewDownloadRoute = ViewDownloadRouteImport.update({ + id: '/download', + path: '/download', getParentRoute: () => ViewRouteRoute, } as any) const ViewAppRouteRoute = ViewAppRouteRouteImport.update({ @@ -120,8 +126,9 @@ const ViewAppAccountRoute = ViewAppAccountRouteImport.update({ export interface FileRoutesByFullPath { '/auth': typeof AuthRoute + '/cal': typeof CalRoute '/app': typeof ViewAppRouteRouteWithChildren - '/downloads': typeof ViewDownloadsRoute + '/download': typeof ViewDownloadRoute '/pricing': typeof ViewPricingRoute '/webhook/nango': typeof WebhookNangoRoute '/webhook/stripe': typeof WebhookStripeRoute @@ -139,7 +146,8 @@ export interface FileRoutesByFullPath { } export interface FileRoutesByTo { '/auth': typeof AuthRoute - '/downloads': typeof ViewDownloadsRoute + '/cal': typeof CalRoute + '/download': typeof ViewDownloadRoute '/pricing': typeof ViewPricingRoute '/webhook/nango': typeof WebhookNangoRoute '/webhook/stripe': typeof WebhookStripeRoute @@ -159,8 +167,9 @@ export interface FileRoutesById { __root__: typeof rootRouteImport '/_view': typeof ViewRouteRouteWithChildren '/auth': typeof AuthRoute + '/cal': typeof CalRoute '/_view/app': typeof ViewAppRouteRouteWithChildren - '/_view/downloads': typeof ViewDownloadsRoute + '/_view/download': typeof ViewDownloadRoute '/_view/pricing': typeof ViewPricingRoute '/webhook/nango': typeof WebhookNangoRoute '/webhook/stripe': typeof WebhookStripeRoute @@ -180,8 +189,9 @@ export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath fullPaths: | '/auth' + | '/cal' | '/app' - | '/downloads' + | '/download' | '/pricing' | '/webhook/nango' | '/webhook/stripe' @@ -199,7 +209,8 @@ export interface FileRouteTypes { fileRoutesByTo: FileRoutesByTo to: | '/auth' - | '/downloads' + | '/cal' + | '/download' | '/pricing' | '/webhook/nango' | '/webhook/stripe' @@ -218,8 +229,9 @@ export interface FileRouteTypes { | '__root__' | '/_view' | '/auth' + | '/cal' | '/_view/app' - | '/_view/downloads' + | '/_view/download' | '/_view/pricing' | '/webhook/nango' | '/webhook/stripe' @@ -239,6 +251,7 @@ export interface FileRouteTypes { export interface RootRouteChildren { ViewRouteRoute: typeof ViewRouteRouteWithChildren AuthRoute: typeof AuthRoute + CalRoute: typeof CalRoute WebhookNangoRoute: typeof WebhookNangoRoute WebhookStripeRoute: typeof WebhookStripeRoute ApiSyncReadRoute: typeof ApiSyncReadRoute @@ -247,6 +260,13 @@ export interface RootRouteChildren { declare module '@tanstack/react-router' { interface FileRoutesByPath { + '/cal': { + id: '/cal' + path: '/cal' + fullPath: '/cal' + preLoaderRoute: typeof CalRouteImport + parentRoute: typeof rootRouteImport + } '/auth': { id: '/auth' path: '/auth' @@ -289,11 +309,11 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ViewPricingRouteImport parentRoute: typeof ViewRouteRoute } - '/_view/downloads': { - id: '/_view/downloads' - path: '/downloads' - fullPath: '/downloads' - preLoaderRoute: typeof ViewDownloadsRouteImport + '/_view/download': { + id: '/_view/download' + path: '/download' + fullPath: '/download' + preLoaderRoute: typeof ViewDownloadRouteImport parentRoute: typeof ViewRouteRoute } '/_view/app': { @@ -394,7 +414,7 @@ const ViewAppRouteRouteWithChildren = ViewAppRouteRoute._addFileChildren( interface ViewRouteRouteChildren { ViewAppRouteRoute: typeof ViewAppRouteRouteWithChildren - ViewDownloadsRoute: typeof ViewDownloadsRoute + ViewDownloadRoute: typeof ViewDownloadRoute ViewPricingRoute: typeof ViewPricingRoute ViewIndexRoute: typeof ViewIndexRoute ViewBlogSlugRoute: typeof ViewBlogSlugRoute @@ -406,7 +426,7 @@ interface ViewRouteRouteChildren { const ViewRouteRouteChildren: ViewRouteRouteChildren = { ViewAppRouteRoute: ViewAppRouteRouteWithChildren, - ViewDownloadsRoute: ViewDownloadsRoute, + ViewDownloadRoute: ViewDownloadRoute, ViewPricingRoute: ViewPricingRoute, ViewIndexRoute: ViewIndexRoute, ViewBlogSlugRoute: ViewBlogSlugRoute, @@ -423,6 +443,7 @@ const ViewRouteRouteWithChildren = ViewRouteRoute._addFileChildren( const rootRouteChildren: RootRouteChildren = { ViewRouteRoute: ViewRouteRouteWithChildren, AuthRoute: AuthRoute, + CalRoute: CalRoute, WebhookNangoRoute: WebhookNangoRoute, WebhookStripeRoute: WebhookStripeRoute, ApiSyncReadRoute: ApiSyncReadRoute, diff --git a/apps/web/src/routes/_view/app/account.tsx b/apps/web/src/routes/_view/app/account.tsx index 654a12190..8366682fa 100644 --- a/apps/web/src/routes/_view/app/account.tsx +++ b/apps/web/src/routes/_view/app/account.tsx @@ -23,7 +23,6 @@ function Component() {
- {/* Profile Info Section */}

Profile info

@@ -156,7 +155,7 @@ function AccountSettingsCard() { }; return ( -
+

Account Settings

@@ -164,7 +163,7 @@ function AccountSettingsCard() {

-
+
Current plan: {getPlanDisplay()}
@@ -178,7 +177,7 @@ function IntegrationsSettingsCard() { const [connectedApps] = useState(0); // TODO: Get actual count from API return ( -
+

Integrations Settings

@@ -186,7 +185,7 @@ function IntegrationsSettingsCard() {

-
+
{connectedApps} {connectedApps === 1 ? "app is" : "apps are"} connected to Hyprnote
diff --git a/apps/web/src/routes/_view/download.tsx b/apps/web/src/routes/_view/download.tsx new file mode 100644 index 000000000..00c8116e2 --- /dev/null +++ b/apps/web/src/routes/_view/download.tsx @@ -0,0 +1,135 @@ +import { cn } from "@hypr/utils"; + +import { Icon } from "@iconify-icon/react"; +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/_view/download")({ + component: Component, +}); + +function Component() { + return ( +
+
+ + + Mac (Apple Silicon) features on-device speech-to-text. Other platforms coming soon without on-device + processing. + +
+ +
+
+
+

+ Download Hyprnote +

+

+ Choose your platform to get started with Hyprnote +

+
+ +
+

+ Desktop +

+
+ + + + +
+
+ +
+

+ Mobile +

+
+ + +
+
+
+
+
+ ); +} + +function DownloadCard({ + iconName, + spec, + downloadUrl, + available, +}: { + iconName: string; + spec: string; + downloadUrl: string; + available: boolean; +}) { + return ( +
+ +

{spec}

+ + {available + ? ( + + Download + + + ) + : ( + + )} +
+ ); +} diff --git a/apps/web/src/routes/_view/downloads.tsx b/apps/web/src/routes/_view/downloads.tsx deleted file mode 100644 index aa969c655..000000000 --- a/apps/web/src/routes/_view/downloads.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { createFileRoute } from "@tanstack/react-router"; - -export const Route = createFileRoute("/_view/downloads")({ - component: Component, -}); - -function Component() { - return ( -
-
-
-
-

- Download Hyprnote -

-

- Choose your platform to get started with Hyprnote -

-
- -
- - - -
-
-
-
- ); -} - -function DownloadCard({ - platform, - icon, - description, - downloadUrl, -}: { - platform: string; - icon: string; - description: string; - downloadUrl: string; -}) { - return ( - -
- {icon} -
-

{platform}

-

{description}

-
-
- - - -
- ); -} diff --git a/apps/web/src/routes/_view/index.tsx b/apps/web/src/routes/_view/index.tsx index de7c3a436..c7702aa0d 100644 --- a/apps/web/src/routes/_view/index.tsx +++ b/apps/web/src/routes/_view/index.tsx @@ -1,115 +1,283 @@ +import { cn } from "@hypr/utils"; + import { Icon } from "@iconify-icon/react"; import { createFileRoute } from "@tanstack/react-router"; +import { useRef, useState } from "react"; import { DownloadButton } from "@/components/download-button"; +import { GitHubOpenSource } from "@/components/github-open-source"; import { GithubStars } from "@/components/github-stars"; +import { JoinWaitlistButton } from "@/components/join-waitlist-button"; import { LogoCloud } from "@/components/logo-cloud"; import { SocialCard } from "@/components/social-card"; +import { VideoModal } from "@/components/video-modal"; +import { VideoPlayer } from "@/components/video-player"; +import { VideoThumbnail } from "@/components/video-thumbnail"; + +const MUX_PLAYBACK_ID = "SGv6JaZsKqF50102xk6no2ybUqqSyngeWO401ic8qJdZR4"; + +const mainFeatures = [ + { + icon: "mdi:text-box-outline", + title: "Transcript", + description: "Realtime transcript and speaker identification", + videoType: "player" as const, + }, + { + icon: "mdi:file-document-outline", + title: "Summary", + description: "Create customized summaries with templates for various formats", + videoType: "player" as const, + }, + { + icon: "mdi:chat-outline", + title: "Chat", + description: "Get context-aware answers in realtime, even from past meetings", + videoType: "player" as const, + }, + { + icon: "mdi:window-restore", + title: "Floating Panel", + description: "Compact notepad with transcript, summary, and chat during meetings", + videoType: "player" as const, + }, + { + icon: "mdi:calendar-check-outline", + title: "Daily Note", + description: "Track todos and navigate emails and events throughout the day", + videoType: "image" as const, + comingSoon: true, + }, +]; + +const detailsFeatures = [ + { + icon: "mdi:text-box-edit-outline", + title: "Notion-like Editor", + description: "Full markdown support with distraction-free writing", + comingSoon: false, + }, + { + icon: "mdi:upload-outline", + title: "Upload Audio", + description: "Import audio files or transcripts to convert into notes", + comingSoon: false, + }, + { + icon: "mdi:account-multiple-outline", + title: "Contacts", + description: "Organize and manage your contacts with ease", + comingSoon: false, + }, + { + icon: "mdi:calendar-outline", + title: "Calendar", + description: "Stay on top of your schedule with integrated calendar", + comingSoon: false, + }, + { + icon: "mdi:bookshelf", + title: "Noteshelf", + description: "Browse and organize all your notes in one place", + comingSoon: true, + }, +]; export const Route = createFileRoute("/_view/")({ component: Component, }); function Component() { + const [expandedVideo, setExpandedVideo] = useState(null); + const [selectedDetail, setSelectedDetail] = useState(0); + const [selectedFeature, setSelectedFeature] = useState(0); + const detailsScrollRef = useRef(null); + const featuresScrollRef = useRef(null); + + const scrollToDetail = (index: number) => { + setSelectedDetail(index); + if (detailsScrollRef.current) { + const container = detailsScrollRef.current; + const scrollLeft = container.offsetWidth * index; + container.scrollTo({ left: scrollLeft, behavior: "smooth" }); + } + }; + + const scrollToFeature = (index: number) => { + setSelectedFeature(index); + if (featuresScrollRef.current) { + const container = featuresScrollRef.current; + const scrollLeft = container.offsetWidth * index; + container.scrollTo({ left: scrollLeft, behavior: "smooth" }); + } + }; + return ( -
-
-
-
-
-
-
-

- The AI notepad for
private meetings -

-

- Hyprnote listens and summarizes your meetings{" "} -
without sending any voice to remote servers -

-
+
+
+ + + + + + + + + +
+ setExpandedVideo(null)} + /> +
+ ); +} - {/* CTAs */} -
- -

- Free and{" "} - - open source - -

-
-
- - {/* Video - Mobile First */} -
-
-
- -

Demo video coming soon

-
-
-
+function YCombinatorBanner() { + return ( + +
+ Backed by + Y Combinator + Y Combinator +
+
+ ); +} - {/* Feature Cards Row */} -
-
-
-

Private

-

- Your notes stay local by default. Sync to a cloud only when you choose. -

-
-
-

Effortless

-

- A simple notepad that just works—fast, minimal, and distraction-free. -

-
-
-

Flexible

-

- Use any STT or LLM. Local or cloud. No lock-ins, no forced stack. -

-
-
+function HeroSection({ onVideoExpand }: { onVideoExpand: (id: string) => void }) { + return ( +
+
+
+
+

+ The AI notepad for
private meetings +

+

+ Hyprnote listens and summarizes your meetings{" "} +
without sending any voice to remote servers +

+
- {/* Video - Desktop (no gap) */} -
-
-
- -

Demo video coming soon

-
-
-
-
-
+
+ +

+ Free and{" "} + + open source + +

+
- {/* Social Proof Section */} -
-
-

- Loved by professionals at -

+
+ onVideoExpand(MUX_PLAYBACK_ID)} + /> +
- +
+ +
+ onVideoExpand(MUX_PLAYBACK_ID)} + /> +
+
+
+
+ ); +} - {/* Testimonials - New Grid Layout */} -
- {/* Mobile: Horizontal scrollable layout */} -
-
-
- +
+

Private

+

+ Your notes stay local by default. Sync to a cloud only when you choose. +

+
+
+

Effortless

+

+ A simple notepad that just works—fast, minimal, and distraction-free. +

+
+
+

Flexible

+

+ Use any STT or LLM. Local or cloud. No lock-ins, no forced stack. +

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

+ Loved by professionals at +

+ + + +
+ + +
+
+
+ ); +} + +function TestimonialsMobileGrid() { + return ( +
+ -
+ url="https://www.reddit.com/r/macapps/comments/1lo24b9/comment/n15dr0t/" + className="border-x-0" + /> -
- -
- -
- -
- -
- -
-
-
+ url="https://www.linkedin.com/posts/flaviews_guys-at-hyprnote-yc-s25-are-wild-had-activity-7360606765530386434-Klj-" + className="border-x-0" + /> + + + + +
+ ); +} - {/* Desktop: Custom grid layout with big and small cards */} -
- {/* Left column - Big card (row-span-2) */} -
-
+ +
+ +
+ +
+ +
+
+ ); +} - {/* Right column - Small card (top) */} -
- +function FeaturesIntroSection() { + return ( +
+
+
+ Hyprnote +
+

Hyprnote works like charm

+

+ {"Super simple and easy to use with its clean interface. And it's getting better with every update — every single week."} +

+
+
+ ); +} + +function MainFeaturesSection({ + featuresScrollRef, + selectedFeature, + setSelectedFeature, + scrollToFeature, + onVideoExpand, +}: { + featuresScrollRef: React.RefObject; + selectedFeature: number; + setSelectedFeature: (index: number) => void; + scrollToFeature: (index: number) => void; + onVideoExpand: (id: string) => void; +}) { + return ( + <> + + + + ); +} + +function FeaturesMobileCarousel({ + featuresScrollRef, + selectedFeature, + setSelectedFeature, + scrollToFeature, + onVideoExpand, +}: { + featuresScrollRef: React.RefObject; + selectedFeature: number; + setSelectedFeature: (index: number) => void; + scrollToFeature: (index: number) => void; + onVideoExpand: (id: string) => void; +}) { + return ( +
+
{ + const container = e.currentTarget; + const scrollLeft = container.scrollLeft; + const itemWidth = container.offsetWidth; + const index = Math.round(scrollLeft / itemWidth); + setSelectedFeature(index); + }} + > +
+ {mainFeatures.map((feature, index) => ( +
+
+
+ {feature.videoType === "player" + ? ( + window.location.href = "#"} + onExpandVideo={() => onVideoExpand(MUX_PLAYBACK_ID)} + /> + ) + : ( + {`${feature.title} + )} +
+
+
+

{feature.title}

+ {feature.comingSoon && ( + + Coming Soon + + )}
+

{feature.description}

+
+
+
+ ))} +
+
+ +
+ {mainFeatures.map((_, index) => ( +
+
+ ); +} + +function FeaturesDesktopGrid({ onVideoExpand }: { onVideoExpand: (id: string) => void }) { + return ( +
+
+
+ window.location.href = "#"} + onExpandVideo={() => onVideoExpand(MUX_PLAYBACK_ID)} + /> +
+
+
+ +

Transcript

+
+

+ Realtime transcript and speaker identification +

+
+
+ +
+
+ window.location.href = "#"} + onExpandVideo={() => onVideoExpand(MUX_PLAYBACK_ID)} + /> +
+
+
+ +

Summary

+
+

+ Create customized summaries with templates for various formats +

+
+
+ +
+
+ window.location.href = "#"} + onExpandVideo={() => onVideoExpand(MUX_PLAYBACK_ID)} + /> +
+
+
+ +

Chat

+
+

+ Get context-aware answers in realtime, even from past meetings +

+
+
+ +
+
+ window.location.href = "#"} + onExpandVideo={() => onVideoExpand(MUX_PLAYBACK_ID)} + /> +
+
+
+ +

Floating Panel

+
+

+ Compact notepad with transcript, summary, and chat during meetings +

+
+
+ +
+
+ Daily Note feature +
+
+
+ +

Daily Note

+ + Coming Soon + +
+

+ Track todos and navigate emails and events throughout the day +

+
+
+
+ ); +} + +function DetailsSection({ + detailsScrollRef, + selectedDetail, + setSelectedDetail, + scrollToDetail, + onVideoExpand, +}: { + detailsScrollRef: React.RefObject; + selectedDetail: number; + setSelectedDetail: (index: number) => void; + scrollToDetail: (index: number) => void; + onVideoExpand: (id: string) => void; +}) { + return ( +
+ + + + +
+ ); +} - {/* Right column - Small card (bottom) */} -
- +function DetailsSectionHeader() { + return ( +
+

We focus on every bit of details

+

+ From powerful editing to seamless organization, every feature is crafted with care +

+
+ ); +} + +function DetailsMobileCarousel({ + detailsScrollRef, + selectedDetail, + setSelectedDetail, + scrollToDetail, + onVideoExpand, +}: { + detailsScrollRef: React.RefObject; + selectedDetail: number; + setSelectedDetail: (index: number) => void; + scrollToDetail: (index: number) => void; + onVideoExpand: (id: string) => void; +}) { + return ( +
+
{ + const container = e.currentTarget; + const scrollLeft = container.scrollLeft; + const itemWidth = container.offsetWidth; + const index = Math.round(scrollLeft / itemWidth); + setSelectedDetail(index); + }} + > +
+ {detailsFeatures.map((feature, index) => ( +
+
+
+ window.location.href = "#"} + onExpandVideo={() => onVideoExpand(MUX_PLAYBACK_ID)} + /> +
+
+
+

{feature.title}

+ {feature.comingSoon && ( + + Coming Soon + + )}
+

{feature.description}

- - -
-
-

- Built for developers -

-
- {[ - "AI-powered search and organization", - "Local-first, fast performance", - "Clean, distraction-free UI", - "Open source & privacy-focused", - ].map((item, index) => ( -
- - - -

{item}

+ ))} +
+
+ +
+ {detailsFeatures.map((_, index) => ( +
+
+ ); +} + +function DetailsTabletView({ + selectedDetail, + setSelectedDetail, + onVideoExpand, +}: { + selectedDetail: number; + setSelectedDetail: (index: number) => void; + onVideoExpand: (id: string) => void; +}) { + return ( +
+
+
+
+ {detailsFeatures.map((feature, index) => ( + + ))} +
+
+ +
+
+ window.location.href = "#"} + onExpandVideo={() => onVideoExpand(MUX_PLAYBACK_ID)} + /> +
+
+
+
+ ); +} + +function DetailsDesktopView({ onVideoExpand }: { onVideoExpand: (id: string) => void }) { + return ( +
+
+ {detailsFeatures.map((feature, index) => ( +
+
+ +
+
+

{feature.title}

+ {feature.comingSoon && ( + + Coming Soon + + )} +
+

{feature.description}

-
+
+ ))} +
+ +
+ window.location.href = "#"} + onExpandVideo={() => onVideoExpand(MUX_PLAYBACK_ID)} + /> +
+
+ ); +} + +function ManifestoSection() { + return ( +
+
+
+
+

Our manifesto

+ +
+

+ We believe in the power of notetaking, not notetakers. Meetings should be moments of presence, not + passive attendance. If you are not adding value, your time is better spent elsewhere for you and your + team. +

+

+ Hyprnote exists to preserve what makes us human: conversations that spark ideas, collaborations that + move work forward. We build tools that amplify human agency, not replace it. No ghost bots. No silent + note lurkers. Just people, thinking together. +

+

We stand with those who value real connection and purposeful collaboration.

+
-
-
-
+
+ John Jeong + Yujong Lee +
+ +
+
+

Hyprnote

+

John Jeong, Yujong Lee

+
+ +
Hyprnote
-

- Where conversations stay yours -

-

- Start using Hyprnote today and bring clarity to your back-to-back meetings -

-
- - -
-
+
- -
+
+ + ); +} + +function CTASection() { + return ( +
+
+
+ Hyprnote +
+

+ Where conversations stay yours +

+

+ Start using Hyprnote today and bring clarity to your back-to-back meetings +

+
+ + +
+
+
); } diff --git a/apps/web/src/routes/_view/pricing.tsx b/apps/web/src/routes/_view/pricing.tsx index 0b5e6d8bc..bfa978194 100644 --- a/apps/web/src/routes/_view/pricing.tsx +++ b/apps/web/src/routes/_view/pricing.tsx @@ -1,9 +1,332 @@ +import { cn } from "@hypr/utils"; + +import { Icon } from "@iconify-icon/react"; import { createFileRoute } from "@tanstack/react-router"; export const Route = createFileRoute("/_view/pricing")({ component: Component, }); +interface PricingPlan { + name: string; + price: { monthly: number; yearly: number } | null; + description: string; + popular?: boolean; + features: Array<{ + label: string; + included: boolean | "partial"; + tooltip?: string; + comingSoon?: boolean; + }>; +} + +const pricingPlans: PricingPlan[] = [ + { + name: "Free", + price: null, + description: "Fully functional with your own API keys. Perfect for individuals who want complete control.", + features: [ + { label: "Local Transcription", included: true }, + { label: "Speaker Identification", included: true }, + { label: "Bring Your Own Key (STT & LLM)", included: true }, + { label: "Basic Sharing (Copy, PDF)", included: true }, + { label: "All Data Local", included: true }, + { label: "Integrations", included: "partial", tooltip: "Available with free account signup" }, + { label: "Templates & Chat", included: false }, + { label: "Cloud Services (STT & LLM)", included: false }, + { label: "Cloud Sync", included: false }, + { label: "Shareable Links", included: false }, + { label: "Unified Billing & Access Management", included: false }, + ], + }, + { + name: "Pro", + price: { + monthly: 35, + yearly: 295, + }, + description: "No API keys needed. Get cloud services, advanced sharing, and team features out of the box.", + popular: true, + features: [ + { label: "Everything in Free", included: true }, + { label: "Integrations", included: true }, + { label: "Templates & Chat", included: true }, + { label: "Cloud Services (STT & LLM)", included: true }, + { label: "Cloud Sync", included: true, tooltip: "Select which notes to sync", comingSoon: true }, + { + label: "Shareable Links", + included: true, + tooltip: "DocSend-like: view tracking, expiration, revocation", + comingSoon: true, + }, + { label: "Unified Billing & Access Management", included: true, comingSoon: true }, + ], + }, +]; + +const infoCards = [ + { + icon: "mdi:account-check-outline", + title: "Speaker identification is included in all plans", + }, + { + icon: "mdi:link-variant", + title: "Shareable link includes DocsSend-like controls", + description: "viewer tracking, expiration, revocation", + }, + { + icon: "mdi:key-outline", + title: "BYOK lets you connect your own LLM", + description: "for full data control", + }, +]; + function Component() { - return
Hello "/_view/_layout/pricing"!
; + return ( +
+
+ + + + + +
+
+ ); +} + +function HeroSection() { + return ( +
+
+

+ Hyprnote Pricing +

+

+ Choose the plan that fits your needs. Start for free, upgrade when you need cloud features. +

+
+
+ ); +} + +function PricingCardsSection() { + return ( +
+
+ {pricingPlans.map((plan) => )} +
+
+ ); +} + +function PricingCard({ plan }: { plan: PricingPlan }) { + return ( +
+ {plan.popular && ( +
+ Most Popular +
+ )} + +
+
+

{plan.name}

+

{plan.description}

+ + {plan.price + ? ( +
+
+ + ${plan.price.monthly} + + /seat/month +
+
+ or ${plan.price.yearly}/seat/year (save 30%) +
+
+ ) + :
Free
} +
+ +
+ {plan.features.map((feature, idx) => ( +
+ +
+
+ + {feature.label} + + {feature.comingSoon && ( + + Coming Soon + + )} +
+ {feature.tooltip && ( +
+ {feature.tooltip} +
+ )} +
+
+ ))} +
+ + +
+
+ ); +} + +function InfoCardsSection() { + return ( +
+
+
+ {infoCards.map((card, idx) => ( +
+ +

+ {card.title} +

+ {card.description &&

{card.description}

} +
+ ))} +
+
+
+ ); +} + +function FAQSection() { + const faqs = [ + { + question: "What does 'Local only' transcription mean?", + answer: + "All transcription happens on your device. Your audio never leaves your computer, ensuring complete privacy.", + }, + { + question: "What is BYOK (Bring Your Own Key)?", + answer: + "BYOK allows you to connect your own LLM provider (like OpenAI, Anthropic, or self-hosted models) for AI features while maintaining full control over your data.", + }, + { + question: "What's included in shareable links?", + answer: + "Pro users get DocsSend-like controls: track who views your notes, set expiration dates, and revoke access anytime.", + }, + { + question: "Is there a team discount?", + answer: "Yes! Teams on annual plans save 30%. Contact us for larger team pricing.", + }, + ]; + + return ( +
+
+

+ Frequently Asked Questions +

+
+ {faqs.map((faq, idx) => ( +
+

+ {faq.question} +

+

{faq.answer}

+
+ ))} +
+
+
+ ); +} + +function CTASection() { + return ( +
+
+
+ Hyprnote +
+

+ Ready to get started? +

+

+ Download Hyprnote for free and upgrade when you need cloud features +

+ +
+
+ ); } diff --git a/apps/web/src/routes/_view/route.tsx b/apps/web/src/routes/_view/route.tsx index ea03f2a7c..c92914144 100644 --- a/apps/web/src/routes/_view/route.tsx +++ b/apps/web/src/routes/_view/route.tsx @@ -7,7 +7,7 @@ export const Route = createFileRoute("/_view")({ function Component() { return ( -
+
@@ -20,43 +20,44 @@ function Component() { function Header() { return (
-
+
- Hyprnote + Hyprnote Docs Blog Pricing
@@ -65,37 +66,12 @@ function Header() { ); } -function HeaderUser() { - const { user } = Route.useLoaderData(); - - if (user) { - return ( - - Account - - ); - } - - return ( - - Get Started - - ); -} - function Footer() { const currentYear = new Date().getFullYear(); return (