Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
},
"dependencies": {
"@hypr/utils": "workspace:*",
"@hypr/ui": "workspace:*",
"@cloudflare/vite-plugin": "^1.13.15",
"@iconify-icon/react": "^3.0.1",
"@nangohq/frontend": "^0.69.5",
Expand Down
21 changes: 21 additions & 0 deletions apps/web/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Route as ViewIndexRouteImport } from './routes/_view/index'
import { Route as WebhookNangoRouteImport } from './routes/webhook/nango'
import { Route as CallbackAuthRouteImport } from './routes/callback/auth'
import { Route as ViewPricingRouteImport } from './routes/_view/pricing'
import { Route as ViewDownloadsRouteImport } from './routes/_view/downloads'
import { Route as ViewAppRouteRouteImport } from './routes/_view/app/route'
import { Route as ViewAppIndexRouteImport } from './routes/_view/app/index'
import { Route as ApiSyncWriteRouteImport } from './routes/api/sync.write'
Expand Down Expand Up @@ -46,6 +47,11 @@ const ViewPricingRoute = ViewPricingRouteImport.update({
path: '/pricing',
getParentRoute: () => ViewRouteRoute,
} as any)
const ViewDownloadsRoute = ViewDownloadsRouteImport.update({
id: '/downloads',
path: '/downloads',
getParentRoute: () => ViewRouteRoute,
} as any)
const ViewAppRouteRoute = ViewAppRouteRouteImport.update({
id: '/app',
path: '/app',
Expand Down Expand Up @@ -84,6 +90,7 @@ const ViewAppAuthRoute = ViewAppAuthRouteImport.update({

export interface FileRoutesByFullPath {
'/app': typeof ViewAppRouteRouteWithChildren
'/downloads': typeof ViewDownloadsRoute
'/pricing': typeof ViewPricingRoute
'/callback/auth': typeof CallbackAuthRoute
'/webhook/nango': typeof WebhookNangoRoute
Expand All @@ -96,6 +103,7 @@ export interface FileRoutesByFullPath {
'/app/': typeof ViewAppIndexRoute
}
export interface FileRoutesByTo {
'/downloads': typeof ViewDownloadsRoute
'/pricing': typeof ViewPricingRoute
'/callback/auth': typeof CallbackAuthRoute
'/webhook/nango': typeof WebhookNangoRoute
Expand All @@ -111,6 +119,7 @@ export interface FileRoutesById {
__root__: typeof rootRouteImport
'/_view': typeof ViewRouteRouteWithChildren
'/_view/app': typeof ViewAppRouteRouteWithChildren
'/_view/downloads': typeof ViewDownloadsRoute
'/_view/pricing': typeof ViewPricingRoute
'/callback/auth': typeof CallbackAuthRoute
'/webhook/nango': typeof WebhookNangoRoute
Expand All @@ -126,6 +135,7 @@ export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths:
| '/app'
| '/downloads'
| '/pricing'
| '/callback/auth'
| '/webhook/nango'
Expand All @@ -138,6 +148,7 @@ export interface FileRouteTypes {
| '/app/'
fileRoutesByTo: FileRoutesByTo
to:
| '/downloads'
| '/pricing'
| '/callback/auth'
| '/webhook/nango'
Expand All @@ -152,6 +163,7 @@ export interface FileRouteTypes {
| '__root__'
| '/_view'
| '/_view/app'
| '/_view/downloads'
| '/_view/pricing'
| '/callback/auth'
| '/webhook/nango'
Expand Down Expand Up @@ -210,6 +222,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ViewPricingRouteImport
parentRoute: typeof ViewRouteRoute
}
'/_view/downloads': {
id: '/_view/downloads'
path: '/downloads'
fullPath: '/downloads'
preLoaderRoute: typeof ViewDownloadsRouteImport
parentRoute: typeof ViewRouteRoute
}
'/_view/app': {
id: '/_view/app'
path: '/app'
Expand Down Expand Up @@ -280,12 +299,14 @@ const ViewAppRouteRouteWithChildren = ViewAppRouteRoute._addFileChildren(

interface ViewRouteRouteChildren {
ViewAppRouteRoute: typeof ViewAppRouteRouteWithChildren
ViewDownloadsRoute: typeof ViewDownloadsRoute
ViewPricingRoute: typeof ViewPricingRoute
ViewIndexRoute: typeof ViewIndexRoute
}

const ViewRouteRouteChildren: ViewRouteRouteChildren = {
ViewAppRouteRoute: ViewAppRouteRouteWithChildren,
ViewDownloadsRoute: ViewDownloadsRoute,
ViewPricingRoute: ViewPricingRoute,
ViewIndexRoute: ViewIndexRoute,
}
Expand Down
86 changes: 86 additions & 0 deletions apps/web/src/routes/_view/downloads.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/_view/downloads")({
component: Component,
});

function Component() {
return (
<div className="bg-linear-to-b from-white via-blue-50/20 to-white min-h-screen">
<div className="max-w-4xl mx-auto px-4 sm:px-6 py-12">
<section className="py-16 sm:py-24 text-center">
<div className="space-y-6 max-w-2xl mx-auto">
<h1 className="text-4xl sm:text-5xl font-serif tracking-tight">
Download Hyprnote
</h1>
<p className="text-lg sm:text-xl text-neutral-600">
Choose your platform to get started with Hyprnote
</p>
</div>

<div className="grid gap-6 mt-12 max-w-2xl mx-auto">
<DownloadCard
platform="macOS"
icon="🍎"
description="For macOS 11.0 or later"
downloadUrl="#"
/>
<DownloadCard
platform="Windows"
icon="🪟"
description="For Windows 10 or later"
downloadUrl="#"
/>
<DownloadCard
platform="Linux"
icon="🐧"
description="For Ubuntu, Debian, and more"
downloadUrl="#"
/>
</div>
</section>
</div>
</div>
);
}

function DownloadCard({
platform,
icon,
description,
downloadUrl,
}: {
platform: string;
icon: string;
description: string;
downloadUrl: string;
}) {
return (
<a
href={downloadUrl}
className="group flex items-center justify-between p-6 rounded-xl border border-neutral-200 hover:border-blue-300 hover:bg-blue-50/30 transition-all duration-200"
>
<div className="flex items-center gap-4 text-left">
<span className="text-4xl">{icon}</span>
<div>
<h3 className="text-xl font-serif">{platform}</h3>
<p className="text-sm text-neutral-600">{description}</p>
</div>
</div>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
className="h-6 w-6 text-neutral-400 group-hover:text-blue-600 group-hover:translate-x-1 transition-all"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3"
/>
</svg>
</a>
);
}
170 changes: 133 additions & 37 deletions apps/web/src/routes/_view/index.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,142 @@
import { Typewriter } from "@hypr/ui/components/ui/typewriter";
import { cn } from "@hypr/utils";

import { Icon } from "@iconify-icon/react";
import { useQuery } from "@tanstack/react-query";
import { createFileRoute } from "@tanstack/react-router";

import { cn } from "@hypr/utils";
import { createFileRoute, Link } from "@tanstack/react-router";

export const Route = createFileRoute("/_view/")({
component: Component,
});

function Component() {
return (
<div className="flex flex-col justify-center items-center h-screen gap-8">
<Hero />
<div>
<main className="flex-1 bg-linear-to-b from-white via-blue-50/20 to-white min-h-screen">
<div className="max-w-6xl mx-auto py-12 border-x border-neutral-100">
<section className="py-16 sm:py-24">
<div className="grid grid-cols-1 lg:grid-cols-8 gap-12 items-center">
<div className="space-y-6 lg:col-span-3">
<h1 className="text-4xl sm:text-5xl font-serif tracking-tight whitespace-pre-wrap">
The AI notepad for{" "}
<Typewriter
text={["your meetings", "your lectures", "your thoughts"]}
className="text-blue-600 text-4xl sm:text-5xl font-serif tracking-tight"
speed={100}
deleteSpeed={50}
waitTime={2000}
/>
</h1>
<p className="text-lg sm:text-xl text-neutral-600 max-w-xl">
Hyprnote is a notetaking app that listens and summarizes the world around you
</p>
<div className="flex flex-col sm:flex-row gap-4 pt-2">
<DownloadButton />
<p className="text-neutral-500 self-center">
Free and{" "}
<a
className="decoration-dotted underline hover:text-blue-600 transition-all"
href="https://github.com/fastrepl/hyprnote"
target="_blank"
>
open source
</a>
</p>
</div>
</div>

<div className="flex gap-4">
<DownloadButton />
<GithubStars />
</div>
</div>
);
}
<div className="relative aspect-video bg-linear-to-br from-blue-50 to-neutral-50 rounded-2xl border-2 border-neutral-200 shadow-xl overflow-hidden lg:col-span-5">
<div className="absolute inset-0 flex items-center justify-center">
<div className="text-center space-y-4">
<Icon icon="mdi:play-circle-outline" className="text-6xl text-neutral-400 mx-auto" />
<p className="text-neutral-500 font-medium">Demo video coming soon</p>
</div>
</div>
</div>
</div>
</section>

function Hero() {
return (
<h1 className="font-mono text-4xl text-center">
AI notetaker that feels good
</h1>
<section className="py-16 sm:py-24 border-t border-neutral-100 text-center">
<div className="space-y-6 max-w-2xl mx-auto">
<h2 className="text-2xl sm:text-3xl font-serif">
Built for developers
</h2>
<div className="grid gap-3 pt-2">
{[
"AI-powered search and organization",
"Local-first, fast performance",
"Clean, distraction-free UI",
"Open source & privacy-focused",
].map((item, index) => (
<div
key={index}
className="group flex items-start gap-3 p-3 rounded-lg hover:bg-blue-50/30 transition-all duration-200"
>
<svg
className="h-5 w-5 flex-none text-blue-600 mt-0.5 group-hover:scale-110 transition-transform"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
clipRule="evenodd"
/>
</svg>
<p className="text-base leading-relaxed text-neutral-700 text-left">{item}</p>
</div>
))}
</div>
</div>
</section>

<section className="py-16 sm:py-24 border-t border-neutral-100">
<div className="space-y-6 text-center">
<h2 className="text-2xl sm:text-3xl">
Ready to get started?
</h2>
<p className="text-lg text-neutral-600 max-w-xl mx-auto">
Download Hyprnote today and experience note-taking reimagined.
</p>
<div className="pt-2 flex flex-col sm:flex-row gap-4 justify-center items-center">
<DownloadButton />
<GithubStars />
</div>
</div>
</section>
</div>
</main>
</div>
);
}

function DownloadButton() {
return (
<button
<Link
to="/downloads"
className={cn([
"px-4 py-2",
"bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium",
"cursor-pointer",
"group px-6 h-12 flex items-center justify-center text-base sm:text-lg",
"bg-linear-to-t from-blue-600 to-blue-500 text-white rounded-full",
"shadow-md hover:shadow-lg hover:scale-[102%] active:scale-[98%]",
"transition-all",
])}
>
Download now
</button>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
className="h-5 w-5 ml-2 group-hover:translate-x-1 transition-transform"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="m12.75 15 3-3m0 0-3-3m3 3h-7.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
/>
</svg>
</Link>
);
}

Expand All @@ -56,22 +153,21 @@ function GithubStars() {
},
});

const render = (n: number) => n > 1000 ? `${(n / 1000).toFixed(1)}k+` : n;
const render = (n: number) => n > 1000 ? `${(n / 1000).toFixed(1)}k` : n;

return (
<button
className={cn([
"px-4 py-2",
"flex items-center gap-2 font-light cursor-pointer",
"border border-gray-300 rounded-lg hover:bg-gray-50",
])}
onClick={() => window.open(`https://github.com/${ORG_REPO}`, "_blank")}
>
<Icon icon="mdi:github" />
<span>Source</span>
<span className="text-xs text-neutral-500 mt-1">
{star.data ? render(star.data) : render(LAST_SEEN)}
</span>
</button>
<a href={`https://github.com/${ORG_REPO}`} target="_blank">
<button
className={cn([
"group px-6 h-12 flex items-center justify-center text-base sm:text-lg",
"bg-linear-to-t from-neutral-800 to-neutral-700 text-white rounded-full",
"shadow-md hover:shadow-lg hover:scale-[102%] active:scale-[98%]",
"transition-all cursor-pointer",
])}
>
<Icon icon="mdi:github" className="text-xl" />
<span className="ml-2">{star.data ? render(star.data) : render(LAST_SEEN)} stars</span>
</button>
</a>
);
}
Loading
Loading