diff --git a/apps/web/src/routes/_view/about.tsx b/apps/web/src/routes/_view/about.tsx index 6c9f37cd86..315be5eb15 100644 --- a/apps/web/src/routes/_view/about.tsx +++ b/apps/web/src/routes/_view/about.tsx @@ -557,180 +557,73 @@ function StoryDetail({ onClose }: { onClose: () => void }) {
-

- Making notetaking effortless +

+ How We Landed on Hyprnote

-

- We believe that capturing and organizing your conversations - shouldn't be a chore. That's why we built Hyprnote - a tool that - listens, learns, and helps you remember what matters. +

+ Our story and what we believe

-

- Our Mission -

-
-
- -

- Privacy First -

-

- Your conversations are personal. We process everything locally - on your device using on-device AI, so your data never leaves - your computer. -

-
-
- -

- Effortless Capture -

-

- Stop worrying about missing important details. Hyprnote captures - both your mic and system audio, giving you complete context for - every conversation. -

-
-
- -

- Intelligent Organization -

-

- AI helps you find what matters. Automatic transcription, smart - summaries, and searchable notes mean you'll never lose track of - important information. -

-
-
- -

- Built for Everyone -

-

- From remote workers to students, from entrepreneurs to - executives - Hyprnote adapts to your workflow and helps you work - smarter. -

-
-
+

+ Hyprnote didn't start as a note-app. We were actually building an AI + hardware toy for kids. It was fun, but for two people, hardware was + too slow and too heavy. When we stepped back, we realized the thing + we cared about wasn't the toy — it was helping people capture and + understand conversations. +

+ +

+ At the same time, I was drowning in meetings and trying every AI + notetaker out there. They were slow, distracting, or shipped every + word to the cloud. None of them felt like something I'd trust or + enjoy using. That became the real beginning of Hyprnote. +

+ +

+ We built the first version quickly. And it showed. Too many + features, too many ideas, no clear philosophy. Even after YC, we + kept moving without asking the hard questions. The product worked, + but it didn't feel right. So we made the hard call: stop patching, + start over. Burn it down and rebuild from scratch with a simple, + focused point of view. +

- Our Story + Our manifesto

- Hyprnote was born from a simple frustration: trying to take notes - while staying engaged in important conversations. Whether it was a - crucial client call, a brainstorming session with the team, or an - online lecture, we found ourselves constantly torn between listening - and writing. + We believe in the power of notetaking, not notetakers. Meetings + should be moments of presence. If you're not adding value, your time + is better spent elsewhere — for you and for your team.

+

- We looked for solutions, but everything required bots joining - meetings, cloud uploads, or compromising on privacy. We knew there - had to be a better way. + Hyprnote exists to preserve what makes us human: conversations that + spark ideas and collaboration that moves work forward. We build + tools that amplify human agency, not replace it. No ghost bots. No + silent note lurkers. Just people, thinking together.

-

- That's when we started building Hyprnote - a desktop application - that captures audio locally, processes it with on-device AI, and - gives you the freedom to be fully present in your conversations - while never missing a detail. + +

+ We stand with those who value real connection and purposeful work.

- What We Stand For + Where we are now

-
-
- -
-

- Privacy is non-negotiable -

-

- We will never compromise on privacy. Your data belongs to you, - period. -

-
-
-
- -
-

- Transparency in everything -

-

- We're open about how Hyprnote works, from our tech stack to - our pricing model. -

-
-
-
- -
-

- Community-driven development -

-

- We build features our users actually need, guided by your - feedback and requests. -

-
-
-
- -
-

- Continuous improvement -

-

- We ship updates regularly and are always working to make - Hyprnote better. -

-
-
-
+

+ Hyprnote today is the result of that reset. A fast, private, + local-first notetaker built for people like us: meeting-heavy, + privacy-conscious, and tired of complicated tools. It stays on your + device. It respects your data. And it helps you think better, not + attend meetings on autopilot. +

-
-

- Built by Fastrepl -

-

- Hyprnote is developed by Fastrepl, a team dedicated to building - productivity tools that respect your privacy and enhance your - workflow. -

-
- - - View on GitHub - -
-
+

+ This is how we got here: a messy start, a full rewrite, and a clear + belief that great work comes from humans — not from machines + pretending to be in the room. +

diff --git a/apps/web/src/routes/_view/changelog/$slug.tsx b/apps/web/src/routes/_view/changelog/$slug.tsx index 8d5b1a1ac1..2c3df9e10e 100644 --- a/apps/web/src/routes/_view/changelog/$slug.tsx +++ b/apps/web/src/routes/_view/changelog/$slug.tsx @@ -1,9 +1,29 @@ import { MDXContent } from "@content-collections/mdx/react"; import { Icon } from "@iconify-icon/react"; import { createFileRoute, Link, notFound } from "@tanstack/react-router"; -import { useCallback } from "react"; +import { useState } from "react"; +import semver from "semver"; -import { getChangelogBySlug } from "@/changelog"; +import { + ResizableHandle, + ResizablePanel, + ResizablePanelGroup, +} from "@hypr/ui/components/ui/resizable"; +import { cn } from "@hypr/utils"; + +import { + type ChangelogWithMeta, + getChangelogBySlug, + getChangelogList, +} from "@/changelog"; +import { MockWindow } from "@/components/mock-window"; + +const ITEMS_PER_PAGE = 20; + +type VersionGroup = { + baseVersion: string; + versions: ChangelogWithMeta[]; +}; export const Route = createFileRoute("/_view/changelog/$slug")({ component: Component, @@ -13,6 +33,8 @@ export const Route = createFileRoute("/_view/changelog/$slug")({ throw notFound(); } + const allChangelogs = getChangelogList(); + const nextChangelog = changelog.newerSlug ? (getChangelogBySlug(changelog.newerSlug) ?? null) : null; @@ -26,130 +48,311 @@ export const Route = createFileRoute("/_view/changelog/$slug")({ ? `https://github.com/fastrepl/hyprnote/compare/desktop_v${beforeVersion}...desktop_v${changelog.version}` : null; - return { changelog, nextChangelog, prevChangelog, diffUrl }; + return { + changelog, + allChangelogs, + nextChangelog, + prevChangelog, + diffUrl, + }; }, }); function Component() { - const { changelog, nextChangelog, prevChangelog, diffUrl } = - Route.useLoaderData(); + const { changelog, allChangelogs } = Route.useLoaderData(); - const isLatest = nextChangelog === null; + return ( +
+
+ + +
+
+ ); +} - const handleDownload = useCallback(() => { - const link = changelog.downloads["dmg-aarch64"]; - if (link) { - window.open(link, "_blank"); - } - }, [changelog]); +function HeroSection({ changelog }: { changelog: ChangelogWithMeta }) { + return ( +
+
+

+ Version {changelog.version} +

+
+
+ ); +} +function ChangelogContentSection({ + changelog, + allChangelogs, +}: { + changelog: ChangelogWithMeta; + allChangelogs: ChangelogWithMeta[]; +}) { return ( -
-
- +
+ - - Back to changelog - - -
-
-
- +
+ +
+ + +
+ + ); +} + +function ChangelogSplitView({ + changelog, + allChangelogs, +}: { + changelog: ChangelogWithMeta; + allChangelogs: ChangelogWithMeta[]; +}) { + return ( + + + + + + + + + + ); +} + +function groupVersions(changelogs: ChangelogWithMeta[]): VersionGroup[] { + const groups = new Map(); + + for (const changelog of changelogs) { + const version = semver.parse(changelog.version); + if (version) { + const baseVersion = `${version.major}.${version.minor}.${version.patch}`; + if (!groups.has(baseVersion)) { + groups.set(baseVersion, []); + } + groups.get(baseVersion)!.push(changelog); + } + } + + return Array.from(groups.entries()) + .map(([baseVersion, versions]) => ({ + baseVersion, + versions, + })) + .sort((a, b) => semver.rcompare(a.baseVersion, b.baseVersion)); +} + +function ChangelogSidebar({ + changelog, + allChangelogs, +}: { + changelog: ChangelogWithMeta; + allChangelogs: ChangelogWithMeta[]; +}) { + const [currentPage, setCurrentPage] = useState(0); + + const versionGroups = groupVersions(allChangelogs); + const totalPages = Math.ceil(versionGroups.length / ITEMS_PER_PAGE); + const startIndex = currentPage * ITEMS_PER_PAGE; + const endIndex = startIndex + ITEMS_PER_PAGE; + const paginatedGroups = versionGroups.slice(startIndex, endIndex); + + const goToNextPage = () => { + if (currentPage < totalPages - 1) { + setCurrentPage(currentPage + 1); + } + }; + + const goToPrevPage = () => { + if (currentPage > 0) { + setCurrentPage(currentPage - 1); + } + }; + + return ( +
+
+
+ {paginatedGroups.map((group) => ( +
+
+ Version {group.baseVersion} +
+
+ {group.versions.map((version) => { + const v = semver.parse(version.version); + const isPrerelease = v && v.prerelease.length > 0; + const iconUrl = isPrerelease + ? "https://ijoptyyjrfqwaqhyxkxj.supabase.co/storage/v1/object/public/public_images/icons/nightly-icon.png" + : "https://ijoptyyjrfqwaqhyxkxj.supabase.co/storage/v1/object/public/public_images/icons/stable-icon.png"; + + return ( + +
+ {`Version +
+
+

+ v{version.version} +

+

+ {isPrerelease ? "Nightly" : "Stable"} +

+
+ + ); + })} +
- {isLatest && ( - - - Latest Release - - )} + ))} +
+
+ + {/* Pagination Controls */} + {totalPages > 1 && ( +
+
+ + + + Page {currentPage + 1} of {totalPages} + + +
+
+ )} +
+ ); +} + +function ChangelogContent({ changelog }: { changelog: ChangelogWithMeta }) { + const { diffUrl } = Route.useLoaderData(); + const currentVersion = semver.parse(changelog.version); + const isPrerelease = currentVersion && currentVersion.prerelease.length > 0; + const isLatest = changelog.newerSlug === null; + + // Parse prerelease info + let prereleaseType = ""; + let buildNumber = ""; + if (isPrerelease && currentVersion && currentVersion.prerelease.length > 0) { + prereleaseType = currentVersion.prerelease[0]?.toString() || ""; + buildNumber = currentVersion.prerelease[1]?.toString() || ""; + } -

- v{changelog.version} -

+ const baseVersion = currentVersion + ? `v${currentVersion.major}.${currentVersion.minor}.${currentVersion.patch}` + : `v${changelog.version}`; - + return ( +
+
+
+

{baseVersion}

+ {isLatest && ( + + + Latest + + )} + {prereleaseType && ( + + {prereleaseType} + + )} + {buildNumber && ( + + #{buildNumber} + + )} +
+
+
+
-
+
+
+
+
+ ); +} - - + return ( +
+ + Viewing v{changelog.version} • {allChangelogs.length} total versions +
); } diff --git a/apps/web/src/routes/_view/changelog/index.tsx b/apps/web/src/routes/_view/changelog/index.tsx index d7fa60f343..b6ca06f55e 100644 --- a/apps/web/src/routes/_view/changelog/index.tsx +++ b/apps/web/src/routes/_view/changelog/index.tsx @@ -1,117 +1,188 @@ import { Icon } from "@iconify-icon/react"; import { createFileRoute, Link } from "@tanstack/react-router"; +import semver from "semver"; import { type ChangelogWithMeta, getChangelogList } from "@/changelog"; +import { MockWindow } from "@/components/mock-window"; export const Route = createFileRoute("/_view/changelog/")({ component: Component, loader: async () => { const changelogs = getChangelogList(); - return { changelogs }; }, }); +type SemanticVersionGroup = { + baseVersion: string; + versions: ChangelogWithMeta[]; +}; + +function groupBySemanticVersion( + changelogs: ChangelogWithMeta[], +): SemanticVersionGroup[] { + const groups = new Map(); + + for (const changelog of changelogs) { + const version = semver.parse(changelog.version); + if (version) { + const baseVersion = `${version.major}.${version.minor}.${version.patch}`; + if (!groups.has(baseVersion)) { + groups.set(baseVersion, []); + } + groups.get(baseVersion)!.push(changelog); + } + } + + return Array.from(groups.entries()) + .map(([baseVersion, versions]) => ({ + baseVersion, + versions, + })) + .sort((a, b) => semver.rcompare(a.baseVersion, b.baseVersion)); +} + function Component() { const { changelogs } = Route.useLoaderData(); + const groups = groupBySemanticVersion(changelogs); return ( -
-
-
-
- -
-

- Changelog -

-

- Track every update, improvement, and fix to Hyprnote -

-
- - {changelogs.length === 0 ? ( -
-
- -
-

No releases yet. Stay tuned!

-
- ) : ( -
-
- -
- {changelogs.map((changelog, index) => ( - - ))} -
+
+
+ + +
+
+ ); +} + +function HeroSection() { + return ( +
+
+

+ Changelog +

+

+ Track every update, improvement, and fix to Hyprnote +

+
+
+ ); +} + +function ChangelogContentSection({ + groups, +}: { + groups: SemanticVersionGroup[]; +}) { + return ( +
+
+ +
+
- )} + +
+
+
+ ); +} + +function ChangelogGridView({ groups }: { groups: SemanticVersionGroup[] }) { + if (groups.length === 0) { + return ( +
+
+ +
+

No releases yet. Stay tuned!

+ ); + } + + return ( +
+ {groups.map((group, index) => ( + + ))}
); } -function ChangelogCard({ - changelog, +function VersionGroup({ + group, isFirst, }: { - changelog: ChangelogWithMeta; + group: SemanticVersionGroup; isFirst: boolean; }) { + return ( +
+
+ Version {group.baseVersion} +
+
+ {group.versions.map((changelog) => ( + + ))} +
+
+ ); +} + +function VersionIcon({ changelog }: { changelog: ChangelogWithMeta }) { + const version = semver.parse(changelog.version); + const isPrerelease = version && version.prerelease.length > 0; + const iconUrl = isPrerelease + ? "https://ijoptyyjrfqwaqhyxkxj.supabase.co/storage/v1/object/public/public_images/icons/nightly-icon.png" + : "https://ijoptyyjrfqwaqhyxkxj.supabase.co/storage/v1/object/public/public_images/icons/stable-icon.png"; + return ( -
-
-
- -
-
+
+ {`Version +
+
+ v{changelog.version} +
+ + ); +} -
-
-
-
-

- v{changelog.version} -

- {isFirst && ( - - - Latest - - )} -
-
- - -
+function ChangelogStatusBar({ groups }: { groups: SemanticVersionGroup[] }) { + const totalVersions = groups.reduce( + (sum, group) => sum + group.versions.length, + 0, + ); -
- - View release notes → - -
-
-
- + return ( +
+ + {totalVersions} {totalVersions === 1 ? "version" : "versions"},{" "} + {groups.length} {groups.length === 1 ? "group" : "groups"} + +
); }