Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
95 changes: 29 additions & 66 deletions app/components/cta.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,21 @@
'use client';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Convention — 'use client' is no longer needed here. This component was rewritten to be purely declarative — no useState, no useRef, no event handlers. The 'use client' directive forces the entire component into a client bundle unnecessarily. Removing it would let Next.js render this as a server component, reducing JS shipped to the browser.

import { zodResolver } from '@hookform/resolvers/zod';
import { FC, useState, FormEvent, startTransition, useActionState, useRef } from 'react';
import { useForm } from 'react-hook-form';
import { formSchema } from '../utils/waitlist/schema';
import { submitWaitlistForm } from '../utils/waitlist/action';
import * as z from 'zod';

type FormValues = z.infer<typeof formSchema>;
import { FC } from 'react';
import { Chrome, Github } from 'lucide-react';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug — dead code left behind: The waitlist form was the only consumer of app/utils/waitlist/action.ts and app/utils/waitlist/schema.ts. These files are now completely unreferenced dead code and should be deleted.

Similarly, components/ui/form.tsx (a shadcn/ui form component) imports react-hook-form but is no longer imported anywhere in the codebase — also dead code.


export const CTA: FC = () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cleanup — unused npm dependencies. With the waitlist form removed, these package.json dependencies are now only referenced by dead code and should be uninstalled:

  • @hookform/resolvers — was used only by the old cta.tsx form
  • react-hook-form — used only by dead cta.tsx form + dead components/ui/form.tsx
  • zod — used only by app/utils/waitlist/schema.ts (dead code)

Keeping unused dependencies inflates install time and bundle size.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They will be reused later

const [email, setEmail] = useState<string>('');
const [submitted, setSubmitted] = useState<boolean>(false);
const [, formAction] = useActionState(submitWaitlistForm, { message: '' });

const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: { email: '' },
});

const formRef = useRef<HTMLFormElement>(null);

const handleSubmit = (e: FormEvent<HTMLFormElement>): void => {
e.preventDefault();
startTransition(() => {
formAction(new FormData(formRef.current!));
form.reset();
});
setSubmitted(true);
};

return (
<section className="py-36 px-6 md:px-12 relative overflow-hidden bg-ink" id="get-access">
<section
className="py-36 px-6 md:px-12 relative overflow-hidden bg-ink"
id="get-access"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Edge case — orphaned anchor id="get-access". The old hero CTA linked to href="#get-access" which smoothly scrolled down to this section. That internal link was replaced with the external Chrome Web Store URL, so nothing navigates to #get-access anymore. This dead anchor should be removed or renamed to something meaningful (e.g. id="install") in case you plan to link to it from elsewhere in the future.

>
<div className="absolute inset-0 grid-bg opacity-70" />
<div className="absolute top-0 left-0 right-0 h-px bg-edge" />

<div className="relative max-w-xl mx-auto text-center">
<div className="inline-flex items-center gap-2 border border-edge bg-ink2/80 px-4 py-1.5 mb-10 rounded-full">
<span className="w-1.5 h-1.5 rounded-full bg-amber animate-blink" />
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-blink" />
<span className="font-mono text-[10px] tracking-[0.18em] uppercase text-fog">
Early access open
Available now — free
</span>
</div>

Expand All @@ -49,45 +26,31 @@ export const CTA: FC = () => {
</h2>

<p className="text-[14px] text-mist mb-10 leading-relaxed font-body">
Be among the first to bring persistent memory to every AI you use.
Install the extension and bring persistent memory to every AI you use.
<br className="hidden md:block" />
No credit card. No setup fees.
Free, open-source, takes 30 seconds.
</p>

{!submitted ? (
<form
ref={formRef}
onSubmit={handleSubmit}
className="flex flex-col sm:flex-row gap-3 max-w-sm mx-auto"
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
<a
href="https://chromewebstore.google.com/detail/jjlelibnopjfeefpdplponpnocjfcega?utm_source=item-share-cb"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2.5 bg-amber text-white px-7 py-3.5 text-[12px] tracking-widest uppercase font-medium hover:bg-amber/90 transition-colors rounded-sm"
>
<input
type="email"
name="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="your@email.com"
className="flex-1 bg-ink2 border border-edge text-cream placeholder:text-fog px-4 py-3 text-[13px] font-body focus:outline-none focus:border-amber/40 transition-colors rounded-sm"
required
/>
<button
type="submit"
className="bg-amber text-white px-7 py-3 text-[11px] tracking-widest uppercase font-medium hover:bg-amber/90 transition-colors whitespace-nowrap rounded-sm"
>
Get Access
</button>
</form>
) : (
<div className="flex items-center justify-center gap-3 border border-edge bg-ink2/80 px-8 py-4 max-w-sm mx-auto animate-fade-in rounded-sm">
<span className="text-amber">✦</span>
<span className="font-mono text-[12px] text-mist">
You&apos;re on the list. We&apos;ll be in touch.
</span>
</div>
)}

<p className="mt-8 font-mono text-[10px] text-fog tracking-wide">
Built with Go · Qdrant · PostgreSQL · MCP
</p>
<Chrome size={20} strokeWidth={1.75} />
Add to Chrome
</a>
<a
href="https://github.com/freedisch/havril"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2.5 border border-edge bg-ink2/60 text-cream px-7 py-3.5 text-[12px] tracking-widest uppercase font-medium hover:bg-ink2 hover:border-edge2 transition-colors rounded-sm"
>
<Github size={18} strokeWidth={1.75} />
Star on GitHub
</a>
</div>
</div>
</section>
);
Expand Down
31 changes: 19 additions & 12 deletions app/components/hero.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
'use client';
import { FC, useEffect, useState } from 'react';
import { Chrome, Github } from 'lucide-react';
import { useTypewriter } from '../utils/customState';

export const Hero: FC = () => {
const memories = [
'"User is building a Go REST API"',
'"User prefers minimal dependencies"',
'"User is based in Kigali, Rwanda"',
'"User uses Chi router + PostgreSQL"',
'"User is writing a thesis on climate policy"',
'"User prefers APA citations and concise summaries"',
'"User is preparing for the GRE in August"',
'"User is learning French at intermediate level"',
];
const typed = useTypewriter(memories, 44, 2600);

Expand All @@ -17,9 +18,9 @@ export const Hero: FC = () => {

{/* Badge */}
<div className="relative z-10 flex items-center gap-2 border border-edge bg-ink2/80 px-4 py-1.5 mb-10 rounded-full animate-fade-up delay-0">
<span className="w-1.5 h-1.5 rounded-full bg-amber animate-blink" />
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-blink" />
<span className="font-mono text-[10px] tracking-widest uppercase text-fog">
Now in development
Live on Chrome Web Store
</span>
</div>

Expand All @@ -43,16 +44,22 @@ export const Hero: FC = () => {
{/* CTAs */}
<div className="relative z-10 mt-9 flex flex-col sm:flex-row items-center gap-4 animate-fade-up delay-300">
<a
href="#get-access"
className="bg-amber text-white px-7 py-3 text-[12px] tracking-widest uppercase font-medium rounded-sm hover:bg-amber/90 transition-colors"
href="https://chromewebstore.google.com/detail/jjlelibnopjfeefpdplponpnocjfcega?utm_source=item-share-cb"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DRY violation — hardcoded Chrome Web Store URL duplicated 4×. This exact URL appears in hero.tsx, cta.tsx, nav.tsx (desktop), and nav.tsx (mobile). If the extension ID or UTM param ever changes, you'd need to update 4 places.

Consider extracting to a shared constant:

// app/utils/constants.ts
export const CHROME_STORE_URL = 'https://chromewebstore.google.com/detail/jjlelibnopjfeefpdplponpnocjfcega?utm_source=item-share-cb';
export const GITHUB_REPO_URL = 'https://github.com/freedisch/havril';

The GitHub URL (https://github.com/freedisch/havril) is similarly duplicated 4 times across these same files.

target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2.5 bg-amber text-white px-7 py-3.5 text-[12px] tracking-widest uppercase font-medium rounded-sm hover:bg-amber/90 transition-colors"
>
Get early access
<Chrome size={20} strokeWidth={1.75} />
Add to Chrome
</a>
<a
href="#integrations"
className="text-[12px] tracking-widest uppercase text-fog hover:text-mist transition-colors"
href="https://github.com/freedisch/havril"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2.5 border border-edge bg-ink2/60 text-cream px-7 py-3.5 text-[12px] tracking-widest uppercase font-medium rounded-sm hover:bg-ink2 hover:border-edge2 transition-colors"
>
See it in action
<Github size={18} strokeWidth={1.75} />
Star on GitHub
</a>
</div>

Expand Down
21 changes: 16 additions & 5 deletions app/components/nav.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use client';
import { FC, useState } from 'react';
import { Chrome } from 'lucide-react';
import { useScrolled } from '../utils/customState';

export const Nav: FC = () => {
Expand Down Expand Up @@ -41,10 +42,13 @@ export const Nav: FC = () => {
</svg>
</a>
<a
href="#get-access"
className="text-[11px] tracking-widest uppercase bg-amber text-white px-5 py-2.5 font-medium hover:bg-amber/90 transition-colors rounded-sm"
href="https://chromewebstore.google.com/detail/jjlelibnopjfeefpdplponpnocjfcega?utm_source=item-share-cb"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 text-[11px] tracking-widest uppercase bg-amber text-white px-5 py-2.5 font-medium hover:bg-amber/90 transition-colors rounded-sm"
>
Get Early Access
<Chrome size={16} strokeWidth={1.75} />
Add to Chrome
</a>
</div>

Expand Down Expand Up @@ -83,8 +87,15 @@ export const Nav: FC = () => {
>
X / Twitter
</a>
<a href="#get-access" className="text-[11px] uppercase bg-amber text-white px-5 py-3 font-medium text-center rounded-sm">
Get Early Access
<a
href="https://chromewebstore.google.com/detail/jjlelibnopjfeefpdplponpnocjfcega?utm_source=item-share-cb"
target="_blank"
rel="noopener noreferrer"
onClick={() => setMenuOpen(false)}
className="inline-flex items-center justify-center gap-2 text-[11px] uppercase bg-amber text-white px-5 py-3 font-medium text-center rounded-sm"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Styling inconsistency — mobile CTA missing tracking-widest. The desktop "Add to Chrome" button (line 48) has tracking-widest but this mobile version only has uppercase without it. The letter-spacing will differ between breakpoints for the same button, which looks inconsistent. Add tracking-widest here to match the desktop variant.

>
<Chrome size={16} strokeWidth={1.75} />
Add to Chrome
</a>
</div>
)}
Expand Down
Loading