Skip to content

Conversation

@mrmps
Copy link
Owner

@mrmps mrmps commented Jan 25, 2026

Summary

  • Redesign home hero with brand identity, value prop tagline, and premium styling (film grain texture, P3 ambient glow for modern displays)
  • Hide ambient effects in light mode for cleaner appearance
  • Fix BottomCornerNav colors for proper light/dark mode support
  • Improve image handling in articles and enhance Clerk drawer visibility

Test plan

  • Verify home hero looks correct on both light and dark modes
  • Check P3 ambient glow renders on displays that support it
  • Test BottomCornerNav colors in both themes
  • Confirm film grain texture is subtle and premium-feeling

🤖 Generated with Claude Code


Open with Devin

Summary by CodeRabbit

  • New Features

    • Premium ArticleReader, redesigned Home & Pricing, richer History, Bottom-corner nav, bookmarklet drag-to-bar UI, new Logo, revamped Share modal, OpenGraph/Twitter image generators, source proxy & fetch improvements, analytics scripts.
  • Style

    • Added Syne font, P3 accent color support, refined typography, responsive image styling with hover/zoom and expanded view, updated drawer/banner visuals.
  • Bug Fixes / UX

    • Suppressed duplicate mobile drawer close buttons, improved image expand/collapse and sharing/copy flows.
  • Internationalization

    • Added extensive navigation and promo translation keys across locales.
  • Chores

    • Dev tooling and config updates (scripts, dev deps, tsconfig).

✏️ Tip: You can customize this high-level summary in your review settings.

mrmps and others added 19 commits January 24, 2026 03:32
- Fix useRef type to include undefined initial value
- Remove unsupported id prop from Base UI Popover components
- Replace useState/useEffect mounted pattern with useSyncExternalStore
- Fix unused imports (Crown, Star, ChevronUp, etc.)
- Rename unused variables to underscore prefix convention
- Fix Math.random in useMemo by moving to useEffect with ref

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Tighten vertical rhythm: 8px → 16px → 8px between elements
- Input: rounded-xl → rounded-lg for less bubbly look
- Input: near-invisible border (8% opacity) instead of prominent border
- Input: subtle background differentiation with bg-muted/30
- Remove chunky focus ring, use subtle bg/border shift instead

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Visual refinements:
- Add subtle ambient glow behind content for depth
- Narrow container from max-w-xl to max-w-md for focus
- Replace chunky CornerDownLeft icon with sleek ArrowRight
- Tighter input padding (py-4 → py-3, px-5 → px-4)
- Lighter placeholder color (50% opacity)

Typography & copy:
- Smaller tagline (text-lg → text-base) with softer color
- Remove awkward period after "Try it" link
- Placeholder: "Paste article URL..." → "Paste any article link"
- Make "Try it" link more prominent (foreground/80)

Simplification:
- Remove verbose "Quick access tips" accordion
- Replace with minimal one-line hint about smry.ai/ prepend
- Remove unused imports (BookmarkletLink, PaperAirplaneIcon)

Also: Simplify share URL generation (remove redundant params)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Apply premium patterns from Darkread UI:
- Nested border-radius: container 14px, inner input 8px, button 10px
- Form as unified container with p-1, gap-1, bg-foreground/[0.045]
- Subtle 15% border opacity (vs previous 8%)
- Square 36px submit button (size-9)
- Minimal placeholder: "https://..." instead of instructional text
- Description moved BELOW input (not tagline above)
- Bold 4xl title with tracking-tight
- Removed ambient glow (unnecessary complexity)
- Container width: 528px (matches reference exactly)

Removed:
- Button component import (using native button)
- isHovered state (no longer needed)
- Ambient glow div

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Brand differentiation:
- Restore Syne font for wordmark (text-5xl font-semibold)
- Add subtle violet gradient glow behind content
  (violet-500/[0.07], positioned slightly above center)
- Increase spacing: mt-6 → mt-8 between wordmark and input

Copy refinement:
- Remove verbose tagline entirely
- Replace with minimal "See it in action →" link
- Confidence through restraint - let the product speak

The violet glow creates subtle brand presence without being
overpowering. It hints at the "magic" of bypassing paywalls.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
"Unlock any article. Read or summarize it."

- Short, powerful, covers both core features
- Positioned between wordmark and input (mt-3)
- Muted color (70% opacity) to not compete with input
- Adjusted input margin (mt-8 → mt-6) for tighter grouping

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Uses SVG feTurbulence filter at 3% opacity for premium feel.
Grain adds analog warmth to digital black - makes it feel
rich rather than flat LCD. Used by Apple, Linear, etc.

The grain is:
- Static (no animation)
- 3% opacity (barely perceptible consciously)
- Full viewport coverage
- Layered behind the violet glow

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Violet feels dated, removed entirely
- Bumped grain opacity slightly (3% → 3.5%)
- Adjusted grain frequency (0.9 → 0.8) for slightly coarser texture
- Cleaner, more timeless aesthetic

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Background-image SVG wasn't rendering
- Switched to inline <svg> with feTurbulence filter
- Bumped opacity to 15% for visibility testing
- Will dial back once confirmed working

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Opacity: 15% → 4% (felt, not seen)
- Added mix-blend-soft-light for natural integration
- Finer grain: baseFrequency 0.7 → 0.9
- More detail: numOctaves 3 → 4
- Removed feColorMatrix (unnecessary)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Subtle radial gradient using display-p3 color space:
- Ellipse 80% x 60% positioned slightly above center
- Cool blue-gray tint (0.15, 0.15, 0.2) at 40% opacity
- P3 enables richer, deeper colors impossible in sRGB
- Graceful fallback on non-P3 displays

Layer order: P3 glow → grain → content

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- P3 glow and grain only show in dark mode (hidden dark:block)
- Light mode stays clean and minimal
- Effects designed for dark backgrounds look forced on light

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Added pb-24 to push centered content above true center.
Optical center is slightly above mathematical center.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Position similar to Brave homepage - upper-middle area.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Hierarchy now:
1. smry (brand - focus)
2. Input (action)
3. "Unlock any article..." with Try it link (explanation)

Brand gets full attention, input is immediate, explanation supports.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replaced all hardcoded neutral-* colors with semantic theme colors:
- bg-neutral-800/900 → bg-accent/bg-popover
- text-neutral-100/200/300 → text-foreground
- text-neutral-400/500/600 → text-muted-foreground variants
- border-neutral-* → border-border

Now adapts properly to both light and dark mode.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@vercel
Copy link

vercel bot commented Jan 25, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
smry Ready Ready Preview, Comment Jan 26, 2026 3:28am

Request Review

@railway-app railway-app bot temporarily deployed to smry / SMRY-pr-45 January 25, 2026 01:18 Destroyed
@railway-app
Copy link

railway-app bot commented Jan 25, 2026

🚅 Deployed to the SMRY-pr-45 environment in smry

Service Status Web Updated (UTC)
smry-api ✅ Success (View Logs) Jan 26, 2026 at 3:28 am
SMRY 🕒 Building (View Logs) Web Jan 26, 2026 at 3:27 am

@coderabbitai
Copy link

coderabbitai bot commented Jan 25, 2026

Warning

Rate limit exceeded

@mrmps has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 7 minutes and 25 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

Adds a new ArticleReader feature, client-side image expand/collapse, global Syne font and P3 color variables, many UI and i18n/localization updates, BottomCornerNav and Logo components, Jina article fetcher, dev-only ReactGrab, ClickHouse analysis scripts, and OG/Twitter image generators.

Changes

Cohort / File(s) Summary
Styling & Theme
app/globals.css
Add Syne font var, image sizing/expand rules (including .expanded state), hide broken images, small-icon rules, and wide‑gamut P3 accent color tokens with dark/P3 overrides; suppress duplicate Clerk drawer close buttons on mobile.
Root Layout & Dev Tooling
app/layout.tsx, package.json
Register Syne font CSS var in root layout, mount ReactGrab, update metadata, wire ClerkProvider appearance to hide duplicate close buttons, and add react-grab devDependency.
Article Reader & Content
components/features/article-reader.tsx, components/article/content.tsx
New ArticleReader, ArticleContent, VerticalScrollIndicator; client-side sanitized HTML rendering with contentRef and image click handlers to toggle expand/collapse.
Home & Marketing UI
components/features/home-content.tsx, components/marketing/bookmarklet.tsx
Large home UI refactor (feature grid, URL validation/paste UX); bookmarklet redesigned as draggable SMRY button with custom DataTransfer and drag preview; copy/tips added.
History & Navigation
components/pages/history-content.tsx, components/shared/bottom-corner-nav.tsx, components/shared/logo.tsx
Major history-page refactor (grouping, view-modes, keyboard nav, favicons); add BottomCornerNav (language/help/theme/bookmarklet/auth) and new Logo component.
Pricing & Share
components/pages/pricing-content.tsx, components/features/share-button.tsx
Pricing page overhaul (billing toggle, publications, features, dynamic CTA); simplified Share modal and reduced ShareContent prop surface / URL handling.
Proxy, Menu & Drawer
components/features/proxy-content.tsx, components/ui/popover.tsx, components/features/responsive-drawer.tsx, proxy.ts
Hydration-safe menu items (history/language/theme), proxied share URLs, PopoverPopup gains contentClassName, drawer/dialog styling tweaks, and proxy matcher excludes OG/twitter image routes.
Client Helpers & UI Primitives
components/shared/react-grab.tsx, components/ui/dialog.tsx, components/ui/popover.tsx
Add dev-only ReactGrab dynamic import, tweak dialog backdrop blur/transition, and expose popover contentClassName prop.
API & i18n Utilities
lib/api/jina.ts, lib/i18n-pathname.ts, components/shared/language-switcher.tsx
Add Jina fetcher with headers, JSON/Markdown parsing, cleaning and HTML conversion helpers; add stripLocaleFromPathname; LanguageSwitcher uses startTransition with pathname+params.
AI / Response
components/ai/response.tsx
Remove default KaTeX rehype plugin from Response plugin list.
Localization
messages/*.json (de, en, es, nl, pt, zh)
Large localization additions/updates: new nav object, expanded home/bookmarklet/pricing/promo keys, tagline/placeholder updates, and pricing.prioritySupport.
Changelog & Promo
lib/changelog.ts, components/marketing/*
Add structured changelog and getRecentChanges; localize promo banner and upgrade CTA.
Scripts & Analysis
scripts/*.ts, scripts/*.sh, scripts/*.sql
Add ClickHouse analysis TypeScript script, shell analyzer, and SQL query suite for source effectiveness.
OpenGraph / Social Images
app/opengraph-image.tsx, app/twitter-image.tsx
Add ImageResponse-based OG and Twitter image generators with exported metadata (alt/size/contentType/runtime).
Misc Config
tsconfig.json, package.json
Exclude scripts from tsconfig; add react-grab devDependency.

Sequence Diagram(s)

(Skipped — changes are broad and disparate; no single multi-component sequential flow met the diagram criteria.)

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Poem

🐰 I nibble fonts and stitch a playful theme,
Images that zoom and a reader that gleams.
Corner nav to guide, locales to roam,
Drag the SMRY bookmarklet — carry it home.
Hop—new colors and pages—code snug as loam.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Redesign home hero and refresh history UX' directly and clearly summarizes the main changes across the codebase.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

- Remove Bloomberg from publications list
- Update typical monthly cost from $92 to $57
- Adjust success rate to 76%

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@greptile-apps
Copy link

greptile-apps bot commented Jan 25, 2026

Greptile Overview

Greptile Summary

This PR redesigns the home hero and refreshes the history UX with premium styling and brand identity improvements.

Major changes:

  • Redesigned home hero with new value proposition, feature visuals, and brand identity using Syne font
  • Added P3 wide gamut color support with proper fallbacks for modern displays
  • Created new BottomCornerNav component with proper light/dark mode theming using design system tokens
  • New premium ArticleReader component with reading progress, accessibility features, and refined typography
  • Updated drawer styling across the app to use design system tokens (bg-card, border-border) for consistent theming
  • Improved image handling in articles with zoom/expand functionality
  • Fixed Clerk drawer visibility by hiding duplicate close buttons

Issue found:

  • The Sparkles icon is used in history-content.tsx (lines 25, 367), which violates the design system rule in AGENTS.md that explicitly bans this icon as "cheap and gimmicky"

Confidence Score: 4/5

  • This PR is safe to merge with one minor design system violation that should be fixed
  • Score reflects well-structured code with proper theming, accessibility, and responsive design. The only issue is the use of the banned Sparkles icon which violates the design system rules in AGENTS.md. The rest of the changes follow best practices and use design system tokens correctly.
  • components/pages/history-content.tsx needs the Sparkles icon replaced with an allowed alternative like BookMarked, TrendingUp, or Crown

Important Files Changed

Filename Overview
components/pages/history-content.tsx Redesigned history UX with improved typography and visual hierarchy; violates design system with banned Sparkles icon
components/features/home-content.tsx Redesigned home hero with brand identity, value prop, feature visuals, and P3 color support
components/shared/bottom-corner-nav.tsx New bottom-corner navigation with language/help popovers, theme switching, and improved light/dark mode support
app/globals.css Added Syne font, P3 wide gamut colors with fallbacks, and improved image handling in articles
components/features/article-reader.tsx New premium article reader with reading progress, accessibility features, and refined typography
components/features/responsive-drawer.tsx Updated drawer styling to use design system tokens (bg-card, border-border) for consistent theming
app/layout.tsx Added Syne font, ReactGrab component, and Clerk appearance config to hide duplicate drawer close buttons
components/shared/logo.tsx New Logo component using Syne font with configurable sizes

Sequence Diagram

sequenceDiagram
    participant User
    participant HomePage
    participant BottomCornerNav
    participant ArticleReader
    participant ThemeSystem
    participant P3Display

    User->>HomePage: Visit site
    HomePage->>ThemeSystem: Load theme (light/dark/system)
    ThemeSystem-->>HomePage: Apply theme tokens
    
    User->>HomePage: View hero section
    HomePage->>P3Display: Check P3 color support
    P3Display-->>HomePage: Render with P3 or fallback colors
    
    User->>BottomCornerNav: Click help button
    BottomCornerNav->>BottomCornerNav: Show popover with menu
    
    User->>BottomCornerNav: Change theme
    BottomCornerNav->>ThemeSystem: setTheme(light/dark/system)
    ThemeSystem->>HomePage: Update CSS tokens
    ThemeSystem->>ArticleReader: Update CSS tokens
    ThemeSystem->>BottomCornerNav: Update CSS tokens
    
    User->>HomePage: Paste article URL
    HomePage->>ArticleReader: Load article content
    ArticleReader->>ArticleReader: Initialize reading progress
    
    User->>ArticleReader: Scroll article
    ArticleReader->>ArticleReader: Update progress bar
    ArticleReader->>ArticleReader: Calculate reading position
    
    User->>ArticleReader: Click image
    ArticleReader->>ArticleReader: Toggle expanded state
Loading

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

1 file reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Grid3X3,
List,
LayoutGrid,
Sparkles,
Copy link

Choose a reason for hiding this comment

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

Remove unused Sparkles import - violates design system rules in AGENTS.md

Suggested change
Sparkles,
BookMarked,

Context Used: Context from dashboard - AGENTS.md (source)

Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 3 potential issues.

View issues and 16 additional flags in Devin Review.

Open in Devin Review

Comment on lines 1 to 24
0 verbose cli /opt/homebrew/Cellar/node/25.2.1/bin/node /opt/homebrew/lib/node_modules/npm/bin/npm-cli.js
1 info using npm@11.6.2
2 info using node@v25.2.1
3 silly config load:file:/opt/homebrew/lib/node_modules/npm/npmrc
4 silly config load:file:/Users/michaelryaboy/projects/13ft/.npmrc
5 silly config load:file:/Users/michaelryaboy/projects/13ft/~/.npmrc
6 silly config load:file:/opt/homebrew/etc/npmrc
7 verbose title npm exec tsc --noEmit
8 verbose argv "exec" "--" "tsc" "--noEmit"
9 verbose logfile logs-max:10 dir:/Users/michaelryaboy/projects/13ft/~/.npm/_logs/2026-01-24T09_54_42_675Z-
10 verbose logfile /Users/michaelryaboy/projects/13ft/~/.npm/_logs/2026-01-24T09_54_42_675Z-debug-0.log
11 silly logfile done cleaning log files
12 http fetch GET 200 https://registry.npmjs.org/npm 126ms
13 verbose cwd /Users/michaelryaboy/projects/13ft
14 verbose os Darwin 25.1.0
15 verbose node v25.2.1
16 verbose npm v11.6.2
17 notice
17 notice New minor version of npm available! 11.6.2 -> 11.8.0
17 notice Changelog: https://github.com/npm/cli/releases/tag/v11.8.0
17 notice To update run: npm install -g npm@11.8.0
17 notice { force: true, Symbol(proc-log.meta): true }
18 verbose exit 0
19 info ok

Choose a reason for hiding this comment

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

🔴 Accidentally committed developer machine npm logs into the repository

The PR adds ~/.npm/_logs/... and ~/.npm/_update-notifier-last-checked into the repo.

Actual: this leaks developer environment paths and tooling metadata and adds noise/bloat to the repository.
Expected: these files should never be committed; they belong in .gitignore.

Files
  • ~/.npm/_logs/2026-01-24T09_54_42_675Z-debug-0.log:1-24

Recommendation: Remove these files from the PR and add ~/.npm/ (and other OS/tooling artifacts) to .gitignore as appropriate.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
messages/de.json (1)

11-25: Keep formality consistent in home tips.

prependTip uses informal "Füge" while the rest of the home copy uses formal address ("Sie/Ihre"). Consider aligning this to formal tone.

Suggested fix
-    "prependTip": "Füge <code>smry.ai/</code> vor jede Artikel-URL ein",
+    "prependTip": "Fügen Sie <code>smry.ai/</code> vor jede Artikel-URL ein",
🤖 Fix all issues with AI agents
In `@components/article/content.tsx`:
- Around line 192-217: The image expand/collapse behavior in the useEffect
watching sanitizedArticleContent is click-only; update the logic around
contentRef/sanitizedArticleContent to make images keyboard-accessible by setting
tabIndex=0 and role="button" on each image, maintain an aria-expanded attribute
that mirrors the "expanded" class, and add a keydown handler (in addition to
handleClick) that toggles expansion when Enter or Space is pressed; register
both click and keydown listeners on each img in the images.forEach block and
ensure both are removed in the cleanup return so event handlers remain balanced
(update the existing handleClick reference or create a handleKeyDown that reuses
the same toggle logic and updates aria-expanded accordingly).

In `@components/features/article-reader.tsx`:
- Around line 1109-1116: The exported ArticleContent component currently exposes
an unsanitized HTML sink (ArticleContent({ html })) and should be removed or
hardened: either delete the ArticleContent export if unused, or update it to
accept and render sanitized markup (use the existing useSanitizedHtml hook
pattern from components/article/content.tsx or sanitize html with DOMPurify
before passing to dangerouslySetInnerHTML), or rename the prop to sanitizedHtml
to enforce callers supply already-sanitized content; locate the ArticleContent
function and apply one of these fixes to eliminate the XSS risk.

In `@components/features/home-content.tsx`:
- Around line 252-268: The global paste handler in the useEffect (handlePaste)
incorrectly only skips when document.activeElement === inputRef.current, so
pastes into other editable elements get hijacked; update handlePaste to
early-return if the paste event's target is an editable element (e.g.,
event.target is an HTMLElement that is contentEditable or has tagName INPUT,
TEXTAREA, or SELECT, or a role="textbox"/[contenteditable="true"]) before
calling setUrl/setUrlError or focusing inputRef; keep inputRef checks intact and
ensure type-narrowing of the ClipboardEvent target to HTMLElement when
performing these checks.

In `@components/pages/history-content.tsx`:
- Around line 352-357: The close button with onClick={() => setIsVisible(false)}
that renders the X icon is missing an accessible name; add an accessible label
by including aria-label="Close" (or aria-label="Dismiss" / aria-label="Close
history panel") on the button or add visually hidden text inside the button so
screen readers can announce its purpose; update the element where the X icon is
used to include this aria-label on the button that calls setIsVisible.
- Around line 741-777: The view-mode icon buttons (buttons that call
onViewModeChange("list"/"compact"/"grid") and reference the viewMode state) lack
accessible attributes; update each button to include an appropriate aria-label
(e.g., "List view", "Compact view", "Grid view") and set aria-pressed to a
boolean expression comparing viewMode to the respective mode (e.g.,
aria-pressed={viewMode === "list"}) so screen readers announce the role and
current pressed state while keeping the existing title and onClick behavior.

In `@messages/de.json`:
- Around line 140-145: Update the brand casing in the FAQ value copy: replace
the lowercase "smry Pro" with the standard "SMRY Pro" (and any other "smry"
occurrences in that same string) in the JSON value for the "faqValueAnswer" key
so it matches the rest of the locale; verify "faqValue" label uses "SMRY" casing
too if present elsewhere.

In `@messages/zh.json`:
- Around line 140-145: The FAQ strings use lowercase "smry" instead of the
branded "SMRY"; update the message values for the affected keys (e.g.,
"faqLegalAnswer" and "faqValueAnswer" — and any other new/edited FAQ keys in
this diff such as "faqSiteSupportAnswer" if present) to use "SMRY" casing
consistently throughout the text while preserving the rest of each sentence and
punctuation.
♻️ Duplicate comments (3)
components/pages/pricing-content.tsx (1)

278-287: Previous i18n issue resolved.

All FAQ entries now consistently use the translation function t(), addressing the previously flagged inconsistency.

components/pages/history-content.tsx (1)

21-27: Replace the banned Sparkles icon.

Sparkles is disallowed by the design system (already flagged). Swap to an allowed icon and remove the import.

🛠️ Proposed fix
-  Sparkles,
   BookMarked,
-            <Sparkles className="size-3 text-primary" />
+            <BookMarked className="size-3 text-primary" />

Also applies to: 363-368

components/features/home-content.tsx (1)

64-227: New user-facing copy should be localized.

The new feature grid text and “Learn more” label are hardcoded English; this will regress non‑English locales. Please move these into t(...) keys like the rest of the home content.

🔤 Example pattern
- <span className="text-xs">Learn more</span>
+ <span className="text-xs">{t("learnMore")}</span>

- <FeatureText
-   title="Three sources race, the fastest wins"
-   description="We fetch from Wayback Machine, Jina.ai, and direct access simultaneously. First one with the article wins."
- />
+ <FeatureText
+   title={t("featureRaceTitle")}
+   description={t("featureRaceDesc")}
+ />

Also applies to: 430-466

🧹 Nitpick comments (4)
messages/es.json (1)

222-239: Minor inconsistency in changelog terminology.

Line 227 uses "changelog": "Cambios" while line 241 uses "Registro de cambios" for the same concept. If this is intentional for brevity in the nav menu, consider adding a comment to clarify. Otherwise, consider aligning to "Historial de cambios" or "Registro de cambios" for consistency.

components/pages/pricing-content.tsx (2)

64-73: Remove unused _publications array.

This array is defined but never referenced in the component. The publicationCategories array (lines 75-80) is used instead for the publications grid.

🧹 Suggested fix
-const _publications = [
-  "Medium",
-  "Business Insider",
-  "Wired",
-  "The Atlantic",
-  "Foreign Policy",
-  "Quora",
-  "The New Yorker",
-  "MIT Tech Review",
-];
-
 const publicationCategories = [

245-268: Remove or fully comment out the unused timer effect.

This effect sets up an interval that runs every minute but the calculated values (_days, _hours, _minutes) are never displayed. Running setInterval with dead code wastes CPU cycles on every page visit.

If this is intended for future use, comment out the entire effect rather than letting it execute.

🧹 Suggested fix - remove entirely
-  // Countdown timer to Feb 15 (currently unused - keeping for future use)
-  const _timerRef = useRef<NodeJS.Timeout | undefined>(undefined);
-
-  useEffect(() => {
-    const calculateTimeLeft = () => {
-      const deadline = new Date("2025-02-15T23:59:59");
-      const now = new Date();
-      const diff = deadline.getTime() - now.getTime();
-
-      if (diff > 0) {
-        // Timer values available but not currently displayed
-        const _days = Math.floor(diff / (1000 * 60 * 60 * 24));
-        const _hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
-        const _minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
-        void [_days, _hours, _minutes]; // Suppress unused warnings
-      }
-    };
-
-    calculateTimeLeft();
-    _timerRef.current = setInterval(calculateTimeLeft, 60000);
-    return () => {
-      if (_timerRef.current) clearInterval(_timerRef.current);
-    };
-  }, []);

Also note: the deadline 2025-02-15 is already in the past (current date is January 2026), so this timer would show zero/negative values if ever enabled.

components/features/article-reader.tsx (1)

109-123: Recompute progress on resize/orientation changes.

Progress can become stale after viewport changes. Consider subscribing to resize and re-running the same RAF update.

♻️ Suggested tweak
   useEffect(() => {
     const handleScroll = () => {
       if (rafRef.current) cancelAnimationFrame(rafRef.current);
       rafRef.current = requestAnimationFrame(updateProgress);
     };
+    const handleResize = () => {
+      if (rafRef.current) cancelAnimationFrame(rafRef.current);
+      rafRef.current = requestAnimationFrame(updateProgress);
+    };

     window.addEventListener("scroll", handleScroll, { passive: true });
+    window.addEventListener("resize", handleResize);
     // Schedule initial progress calculation for next frame to avoid sync setState in effect
     rafRef.current = requestAnimationFrame(updateProgress);

     return () => {
       window.removeEventListener("scroll", handleScroll);
+      window.removeEventListener("resize", handleResize);
       if (rafRef.current) cancelAnimationFrame(rafRef.current);
     };
   }, [updateProgress]);

Comment on lines 192 to 217
const contentRef = React.useRef<HTMLDivElement>(null);

const sanitizedArticleContent = useSanitizedHtml(data?.article?.content);

// Add click-to-expand functionality to images
// Must depend on sanitizedArticleContent since that's what's actually rendered
useEffect(() => {
if (!contentRef.current || !sanitizedArticleContent) return;

const images = contentRef.current.querySelectorAll("img");
const handleClick = (e: Event) => {
const img = e.target as HTMLImageElement;
img.classList.toggle("expanded");
};

images.forEach((img) => {
img.addEventListener("click", handleClick);
img.title = "Click to expand/collapse";
});

return () => {
images.forEach((img) => {
img.removeEventListener("click", handleClick);
});
};
}, [sanitizedArticleContent]);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add keyboard-accessible toggling for expandable images.

Currently the expand/collapse affordance is click-only (Line 202-210), which excludes keyboard users. Add tabIndex, role="button", and key handling for Enter/Space, and keep an aria-expanded state in sync.

♿ Suggested fix
-    const handleClick = (e: Event) => {
-      const img = e.target as HTMLImageElement;
-      img.classList.toggle("expanded");
-    };
+    const toggle = (img: HTMLImageElement) => {
+      const expanded = img.classList.toggle("expanded");
+      img.setAttribute("aria-expanded", expanded ? "true" : "false");
+    };
+
+    const handleClick = (e: Event) => {
+      toggle(e.currentTarget as HTMLImageElement);
+    };
+
+    const handleKeyDown = (e: KeyboardEvent) => {
+      if (e.key === "Enter" || e.key === " ") {
+        e.preventDefault();
+        toggle(e.currentTarget as HTMLImageElement);
+      }
+    };

     images.forEach((img) => {
       img.addEventListener("click", handleClick);
+      img.addEventListener("keydown", handleKeyDown);
+      img.setAttribute("tabindex", "0");
+      img.setAttribute("role", "button");
+      img.setAttribute("aria-expanded", "false");
       img.title = "Click to expand/collapse";
     });

     return () => {
       images.forEach((img) => {
         img.removeEventListener("click", handleClick);
+        img.removeEventListener("keydown", handleKeyDown);
       });
     };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const contentRef = React.useRef<HTMLDivElement>(null);
const sanitizedArticleContent = useSanitizedHtml(data?.article?.content);
// Add click-to-expand functionality to images
// Must depend on sanitizedArticleContent since that's what's actually rendered
useEffect(() => {
if (!contentRef.current || !sanitizedArticleContent) return;
const images = contentRef.current.querySelectorAll("img");
const handleClick = (e: Event) => {
const img = e.target as HTMLImageElement;
img.classList.toggle("expanded");
};
images.forEach((img) => {
img.addEventListener("click", handleClick);
img.title = "Click to expand/collapse";
});
return () => {
images.forEach((img) => {
img.removeEventListener("click", handleClick);
});
};
}, [sanitizedArticleContent]);
const contentRef = React.useRef<HTMLDivElement>(null);
const sanitizedArticleContent = useSanitizedHtml(data?.article?.content);
// Add click-to-expand functionality to images
// Must depend on sanitizedArticleContent since that's what's actually rendered
useEffect(() => {
if (!contentRef.current || !sanitizedArticleContent) return;
const images = contentRef.current.querySelectorAll("img");
const toggle = (img: HTMLImageElement) => {
const expanded = img.classList.toggle("expanded");
img.setAttribute("aria-expanded", expanded ? "true" : "false");
};
const handleClick = (e: Event) => {
toggle(e.currentTarget as HTMLImageElement);
};
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
toggle(e.currentTarget as HTMLImageElement);
}
};
images.forEach((img) => {
img.addEventListener("click", handleClick);
img.addEventListener("keydown", handleKeyDown);
img.setAttribute("tabindex", "0");
img.setAttribute("role", "button");
img.setAttribute("aria-expanded", "false");
img.title = "Click to expand/collapse";
});
return () => {
images.forEach((img) => {
img.removeEventListener("click", handleClick);
img.removeEventListener("keydown", handleKeyDown);
});
};
}, [sanitizedArticleContent]);
🤖 Prompt for AI Agents
In `@components/article/content.tsx` around lines 192 - 217, The image
expand/collapse behavior in the useEffect watching sanitizedArticleContent is
click-only; update the logic around contentRef/sanitizedArticleContent to make
images keyboard-accessible by setting tabIndex=0 and role="button" on each
image, maintain an aria-expanded attribute that mirrors the "expanded" class,
and add a keydown handler (in addition to handleClick) that toggles expansion
when Enter or Space is pressed; register both click and keydown listeners on
each img in the images.forEach block and ensure both are removed in the cleanup
return so event handlers remain balanced (update the existing handleClick
reference or create a handleKeyDown that reuses the same toggle logic and
updates aria-expanded accordingly).

Comment on lines +1109 to +1116
// Sub-component for rendering HTML content safely
export function ArticleContent({ html }: { html: string }) {
return (
<div
className="prose-article-inner"
dangerouslySetInnerHTML={{ __html: html }}
/>
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all call sites of ArticleContent
rg -n "ArticleContent" --max-count=20

Repository: mrmps/SMRY

Length of output: 1263


🏁 Script executed:

#!/bin/bash
# Check if DOMPurify is already in the project dependencies
fd -e package.json -x cat {} \; | head -100

Repository: mrmps/SMRY

Length of output: 36


🏁 Script executed:

#!/bin/bash
# Get more context around the ArticleContent component definition
sed -n '1100,1130p' components/features/article-reader.tsx

Repository: mrmps/SMRY

Length of output: 811


🏁 Script executed:

#!/bin/bash
# Search for where html prop is being processed or sanitized in the file
rg -n "sanitize|DOMPurify|xss|innerHTML" components/features/article-reader.tsx

Repository: mrmps/SMRY

Length of output: 36


🏁 Script executed:

#!/bin/bash
# Check if ArticleContent from article-reader.tsx is exported or used elsewhere
rg -n "from.*article-reader|import.*ArticleContent.*from" --max-count=20

Repository: mrmps/SMRY

Length of output: 127


🏁 Script executed:

#!/bin/bash
# Check what ArticleReader exports and look for its usage
rg -n "ArticleReader|article-reader" -S --max-count=30

Repository: mrmps/SMRY

Length of output: 267


🏁 Script executed:

#!/bin/bash
# Look for useSanitizedHtml implementation to understand the sanitization approach
rg -n "useSanitizedHtml" -A 5

Repository: mrmps/SMRY

Length of output: 982


🏁 Script executed:

#!/bin/bash
# Check package.json for dependencies
cat package.json | grep -A 50 "dependencies"

Repository: mrmps/SMRY

Length of output: 1637


🏁 Script executed:

#!/bin/bash
# Check what is exported from article-reader.tsx
rg -n "^export" components/features/article-reader.tsx

Repository: mrmps/SMRY

Length of output: 201


🏁 Script executed:

#!/bin/bash
# Search for any imports of ArticleReader
rg -n "ArticleReader" --max-count=50

Repository: mrmps/SMRY

Length of output: 267


🏁 Script executed:

#!/bin/bash
# Get the full ArticleReader function to see if it uses the vulnerable ArticleContent sub-component
sed -n '80,200p' components/features/article-reader.tsx

Repository: mrmps/SMRY

Length of output: 4197


🏁 Script executed:

#!/bin/bash
# Search for any imports of ArticleReader from article-reader.tsx
rg -n "from.*article-reader|import.*ArticleReader"

Repository: mrmps/SMRY

Length of output: 36


🏁 Script executed:

#!/bin/bash
# Search for direct usage of the vulnerable ArticleContent from article-reader
rg -n "ArticleContent.*html|ArticleContent.*from.*article-reader"

Repository: mrmps/SMRY

Length of output: 158


🏁 Script executed:

#!/bin/bash
# Look at where article-reader.tsx is located and what files import from that directory
fd -e tsx -e ts | xargs grep -l "article-reader" 2>/dev/null || echo "No imports found"

Repository: mrmps/SMRY

Length of output: 70


🏁 Script executed:

#!/bin/bash
# Check if article-reader is mentioned in any config or exported from index files
rg -n "article-reader" --max-count=30

Repository: mrmps/SMRY

Length of output: 36


Remove or sanitize the unused ArticleContent sub-component.

This exported component uses dangerouslySetInnerHTML without sanitization, creating an XSS risk. However, no imports or usage of this component were found in the codebase, suggesting it may be dead code. Either remove it entirely, or if it's intended as a public API, sanitize the html prop (DOMPurify is available in dependencies) or rename it to sanitizedHtml to enforce the sanitization contract at call sites. The properly sanitized ArticleContent in components/article/content.tsx provides a working pattern using the useSanitizedHtml hook.

🧰 Tools
🪛 ast-grep (0.40.5)

[warning] 1113-1113: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

🤖 Prompt for AI Agents
In `@components/features/article-reader.tsx` around lines 1109 - 1116, The
exported ArticleContent component currently exposes an unsanitized HTML sink
(ArticleContent({ html })) and should be removed or hardened: either delete the
ArticleContent export if unused, or update it to accept and render sanitized
markup (use the existing useSanitizedHtml hook pattern from
components/article/content.tsx or sanitize html with DOMPurify before passing to
dangerouslySetInnerHTML), or rename the prop to sanitizedHtml to enforce callers
supply already-sanitized content; locate the ArticleContent function and apply
one of these fixes to eliminate the XSS risk.

Comment on lines 252 to 268
// Global paste handler - allows pasting from anywhere on the page
useEffect(() => {
const handlePaste = (e: ClipboardEvent) => {
// If input is already focused, let the default behavior handle it
if (document.activeElement === inputRef.current) return;

const pastedText = e.clipboardData?.getData("text");
if (pastedText) {
setUrl(pastedText.trim());
setUrlError(null);
inputRef.current?.focus();
}
};

document.addEventListener("paste", handlePaste);
return () => document.removeEventListener("paste", handlePaste);
}, []);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Global paste handler hijacks other inputs.

Line 254 only skips when the URL input is focused. If users paste into any other editable field (e.g., the Help search), this handler will overwrite the URL and steal focus.

🛠️ Suggested guard for editable elements
   const handlePaste = (e: ClipboardEvent) => {
-    // If input is already focused, let the default behavior handle it
-    if (document.activeElement === inputRef.current) return;
+    const active = document.activeElement as HTMLElement | null;
+    if (
+      active &&
+      (active === inputRef.current ||
+        active.tagName === "INPUT" ||
+        active.tagName === "TEXTAREA" ||
+        active.isContentEditable)
+    ) {
+      return;
+    }
 
     const pastedText = e.clipboardData?.getData("text");
     if (pastedText) {
       setUrl(pastedText.trim());
       setUrlError(null);
       inputRef.current?.focus();
     }
   };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Global paste handler - allows pasting from anywhere on the page
useEffect(() => {
const handlePaste = (e: ClipboardEvent) => {
// If input is already focused, let the default behavior handle it
if (document.activeElement === inputRef.current) return;
const pastedText = e.clipboardData?.getData("text");
if (pastedText) {
setUrl(pastedText.trim());
setUrlError(null);
inputRef.current?.focus();
}
};
document.addEventListener("paste", handlePaste);
return () => document.removeEventListener("paste", handlePaste);
}, []);
// Global paste handler - allows pasting from anywhere on the page
useEffect(() => {
const handlePaste = (e: ClipboardEvent) => {
const active = document.activeElement as HTMLElement | null;
if (
active &&
(active === inputRef.current ||
active.tagName === "INPUT" ||
active.tagName === "TEXTAREA" ||
active.isContentEditable)
) {
return;
}
const pastedText = e.clipboardData?.getData("text");
if (pastedText) {
setUrl(pastedText.trim());
setUrlError(null);
inputRef.current?.focus();
}
};
document.addEventListener("paste", handlePaste);
return () => document.removeEventListener("paste", handlePaste);
}, []);
🤖 Prompt for AI Agents
In `@components/features/home-content.tsx` around lines 252 - 268, The global
paste handler in the useEffect (handlePaste) incorrectly only skips when
document.activeElement === inputRef.current, so pastes into other editable
elements get hijacked; update handlePaste to early-return if the paste event's
target is an editable element (e.g., event.target is an HTMLElement that is
contentEditable or has tagName INPUT, TEXTAREA, or SELECT, or a
role="textbox"/[contenteditable="true"]) before calling setUrl/setUrlError or
focusing inputRef; keep inputRef checks intact and ensure type-narrowing of the
ClipboardEvent target to HTMLElement when performing these checks.

Comment on lines +352 to +357
<button
onClick={() => setIsVisible(false)}
className="absolute top-2 right-2 p-1 rounded-md text-muted-foreground hover:text-foreground hover:bg-accent/50 transition-colors"
>
<X className="size-3.5" />
</button>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add an accessible label to the dismiss button.

The icon-only close button needs an accessible name for screen readers.

🛠️ Proposed fix
-      <button
+      <button
         onClick={() => setIsVisible(false)}
+        aria-label="Dismiss revisit suggestion"
         className="absolute top-2 right-2 p-1 rounded-md text-muted-foreground hover:text-foreground hover:bg-accent/50 transition-colors"
       >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button
onClick={() => setIsVisible(false)}
className="absolute top-2 right-2 p-1 rounded-md text-muted-foreground hover:text-foreground hover:bg-accent/50 transition-colors"
>
<X className="size-3.5" />
</button>
<button
onClick={() => setIsVisible(false)}
aria-label="Dismiss revisit suggestion"
className="absolute top-2 right-2 p-1 rounded-md text-muted-foreground hover:text-foreground hover:bg-accent/50 transition-colors"
>
<X className="size-3.5" />
</button>
🤖 Prompt for AI Agents
In `@components/pages/history-content.tsx` around lines 352 - 357, The close
button with onClick={() => setIsVisible(false)} that renders the X icon is
missing an accessible name; add an accessible label by including
aria-label="Close" (or aria-label="Dismiss" / aria-label="Close history panel")
on the button or add visually hidden text inside the button so screen readers
can announce its purpose; update the element where the X icon is used to include
this aria-label on the button that calls setIsVisible.

Comment on lines +741 to +777
<div className="flex items-center rounded-lg border bg-card p-0.5">
<button
onClick={() => onViewModeChange("list")}
className={cn(
"p-1.5 rounded-md transition-colors",
viewMode === "list"
? "bg-accent text-foreground"
: "text-muted-foreground hover:text-foreground"
)}
title="List view"
>
<List className="size-4" />
</button>
<button
onClick={() => onViewModeChange("compact")}
className={cn(
"p-1.5 rounded-md transition-colors",
viewMode === "compact"
? "bg-accent text-foreground"
: "text-muted-foreground hover:text-foreground"
)}
title="Compact view"
>
<Grid3X3 className="size-4" />
</button>
<button
onClick={() => onViewModeChange("grid")}
className={cn(
"p-1.5 rounded-md transition-colors",
viewMode === "grid"
? "bg-accent text-foreground"
: "text-muted-foreground hover:text-foreground"
)}
title="Grid view"
>
<LayoutGrid className="size-4" />
</button>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add aria labels (and pressed state) to view-mode icon buttons.

title isn’t reliably announced; add aria-label and aria-pressed.

🛠️ Proposed fix
           <button
             onClick={() => onViewModeChange("list")}
+            aria-label="List view"
+            aria-pressed={viewMode === "list"}
             className={cn(
               "p-1.5 rounded-md transition-colors",
               viewMode === "list"
                 ? "bg-accent text-foreground"
                 : "text-muted-foreground hover:text-foreground"
             )}
             title="List view"
           >
             <List className="size-4" />
           </button>
           <button
             onClick={() => onViewModeChange("compact")}
+            aria-label="Compact view"
+            aria-pressed={viewMode === "compact"}
             className={cn(
               "p-1.5 rounded-md transition-colors",
               viewMode === "compact"
                 ? "bg-accent text-foreground"
                 : "text-muted-foreground hover:text-foreground"
             )}
             title="Compact view"
           >
             <Grid3X3 className="size-4" />
           </button>
           <button
             onClick={() => onViewModeChange("grid")}
+            aria-label="Grid view"
+            aria-pressed={viewMode === "grid"}
             className={cn(
               "p-1.5 rounded-md transition-colors",
               viewMode === "grid"
                 ? "bg-accent text-foreground"
                 : "text-muted-foreground hover:text-foreground"
             )}
             title="Grid view"
           >
             <LayoutGrid className="size-4" />
           </button>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className="flex items-center rounded-lg border bg-card p-0.5">
<button
onClick={() => onViewModeChange("list")}
className={cn(
"p-1.5 rounded-md transition-colors",
viewMode === "list"
? "bg-accent text-foreground"
: "text-muted-foreground hover:text-foreground"
)}
title="List view"
>
<List className="size-4" />
</button>
<button
onClick={() => onViewModeChange("compact")}
className={cn(
"p-1.5 rounded-md transition-colors",
viewMode === "compact"
? "bg-accent text-foreground"
: "text-muted-foreground hover:text-foreground"
)}
title="Compact view"
>
<Grid3X3 className="size-4" />
</button>
<button
onClick={() => onViewModeChange("grid")}
className={cn(
"p-1.5 rounded-md transition-colors",
viewMode === "grid"
? "bg-accent text-foreground"
: "text-muted-foreground hover:text-foreground"
)}
title="Grid view"
>
<LayoutGrid className="size-4" />
</button>
<div className="flex items-center rounded-lg border bg-card p-0.5">
<button
onClick={() => onViewModeChange("list")}
aria-label="List view"
aria-pressed={viewMode === "list"}
className={cn(
"p-1.5 rounded-md transition-colors",
viewMode === "list"
? "bg-accent text-foreground"
: "text-muted-foreground hover:text-foreground"
)}
title="List view"
>
<List className="size-4" />
</button>
<button
onClick={() => onViewModeChange("compact")}
aria-label="Compact view"
aria-pressed={viewMode === "compact"}
className={cn(
"p-1.5 rounded-md transition-colors",
viewMode === "compact"
? "bg-accent text-foreground"
: "text-muted-foreground hover:text-foreground"
)}
title="Compact view"
>
<Grid3X3 className="size-4" />
</button>
<button
onClick={() => onViewModeChange("grid")}
aria-label="Grid view"
aria-pressed={viewMode === "grid"}
className={cn(
"p-1.5 rounded-md transition-colors",
viewMode === "grid"
? "bg-accent text-foreground"
: "text-muted-foreground hover:text-foreground"
)}
title="Grid view"
>
<LayoutGrid className="size-4" />
</button>
</div>
🤖 Prompt for AI Agents
In `@components/pages/history-content.tsx` around lines 741 - 777, The view-mode
icon buttons (buttons that call onViewModeChange("list"/"compact"/"grid") and
reference the viewMode state) lack accessible attributes; update each button to
include an appropriate aria-label (e.g., "List view", "Compact view", "Grid
view") and set aria-pressed to a boolean expression comparing viewMode to the
respective mode (e.g., aria-pressed={viewMode === "list"}) so screen readers
announce the role and current pressed state while keeping the existing title and
onClick behavior.

Comment on lines +140 to +145
"faqSiteSupport": "Was ist, wenn es für meine Lieblingsseite nicht funktioniert?",
"faqSiteSupportAnswer": "Unsere Bypass-Erkennung zeigt Ihnen sofort an, ob ein Artikel vollständig abgerufen wurde. Wenn eine Seite nicht funktioniert, können Sie sie anfordern und wir werden die Unterstützung priorisieren. Mit über 1.000 unterstützten Publikationen funktionieren die meisten großen Seiten hervorragend.",
"faqLegal": "Ist das legal?",
"faqLegalAnswer": "Ja. smry greift über verschiedene legale Methoden auf öffentlich zugängliche Inhalte zu. Wir speichern oder verbreiten keine urheberrechtlich geschützten Inhalte — wir helfen Ihnen einfach, Artikel zu lesen, die Sie gefunden haben.",
"faqValue": "Warum sollte ich zahlen, wenn es kostenlose Alternativen gibt?",
"faqValueAnswer": "Kostenlose Tools sind oft langsam, unzuverlässig oder voller Werbung. smry Pro bietet sofortigen Zugang, Premium-KI-Zusammenfassungen, Bypass-Erkennung und ein sauberes Leseerlebnis. Für 3€/Monat lohnt es sich schon bei einem einzigen Artikel von einer großen Publikation.",
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix brand casing in FAQ value statement.

Use the standard “SMRY” casing to match the rest of the locale.

Suggested fix
-    "faqValueAnswer": "Kostenlose Tools sind oft langsam, unzuverlässig oder voller Werbung. smry Pro bietet sofortigen Zugang, Premium-KI-Zusammenfassungen, Bypass-Erkennung und ein sauberes Leseerlebnis. Für 3€/Monat lohnt es sich schon bei einem einzigen Artikel von einer großen Publikation.",
+    "faqValueAnswer": "Kostenlose Tools sind oft langsam, unzuverlässig oder voller Werbung. SMRY Pro bietet sofortigen Zugang, Premium-KI-Zusammenfassungen, Bypass-Erkennung und ein sauberes Leseerlebnis. Für 3€/Monat lohnt es sich schon bei einem einzigen Artikel von einer großen Publikation.",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"faqSiteSupport": "Was ist, wenn es für meine Lieblingsseite nicht funktioniert?",
"faqSiteSupportAnswer": "Unsere Bypass-Erkennung zeigt Ihnen sofort an, ob ein Artikel vollständig abgerufen wurde. Wenn eine Seite nicht funktioniert, können Sie sie anfordern und wir werden die Unterstützung priorisieren. Mit über 1.000 unterstützten Publikationen funktionieren die meisten großen Seiten hervorragend.",
"faqLegal": "Ist das legal?",
"faqLegalAnswer": "Ja. smry greift über verschiedene legale Methoden auf öffentlich zugängliche Inhalte zu. Wir speichern oder verbreiten keine urheberrechtlich geschützten Inhalte — wir helfen Ihnen einfach, Artikel zu lesen, die Sie gefunden haben.",
"faqValue": "Warum sollte ich zahlen, wenn es kostenlose Alternativen gibt?",
"faqValueAnswer": "Kostenlose Tools sind oft langsam, unzuverlässig oder voller Werbung. smry Pro bietet sofortigen Zugang, Premium-KI-Zusammenfassungen, Bypass-Erkennung und ein sauberes Leseerlebnis. Für 3€/Monat lohnt es sich schon bei einem einzigen Artikel von einer großen Publikation.",
"faqSiteSupport": "Was ist, wenn es für meine Lieblingsseite nicht funktioniert?",
"faqSiteSupportAnswer": "Unsere Bypass-Erkennung zeigt Ihnen sofort an, ob ein Artikel vollständig abgerufen wurde. Wenn eine Seite nicht funktioniert, können Sie sie anfordern und wir werden die Unterstützung priorisieren. Mit über 1.000 unterstützten Publikationen funktionieren die meisten großen Seiten hervorragend.",
"faqLegal": "Ist das legal?",
"faqLegalAnswer": "Ja. smry greift über verschiedene legale Methoden auf öffentlich zugängliche Inhalte zu. Wir speichern oder verbreiten keine urheberrechtlich geschützten Inhalte — wir helfen Ihnen einfach, Artikel zu lesen, die Sie gefunden haben.",
"faqValue": "Warum sollte ich zahlen, wenn es kostenlose Alternativen gibt?",
"faqValueAnswer": "Kostenlose Tools sind oft langsam, unzuverlässig oder voller Werbung. SMRY Pro bietet sofortigen Zugang, Premium-KI-Zusammenfassungen, Bypass-Erkennung und ein sauberes Leseerlebnis. Für 3€/Monat lohnt es sich schon bei einem einzigen Artikel von einer großen Publikation.",
🤖 Prompt for AI Agents
In `@messages/de.json` around lines 140 - 145, Update the brand casing in the FAQ
value copy: replace the lowercase "smry Pro" with the standard "SMRY Pro" (and
any other "smry" occurrences in that same string) in the JSON value for the
"faqValueAnswer" key so it matches the rest of the locale; verify "faqValue"
label uses "SMRY" casing too if present elsewhere.

Comment on lines +140 to +145
"faqSiteSupport": "如果我喜欢的网站不支持怎么办?",
"faqSiteSupportAnswer": "我们的绕过检测会立即告诉您文章是否已完全获取。如果某个网站不工作,您可以提出请求,我们会优先添加支持。目前已支持1000多个出版物,大多数主要网站都运行良好。",
"faqLegal": "这合法吗?",
"faqLegalAnswer": "是的。smry通过各种合法方法访问公开可用的内容。我们不存储或重新分发受版权保护的内容 - 我们只是帮助您阅读您找到的文章。",
"faqValue": "既然有免费替代品,为什么要付费?",
"faqValueAnswer": "免费工具通常速度慢、不可靠或充满广告。smry Pro为您提供即时访问、优质AI摘要、绕过检测和清爽的阅读体验。每月仅需3美元,一篇来自主要出版物的文章就能值回票价。",
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use consistent brand casing (“SMRY”) in new FAQ answers.
New strings use lowercase “smry,” which conflicts with the established “SMRY” branding elsewhere.

💬 Suggested wording tweak
-    "faqLegalAnswer": "是的。smry通过各种合法方法访问公开可用的内容。我们不存储或重新分发受版权保护的内容 - 我们只是帮助您阅读您找到的文章。",
+    "faqLegalAnswer": "是的。SMRY通过各种合法方法访问公开可用的内容。我们不存储或重新分发受版权保护的内容 - 我们只是帮助您阅读您找到的文章。",
-    "faqValueAnswer": "免费工具通常速度慢、不可靠或充满广告。smry Pro为您提供即时访问、优质AI摘要、绕过检测和清爽的阅读体验。每月仅需3美元,一篇来自主要出版物的文章就能值回票价。",
+    "faqValueAnswer": "免费工具通常速度慢、不可靠或充满广告。SMRY Pro为您提供即时访问、优质AI摘要、绕过检测和清爽的阅读体验。每月仅需3美元,一篇来自主要出版物的文章就能值回票价。",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"faqSiteSupport": "如果我喜欢的网站不支持怎么办?",
"faqSiteSupportAnswer": "我们的绕过检测会立即告诉您文章是否已完全获取。如果某个网站不工作,您可以提出请求,我们会优先添加支持。目前已支持1000多个出版物,大多数主要网站都运行良好。",
"faqLegal": "这合法吗?",
"faqLegalAnswer": "是的。smry通过各种合法方法访问公开可用的内容。我们不存储或重新分发受版权保护的内容 - 我们只是帮助您阅读您找到的文章。",
"faqValue": "既然有免费替代品,为什么要付费?",
"faqValueAnswer": "免费工具通常速度慢、不可靠或充满广告。smry Pro为您提供即时访问、优质AI摘要、绕过检测和清爽的阅读体验。每月仅需3美元,一篇来自主要出版物的文章就能值回票价。",
"faqSiteSupport": "如果我喜欢的网站不支持怎么办?",
"faqSiteSupportAnswer": "我们的绕过检测会立即告诉您文章是否已完全获取。如果某个网站不工作,您可以提出请求,我们会优先添加支持。目前已支持1000多个出版物,大多数主要网站都运行良好。",
"faqLegal": "这合法吗?",
"faqLegalAnswer": "是的。SMRY通过各种合法方法访问公开可用的内容。我们不存储或重新分发受版权保护的内容 - 我们只是帮助您阅读您找到的文章。",
"faqValue": "既然有免费替代品,为什么要付费?",
"faqValueAnswer": "免费工具通常速度慢、不可靠或充满广告。SMRY Pro为您提供即时访问、优质AI摘要、绕过检测和清爽的阅读体验。每月仅需3美元,一篇来自主要出版物的文章就能值回票价。",
🤖 Prompt for AI Agents
In `@messages/zh.json` around lines 140 - 145, The FAQ strings use lowercase
"smry" instead of the branded "SMRY"; update the message values for the affected
keys (e.g., "faqLegalAnswer" and "faqValueAnswer" — and any other new/edited FAQ
keys in this diff such as "faqSiteSupportAnswer" if present) to use "SMRY"
casing consistently throughout the text while preserving the rest of each
sentence and punctuation.

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

No files reviewed, no comments

Edit Code Review Agent Settings | Greptile

Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 2 new potential issues.

View issues and 28 additional flags in Devin Review.

Open in Devin Review

Comment on lines 384 to 407
</div>
<div className="flex flex-col">
<span className="text-sm font-medium text-foreground/90">{t("feature1Title")}</span>
<span className="text-xs text-muted-foreground/60">{t("feature1Desc")}</span>
</div>
</div>

<div className="flex items-center gap-2.5">
<div className="flex size-8 items-center justify-center rounded-lg bg-black/[0.04] dark:bg-white/[0.06]">
<Eye className="size-4 text-muted-foreground/70" strokeWidth={1.5} />
</div>
<div className="flex flex-col">
<span className="text-sm font-medium text-foreground/90">{t("feature2Title")}</span>
<span className="text-xs text-muted-foreground/60">{t("feature2Desc")}</span>
</div>
</div>

<div className="flex items-center gap-2.5">
<div className="flex size-8 items-center justify-center rounded-lg bg-black/[0.04] dark:bg-white/[0.06]">
<Languages className="size-4 text-muted-foreground/70" strokeWidth={1.5} />
</div>
<div className="flex flex-col">
<span className="text-sm font-medium text-foreground/90">{t("feature3Title")}</span>
<span className="text-xs text-muted-foreground/60">{t("feature3Desc")}</span>

Choose a reason for hiding this comment

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

🔴 Home page references non-existent i18n keys (feature1Title/Desc etc.), causing runtime translation errors

HomeContent renders three feature highlight blocks using translation keys that do not exist in the locale message files.

Actual: useTranslations("home") will throw (or at minimum render missing-message placeholders depending on next-intl configuration) when calling t("feature1Title"), t("feature1Desc"), t("feature2Title"), t("feature2Desc"), t("feature3Title"), t("feature3Desc").

Expected: All referenced translation keys should exist for every supported locale.

Click to expand

In components/features/home-content.tsx, the new UI calls:

<span>{t("feature1Title")}</span>
<span>{t("feature1Desc")}</span>
...

(components/features/home-content.tsx:384-407)

A repo-wide search of messages/*.json finds no occurrences of these keys.

Impact: The home page can fail to render for users (especially in production where missing keys are treated as errors), breaking the primary entry point.

Recommendation: Add the missing home.feature{1,2,3}{Title,Desc} keys to all locale JSON files, or change the component to use existing keys. If these are meant to be static English strings, remove t(...) and hardcode (or add them only to en and provide fallbacks explicitly).

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines 253 to 267
useEffect(() => {
const handlePaste = (e: ClipboardEvent) => {
// If input is already focused, let the default behavior handle it
if (document.activeElement === inputRef.current) return;

const pastedText = e.clipboardData?.getData("text");
if (pastedText) {
setUrl(pastedText.trim());
setUrlError(null);
inputRef.current?.focus();
}
};

document.addEventListener("paste", handlePaste);
return () => document.removeEventListener("paste", handlePaste);

Choose a reason for hiding this comment

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

🟡 Global paste handler hijacks paste events from other inputs (e.g., help popover search), unexpectedly overwriting URL state

HomeContent installs a document-level paste listener that triggers whenever the URL input is not focused, and then sets the url state and focuses the URL input.

Actual: If the user is focused in any other editable control on the page (e.g., the help popover search input in BottomCornerNav), pasting will both (1) paste into that focused field (default browser behavior) and (2) also update url + steal focus to the main URL input.

Expected: The global handler should only activate when the user is not currently typing into another input/textarea/contenteditable element.

Click to expand

Handler:

if (document.activeElement === inputRef.current) return;
...
setUrl(pastedText.trim());
inputRef.current?.focus();

(components/features/home-content.tsx:253-263)

This check only exempts the main URL input, not other inputs.

Impact: Confusing UX; user cannot reliably paste into other fields on the home page.

Recommendation: In the paste handler, also return early when document.activeElement is an input, textarea, or has isContentEditable set. Optionally call e.preventDefault() only when you intentionally consume the paste.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

- Add 22+ new translation keys for home page visual demo cards
  (source fetching, clean mode, AI summary components)
- Add 50+ new translation keys for pricing page
  (billing toggles, feature comparison, stats, guarantees)
- Update home-content.tsx to pass translations to sub-components
- Update bookmarklet.tsx and bottom-corner-nav.tsx with translations
- Fix pricing-content.tsx to use t.rich() for highlight markup
- Add translations for all 6 languages (en, de, es, nl, pt, zh)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

1 file reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

<span className="text-[11px] font-semibold uppercase tracking-wider text-primary">
Revisit this
</span>
<Sparkles className="size-3 text-primary" />
Copy link

Choose a reason for hiding this comment

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

Use BookMarked, TrendingUp, or Crown instead per AGENTS.md

Suggested change
<Sparkles className="size-3 text-primary" />
<BookMarked className="size-3 text-primary" />

Context Used: Context from dashboard - AGENTS.md (source)

Uses cookie-based locale switching to preserve URL state.
- Desktop: Language menu in More options dropdown
- Mobile: Language section in Settings drawer
- Sets NEXT_LOCALE cookie + router.refresh() instead of navigation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
router.refresh() does not trigger middleware to re-read the cookie.
window.location.reload() ensures the new NEXT_LOCALE cookie is picked up.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (8)
components/pages/pricing-content.tsx (1)

135-145: Checkout button has inconsistent styling compared to other variants.

The button inside CheckoutButton (line 141-143) is styled via the .checkout-btn-primary CSS class, which differs from baseStyles and lacks the interactiveStyles applied to other buttons:

  • Border-radius: 0.5rem (CSS) vs rounded-xl (~1rem in baseStyles)
  • Hover effect: opacity: 0.9 vs hover:bg-foreground/90
  • Missing interactive states: no active:scale-[0.98] or focus-visible ring styling from interactiveStyles

This creates visual inconsistency when comparing the checkout button (used for non-pro signed-in users) with the manage subscription button (line 130) and sign-out button (line 150-155), which both use baseStyles and interactiveStyles.

messages/nl.json (1)

145-145: **Duplicate key startFreeTrial in pricing object.**The pricing object contains duplicate startFreeTrial keys (line 145 and line 180). "When the names within an object are not unique, the behavior of software that receives such an object is unpredictable. Many implementations report the last name/value pair only." This means the translation at line 145 ("Start 7 dagen gratis proefperiode") is silently overwritten by line 180 ("Start gratis proefperiode").

Rename one of the keys to avoid losing the translation.

🛠️ Recommended fix
     "startFreeTrial": "Start 7 dagen gratis proefperiode",
     ...
-    "startFreeTrial": "Start gratis proefperiode",
+    "startFreeTrialShort": "Start gratis proefperiode",

Then update the component code to use the appropriate key for each context.

messages/en.json (1)

145-145: Duplicate key startFreeTrial in pricing object.

Same issue as other locale files: the pricing object contains startFreeTrial at both line 145 ("Start my 7-day free trial") and line 180 ("Start My Free Trial"). The first value is silently overwritten.

🛠️ Recommended fix
     "startFreeTrial": "Start my 7-day free trial",
     ...
-    "startFreeTrial": "Start My Free Trial",
+    "startFreeTrialShort": "Start My Free Trial",
messages/es.json (1)

145-145: Duplicate key startFreeTrial in pricing object.

Same issue: duplicate startFreeTrial keys at lines 145 and 180. The first translation is lost.

🛠️ Recommended fix
     "startFreeTrial": "Iniciar prueba gratis de 7 días",
     ...
-    "startFreeTrial": "Comenzar prueba gratis",
+    "startFreeTrialShort": "Comenzar prueba gratis",
messages/de.json (1)

145-145: Duplicate key startFreeTrial in pricing object.

Same issue: duplicate startFreeTrial keys at lines 145 and 180. The first translation is lost.

🛠️ Recommended fix
     "startFreeTrial": "7 Tage kostenlos testen",
     ...
-    "startFreeTrial": "Kostenlos testen",
+    "startFreeTrialShort": "Kostenlos testen",
messages/zh.json (1)

145-182: Duplicate startFreeTrial key overrides the earlier value.

JSON keeps only the last value, so the earlier translation is ignored. Please rename one key (and update references across locales) to preserve both strings.

🔧 Example adjustment (rename the later key)
-    "startFreeTrial": "开始免费试用",
+    "startFreeTrialCta": "开始免费试用",
messages/pt.json (1)

145-182: Duplicate startFreeTrial key overrides the earlier value.

Only the last value is kept, so one of the two strings is ignored. Please rename one key (and keep it consistent across locales) to preserve both.

🔧 Example adjustment (rename the later key)
-    "startFreeTrial": "Iniciar teste grátis",
+    "startFreeTrialCta": "Iniciar teste grátis",
components/marketing/faq.tsx (1)

42-55: Fix code snippet rendering for a8Option1.

The t() function interpolates {code} immediately, replacing it with its value. Splitting on the literal {code} string finds no match, leaving a single-element array, so the <code> wrapper never renders. Split on the actual code value instead (or use t.rich() for rich-text formatting).

🐛 Suggested fix (split on the interpolated value)
-              {t("a8Option1", {
-                code: "http://smry.ai/",
-                example: "http://smry.ai/https://www.wsj.com/..."
-              }).split("{code}").map((part, i) =>
-                i === 0 ? part : (
-                  <span key={i}>
-                    <code className="rounded bg-foreground/5 px-1.5 py-0.5 font-mono text-xs">
-                      http://smry.ai/
-                    </code>
-                    {part}
-                  </span>
-                )
-              )}
+              {(() => {
+                const code = "http://smry.ai/";
+                const example = "http://smry.ai/https://www.wsj.com/...";
+                return t("a8Option1", { code, example }).split(code).map((part, i) =>
+                  i === 0 ? part : (
+                    <span key={i}>
+                      <code className="rounded bg-foreground/5 px-1.5 py-0.5 font-mono text-xs">
+                        {code}
+                      </code>
+                      {part}
+                    </span>
+                  )
+                );
+              })()}
🤖 Fix all issues with AI agents
In `@components/marketing/bookmarklet.tsx`:
- Around line 23-31: The drag handler handleDragStart currently sets an invalid
text/plain fallback ("SMRY"); change it to provide the actual bookmarklet URL so
native bookmark targets receive a valid URL. In handleDragStart, replace the
text/plain payload value with the bookmarklet variable (the same string used for
"text/uri-list") and keep the existing text/html and effectAllowed behavior so
web and native drop targets both get a usable URL.

In `@components/marketing/upgrade-cta.tsx`:
- Around line 27-50: Replace hardcoded white-based color tokens with theme-aware
classes using Tailwind's dark: and light-friendly utilities in the Upgrade CTA
component: change background/ring/text classes like "bg-gradient-to-r
from-white/[0.03] to-white/[0.05]", "text-white/60", and "text-white/30" to
pairs that provide appropriate light-mode equivalents (e.g., light-mode text and
bg classes plus corresponding dark:... overrides) so the element renders
correctly in both themes; also replace the inline English strings ("Know which
sources worked", "Upgrade") by calling the already-imported useTranslations hook
(useTranslations(...)) and using t(...) for those labels, leaving the dismiss
handler (setIsDismissed) and Link/Image usage unchanged.

In `@components/pages/pricing-content.tsx`:
- Around line 247-270: The countdown timer block is stale and unnecessary:
remove or disable the useEffect that defines calculateTimeLeft, the _timerRef
ref, and the interval setup/cleanup (symbols: _timerRef, calculateTimeLeft,
useEffect) so the component no longer sets a 60s interval for an
unused/past-deadline timer; alternatively, if you prefer to keep it for future
use, comment out these lines or gate the effect behind a feature flag/prop so it
doesn't run in production.

In `@components/shared/bottom-corner-nav.tsx`:
- Around line 223-255: WhatsNewItem currently always renders an "a" tag for
hrefs which loses locale for internal routes; change rendering to use the
locale-aware Link component from "@/i18n/navigation" for internal hrefs (e.g.
hrefs that start with "/" and are not absolute/external) and only use a normal
"a" tag for external URLs (absolute URLs like "http(s)://"). Update the Wrapper
selection logic in WhatsNewItem to choose Link when internal, pass href and
className to it, and keep target="_blank" and rel="noopener noreferrer" only for
external anchors; ensure the ArrowUpRight icon and isNew dot rendering remain
unchanged.
- Around line 114-123: The ARIA labels for the Language and Help buttons are
hardcoded in English; use the existing localization hook to replace them by
calling useTranslations("nav") and passing the translated strings to the
aria-label props: update the PopoverTrigger (language button) to
aria-label={t("language")} and update the Help button (the other clickable
element currently using "Help") to aria-label={t("help")}; ensure
useTranslations("nav") is imported/available in the component and that the
variable name (e.g., t) matches existing usage in this file.

In `@lib/changelog.ts`:
- Around line 105-112: The getRecentChanges function should guard against
non-positive limits by returning an empty array immediately when the incoming
limit is <= 0; update getRecentChanges(limit: number = 3) to check the limit
variable at the start (before iterating over changelog) and return [] for
non‑positive values so that passing 0 or negative numbers doesn’t yield
unexpected results while preserving the existing behavior for positive limits
and the default of 3.

In `@messages/pt.json`:
- Around line 169-172: Update the brand casing in the translated strings to use
"SMRY" and "SMRY Pro" consistently: change the values for "faqLegalAnswer"
(replace "smry" with "SMRY") and "faqValueAnswer" (replace "smry Pro" with "SMRY
Pro" and adjust surrounding text casing if needed) so all occurrences use the
branded uppercase form "SMRY" and "SMRY Pro". Ensure punctuation and spacing
remain correct after the replacements.

In `@scripts/analyze-sources.ts`:
- Around line 9-22: The env-loading block uses .env unconditionally and will
crash if missing and also overwrites existing vars; update the logic around
projectRoot/envFile/envContent to first prefer .env.local and fall back to .env,
check file existence (or wrap Bun.file(...).text() in a try/catch) before
reading, and when parsing each line only assign to process.env[key] if
process.env[key] is undefined to avoid clobbering existing environment values;
refer to the variables projectRoot, envFile, envContent and the parsing loop
(trimmed / key / valueParts) to locate and replace the current implementation.

In `@scripts/source-analysis-queries.sql`:
- Around line 47-48: The percentage expressions (e.g., round(count() / (SELECT
count() FROM url_outcomes) * 100, 2) as percentage and other similar
calculations) can divide by zero; wrap each denominator in NULLIF(denominator,
0) so zero becomes NULL and the division returns NULL instead of error. Locate
the percentage calculations referencing aggregated counts or subqueries (for
example the SELECT count() FROM url_outcomes subquery and other denominators
used for percentage aliases) and replace each plain denominator with NULLIF(...,
0) inside the division before multiplying by 100 and rounding.
♻️ Duplicate comments (4)
components/features/home-content.tsx (1)

285-302: Global paste handler still hijacks other inputs.

The guard at line 289 only checks inputRef.current. Pasting into any other editable field (e.g., help popover search) will still overwrite the URL and steal focus.

🛠️ Recommended fix
   const handlePaste = (e: ClipboardEvent) => {
-    // If input is already focused, let the default behavior handle it
-    if (document.activeElement === inputRef.current) return;
+    const active = document.activeElement as HTMLElement | null;
+    if (
+      active &&
+      (active === inputRef.current ||
+        active.tagName === "INPUT" ||
+        active.tagName === "TEXTAREA" ||
+        active.isContentEditable)
+    ) {
+      return;
+    }

     const pastedText = e.clipboardData?.getData("text");
messages/de.json (1)

170-172: Inconsistent brand casing: "smry" should be "SMRY".

Lines 170 and 172 use lowercase "smry" instead of the standard "SMRY" brand casing used elsewhere in the locale.

🛠️ Recommended fix
-    "faqLegalAnswer": "Ja. smry greift über verschiedene legale Methoden auf öffentlich zugängliche Inhalte zu...",
+    "faqLegalAnswer": "Ja. SMRY greift über verschiedene legale Methoden auf öffentlich zugängliche Inhalte zu...",
...
-    "faqValueAnswer": "Kostenlose Tools sind oft langsam, unzuverlässig oder voller Werbung. smry Pro bietet...",
+    "faqValueAnswer": "Kostenlose Tools sind oft langsam, unzuverlässig oder voller Werbung. SMRY Pro bietet...",
messages/zh.json (1)

169-172: Brand casing still uses “smry” in the new FAQ answers. Please keep it consistent with “SMRY/SMRY Pro.”

💬 Suggested wording tweak
-    "faqLegalAnswer": "是的。smry通过各种合法方法访问公开可用的内容。我们不存储或重新分发受版权保护的内容 - 我们只是帮助您阅读您找到的文章。",
+    "faqLegalAnswer": "是的。SMRY通过各种合法方法访问公开可用的内容。我们不存储或重新分发受版权保护的内容 - 我们只是帮助您阅读您找到的文章。",
-    "faqValueAnswer": "免费工具通常速度慢、不可靠或充满广告。smry Pro为您提供即时访问、优质AI摘要、绕过检测和清爽的阅读体验。每月仅需3美元,一篇来自主要出版物的文章就能值回票价。",
+    "faqValueAnswer": "免费工具通常速度慢、不可靠或充满广告。SMRY Pro为您提供即时访问、优质AI摘要、绕过检测和清爽的阅读体验。每月仅需3美元,一篇来自主要出版物的文章就能值回票价。",
components/shared/bottom-corner-nav.tsx (1)

352-353: Use the translated theme label for search filtering.

The hardcoded "theme" string only works in English. Since query is already lowercased, compare it against the translated label like other menu items do.

🔧 Suggested fix
-  const showTheme = !query || "theme".includes(query);
+  const themeLabel = t("theme").toLowerCase();
+  const showTheme = !query || themeLabel.includes(query);
🧹 Nitpick comments (9)
components/features/share-button.tsx (2)

69-69: Consider removing redundant variable assignment.

finalUrl is now a direct alias for url with no transformation. You could use url directly throughout the component, or keep this as an intentional extension point for future URL processing.


284-290: Remove unused props viewMode and sidebarOpen.

These props are passed to ShareContent but the component doesn't destructure or use them (see lines 62-65). The types (ShareContentProps extending ShareButtonDataProps) still include these fields, creating a mismatch between the type contract and actual implementation.

Consider cleaning up by either:

  1. Removing these props from being passed here and from ShareModalContent's signature
  2. Updating ShareButtonDataProps to exclude them if they're no longer needed
♻️ Proposed fix
 const ShareModalContent = React.memo(function ShareModalContent({
   articleTitle,
   url,
   source,
-  viewMode,
-  sidebarOpen,
   onClose,
-}: ShareButtonDataProps & { onClose: () => void }) {
+}: Pick<ShareButtonDataProps, 'url' | 'articleTitle' | 'source'> & { onClose: () => void }) {

And update the ShareContent usage:

           <ShareContent
             url={url}
             articleTitle={articleTitle}
             source={source}
-            viewMode={viewMode}
-            sidebarOpen={sidebarOpen}
             onActionComplete={onClose}
           />
components/pages/pricing-content.tsx (1)

566-599: Derive savings value from actual data to avoid maintenance burden.

The hardcoded value 57 at line 596 should match the sum of competitor prices ($17 + $20 + $10 + $10 = $57). If the prices in lines 567-571 are updated, this value could become inconsistent.

♻️ Suggested refactor
+            const competitorPrices = [
+              { pub: "New York Times", price: 17 },
+              { pub: "Wall Street Journal", price: 20 },
+              { pub: "The Atlantic", price: 10 },
+              { pub: "Washington Post", price: 10 },
+            ];
+            const totalCompetitorCost = competitorPrices.reduce((sum, item) => sum + item.price, 0);

             {/* Comparison rows */}
             <div className="space-y-2 mb-4">
-              {[
-                { pub: "New York Times", price: "$17" },
-                { pub: "Wall Street Journal", price: "$20" },
-                { pub: "The Atlantic", price: "$10" },
-                { pub: "Washington Post", price: "$10" },
-              ].map((item) => (
+              {competitorPrices.map((item) => (
                 <div
                   key={item.pub}
                   className="flex items-center justify-between h-10 px-4 rounded-xl bg-black/[0.03] dark:bg-white/[0.03]"
                 >
                   <span className="text-[14px] text-muted-foreground">{item.pub}</span>
-                  <span className="text-[14px] text-muted-foreground/50 line-through">{item.price}/mo</span>
+                  <span className="text-[14px] text-muted-foreground/50 line-through">${item.price}/mo</span>
                 </div>
               ))}
             </div>
             ...
             <p className="text-center text-[13px] text-muted-foreground mt-4">
               {t.rich("savingsCallout", {
-                savings: 57 - (billingPeriod === "annual" ? annualMonthly : monthlyPrice),
+                savings: totalCompetitorCost - (billingPeriod === "annual" ? annualMonthly : monthlyPrice),
                 highlight: (chunks) => <span className="text-foreground font-medium">{chunks}</span>
               })}
             </p>
app/opengraph-image.tsx (1)

10-32: Consider aligning styling with twitter-image.tsx for brand consistency.

This OpenGraph image uses a minimal design (plain text on dark background), while app/twitter-image.tsx loads custom Inter fonts and renders an elaborate layout with gradients, badges, and radial glows. When both images appear on social platforms, the visual disparity may dilute brand identity.

If the simplicity is intentional, consider adding a brief comment explaining the design choice. Otherwise, consider:

  • Loading the same Inter fonts for consistent typography
  • Matching the visual hierarchy (headline styling, decorative elements)
scripts/analyze-sources-api.sh (1)

16-43: Add error handling for API failures.

The script silently suppresses curl errors (-s flag) and pipes directly to jq without checking if the request succeeded. If the API returns an error, non-JSON response, or the request times out, the script will fail cryptically or produce unexpected output.

♻️ Proposed fix with error handling
-curl -s "$API_URL/api/admin?range=7d" \
-  -H "Authorization: Bearer $ADMIN_SECRET" \
-  | jq '
+response=$(curl -sf --max-time 30 "$API_URL/api/admin?range=7d" \
+  -H "Authorization: Bearer $ADMIN_SECRET") || {
+  echo "Error: Failed to fetch analytics from $API_URL (HTTP error or timeout)" >&2
+  exit 1
+}
+
+echo "$response" | jq '

The -f flag causes curl to return a non-zero exit code on HTTP errors, and --max-time prevents indefinite hangs.

app/layout.tsx (1)

6-13: Consider grouping import statements before variable declarations.

The syne font configuration (lines 9-13) is placed between import statements. While functional, grouping all imports at the top improves readability.

♻️ Suggested reordering
 import { ReactScan } from "@/components/shared/react-scan";
 import { ReactGrab } from "@/components/shared/react-grab";
 import type { Metadata } from "next";
 import { GeistSans } from "geist/font/sans";
 import { Syne } from "next/font/google";
 import "./globals.css";
-
-const syne = Syne({
-  subsets: ["latin"],
-  variable: "--font-syne",
-  display: "swap",
-});
 import { NuqsAdapter } from 'nuqs/adapters/next/app'
 import { GoogleAnalytics } from '@next/third-parties/google'
 import { QueryProvider } from "@/components/shared/query-provider";
 import { ThemeProvider } from "@/components/theme-provider";
 import { ClerkProvider } from "@clerk/nextjs";
 import { getLocale } from 'next-intl/server';
+
+const syne = Syne({
+  subsets: ["latin"],
+  variable: "--font-syne",
+  display: "swap",
+});
components/marketing/upgrade-cta.tsx (1)

9-9: Remove unused import.

useTranslations is imported but never used in this component.

♻️ Proposed fix
-import { useTranslations } from "next-intl";
app/twitter-image.tsx (1)

12-35: Hoist static constants out of Image() for clarity and tiny perf wins.

The font URLs and content types are immutable; moving them to module scope avoids reallocation per request and keeps the handler focused.

♻️ Proposed refactor
+const CONTENT_TYPES = [
+  "Paywalled News",
+  "Research Papers",
+  "Academic Journals",
+  "Long-form Articles",
+];
+const INTER_SEMIBOLD_URL = new URL(
+  "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuGKYAZ9hiJ-Ek-_EeA.woff2"
+);
+const INTER_MEDIUM_URL = new URL(
+  "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuI6fAZ9hiJ-Ek-_EeA.woff2"
+);
+
 export default async function Image() {
-  const interSemiBold = fetch(
-    new URL("https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuGKYAZ9hiJ-Ek-_EeA.woff2")
-  ).then((res) => res.arrayBuffer());
+  const interSemiBold = fetch(INTER_SEMIBOLD_URL).then((res) => res.arrayBuffer());

-  const interMedium = fetch(
-    new URL("https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuI6fAZ9hiJ-Ek-_EeA.woff2")
-  ).then((res) => res.arrayBuffer());
+  const interMedium = fetch(INTER_MEDIUM_URL).then((res) => res.arrayBuffer());

-  const contentTypes = [
-    "Paywalled News",
-    "Research Papers",
-    "Academic Journals",
-    "Long-form Articles",
-  ];
+  // use CONTENT_TYPES
...
-            {contentTypes.map((type) => (
+            {CONTENT_TYPES.map((type) => (

Also applies to: 174-197

scripts/analyze-sources.ts (1)

37-377: Ensure ClickHouse client closes on errors.

If any query throws, client.close() is skipped. Wrapping the body in try/finally keeps connections tidy.

🧹 Suggested refactor
 async function main() {
-  console.log("=== Source Effectiveness Analysis ===\n");
-
-  // 1. Overall source success rates
-  console.log("1. OVERALL SOURCE SUCCESS RATES (last 7 days)");
-  console.log("-".repeat(60));
-  const sourceRates = await query<{
+  try {
+    console.log("=== Source Effectiveness Analysis ===\n");
+
+    // 1. Overall source success rates
+    console.log("1. OVERALL SOURCE SUCCESS RATES (last 7 days)");
+    console.log("-".repeat(60));
+    const sourceRates = await query<{
       source: string;
       total: string;
       successes: string;
       success_rate: string;
-  }>(`
+    }>(`
       SELECT
         source,
         count() as total,
         countIf(outcome = 'success') as successes,
         round(countIf(outcome = 'success') / count() * 100, 2) as success_rate
       FROM request_events
       WHERE timestamp > now() - INTERVAL 7 DAY
         AND source != ''
         AND endpoint = '/api/article'
       GROUP BY source
       ORDER BY total DESC
-  `);
-  console.table(sourceRates);
+    `);
+    console.table(sourceRates);
 
-  // ... rest of analysis
-  await client.close();
-  console.log("\n=== Analysis Complete ===");
+    // ... rest of analysis
+    console.log("\n=== Analysis Complete ===");
+  } finally {
+    await client.close();
+  }
 }

Comment on lines +23 to +31
const handleDragStart = (event: DragEvent<HTMLAnchorElement>) => {
event.dataTransfer.setData("text/uri-list", bookmarklet);
event.dataTransfer.setData("text/plain", "SMRY");
event.dataTransfer.setData(
"text/html",
`<a href="${bookmarklet}" title="SMRY">SMRY</a>`
);
event.dataTransfer.effectAllowed = "copyLink";

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n components/marketing/bookmarklet.tsx | head -50

Repository: mrmps/SMRY

Length of output: 2034


🌐 Web query:

browser drag drop bookmarks bar MIME types text/plain text/uri-list priority

💡 Result:

For browser drag-and-drop (including dragging a link/bookmark to a bookmarks bar or into a webpage drop target), the conventional “URL payload” types are:

  • Primary: text/uri-list (the URL(s); multiple URLs separated by CRLF; # lines are comments) [1]
  • Fallback: text/plain (typically the URL as plain text) — add this last so it’s not preferred over more specific types [1]

Priority/order rule (when you set data): set “most specific → least specific”, with text/plain last [1].

What you may observe from browsers (when reading dragged link data):

  • When dragging an actual <a> element, browsers commonly include text/uri-list and text/plain, but ordering can differ; e.g. Chrome may list text/plain before text/uri-list even though text/uri-list is the more specific type [1].
  • DataTransfer.types is the list of available formats (MIME-like strings such as text/plain, text/html, plus Files when files are present) [2].
  • getData("URL") is treated as text/uri-list (and returns the first URL in the list) [1].

Practical drop-target handling: Prefer text/uri-list if present; otherwise fall back to text/plain [1][2].

Sources: [1] MDN “Recommended drag types” (HTML Drag and Drop API) https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Recommended_drag_types, [2] MDN “DataTransfer.types” https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/types


🌐 Web query:

Safari Firefox bookmarks bar drag drop text/plain URL handling

💡 Result:

For dragging something that should be accepted as a bookmark drop (e.g., onto Safari Favorites/Bookmarks bar or Firefox Bookmarks Toolbar), you generally need to provide a URL flavor, not just arbitrary text:

  • Use text/uri-list (primary) + text/plain (fallback), both containing the URL, and set text/plain last. This is the recommended, cross-browser pattern for “dragging links”. [1][2]
    const dt = e.dataTransfer;
    dt.setData("text/uri-list", url);
    dt.setData("text/plain", url); // fallback, set last

Firefox specifics (why titles often become the URL)

  • When Firefox drags an actual <a> link, it includes Firefox-only types like text/x-moz-url (URL + title separated by a newline) in addition to text/uri-list and text/plain. [2]
  • If you only supply text/plain/text/uri-list, Firefox may create a bookmark whose title is the URL (common complaint when dragging the address-bar text instead of the tab/link). Mozilla’s guidance is to drag the tab or link (which carries title metadata), not the URL text. [3]

Safari / WebKit notes

  • WebKit allows pages to write/read text/plain, text/html, and text/uri-list to/from the system pasteboard via DataTransfer during drag/copy, with some security heuristics around file URLs. [4]
  • Practically: Safari will accept a drop as a URL when text/uri-list (and/or text/plain) contains a valid http/https URL; it won’t necessarily use any “title” you try to embed unless the drop target recognizes it.

If you’re trying to make Firefox save a custom title when dropped onto the bookmarks toolbar

  • There isn’t a reliable, standard web DnD way to force the native bookmarks UI to use your provided title if it doesn’t honor it; Firefox’s bookmark UI behavior varies by what was dragged (tab/link vs plain URL text). This is exactly what extension authors run into. [5]

Sources: [1] MDN Recommended drag types (links) • [2] MDN “Working with the drag data store” (default link drag types, including text/x-moz-url) • [3] Mozilla Support thread on URL-drag creating URL-as-title bookmarks • [4] WebKit blog on Clipboard/DataTransfer support • [5] Stack Overflow discussion from an extension author hitting this behavior.


Ensure drag data provides a valid URL for text/plain fallback.

Some browsers fall back to text/plain when text/uri-list is unavailable or when dropping into native bookmarks bars. Setting it to "SMRY" creates an invalid bookmark in those cases. The text/html payload only helps web drop targets, not native bookmarks UI. Use the bookmarklet URL for text/plain to ensure consistency across browsers (Safari, Firefox, Chrome).

🔧 Suggested fix
-    event.dataTransfer.setData("text/plain", "SMRY");
+    event.dataTransfer.setData("text/plain", bookmarklet);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleDragStart = (event: DragEvent<HTMLAnchorElement>) => {
event.dataTransfer.setData("text/uri-list", bookmarklet);
event.dataTransfer.setData("text/plain", "SMRY");
event.dataTransfer.setData(
"text/html",
`<a href="${bookmarklet}" title="SMRY">SMRY</a>`
);
event.dataTransfer.effectAllowed = "copyLink";
const handleDragStart = (event: DragEvent<HTMLAnchorElement>) => {
event.dataTransfer.setData("text/uri-list", bookmarklet);
event.dataTransfer.setData("text/plain", bookmarklet);
event.dataTransfer.setData(
"text/html",
`<a href="${bookmarklet}" title="SMRY">SMRY</a>`
);
event.dataTransfer.effectAllowed = "copyLink";
🤖 Prompt for AI Agents
In `@components/marketing/bookmarklet.tsx` around lines 23 - 31, The drag handler
handleDragStart currently sets an invalid text/plain fallback ("SMRY"); change
it to provide the actual bookmarklet URL so native bookmark targets receive a
valid URL. In handleDragStart, replace the text/plain payload value with the
bookmarklet variable (the same string used for "text/uri-list") and keep the
existing text/html and effectAllowed behavior so web and native drop targets
both get a usable URL.

Comment on lines +247 to +270
// Countdown timer to Feb 15 (currently unused - keeping for future use)
const _timerRef = useRef<NodeJS.Timeout | undefined>(undefined);

useEffect(() => {
const calculateTimeLeft = () => {
const deadline = new Date("2025-02-15T23:59:59");
const now = new Date();
const diff = deadline.getTime() - now.getTime();

if (diff > 0) {
// Timer values available but not currently displayed
const _days = Math.floor(diff / (1000 * 60 * 60 * 24));
const _hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const _minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
void [_days, _hours, _minutes]; // Suppress unused warnings
}
};

calculateTimeLeft();
_timerRef.current = setInterval(calculateTimeLeft, 60000);
return () => {
if (_timerRef.current) clearInterval(_timerRef.current);
};
}, []);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove or disable unused countdown timer with past deadline.

This countdown timer targets Feb 15, 2025, which has already passed. The timer runs every 60 seconds but produces no visible output—this wastes resources for no benefit. Consider removing this code entirely or commenting it out until it's actually needed.

🧹 Suggested cleanup
-  // Countdown timer to Feb 15 (currently unused - keeping for future use)
-  const _timerRef = useRef<NodeJS.Timeout | undefined>(undefined);
-
-  useEffect(() => {
-    const calculateTimeLeft = () => {
-      const deadline = new Date("2025-02-15T23:59:59");
-      const now = new Date();
-      const diff = deadline.getTime() - now.getTime();
-
-      if (diff > 0) {
-        // Timer values available but not currently displayed
-        const _days = Math.floor(diff / (1000 * 60 * 60 * 24));
-        const _hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
-        const _minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
-        void [_days, _hours, _minutes]; // Suppress unused warnings
-      }
-    };
-
-    calculateTimeLeft();
-    _timerRef.current = setInterval(calculateTimeLeft, 60000);
-    return () => {
-      if (_timerRef.current) clearInterval(_timerRef.current);
-    };
-  }, []);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Countdown timer to Feb 15 (currently unused - keeping for future use)
const _timerRef = useRef<NodeJS.Timeout | undefined>(undefined);
useEffect(() => {
const calculateTimeLeft = () => {
const deadline = new Date("2025-02-15T23:59:59");
const now = new Date();
const diff = deadline.getTime() - now.getTime();
if (diff > 0) {
// Timer values available but not currently displayed
const _days = Math.floor(diff / (1000 * 60 * 60 * 24));
const _hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const _minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
void [_days, _hours, _minutes]; // Suppress unused warnings
}
};
calculateTimeLeft();
_timerRef.current = setInterval(calculateTimeLeft, 60000);
return () => {
if (_timerRef.current) clearInterval(_timerRef.current);
};
}, []);
🤖 Prompt for AI Agents
In `@components/pages/pricing-content.tsx` around lines 247 - 270, The countdown
timer block is stale and unnecessary: remove or disable the useEffect that
defines calculateTimeLeft, the _timerRef ref, and the interval setup/cleanup
(symbols: _timerRef, calculateTimeLeft, useEffect) so the component no longer
sets a 60s interval for an unused/past-deadline timer; alternatively, if you
prefer to keep it for future use, comment out these lines or gate the effect
behind a feature flag/prop so it doesn't run in production.

Comment on lines +114 to +123
<Popover>
<PopoverTrigger
className={cn(
"flex items-center justify-center size-8 rounded-full",
"bg-muted border border-border",
"text-muted-foreground hover:text-foreground hover:bg-accent",
"shadow-sm transition-all duration-150"
)}
aria-label="Language"
>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n components/shared/bottom-corner-nav.tsx | head -150

Repository: mrmps/SMRY

Length of output: 5571


🏁 Script executed:

cat -n components/shared/bottom-corner-nav.tsx | sed -n '460,490p'

Repository: mrmps/SMRY

Length of output: 1374


🏁 Script executed:

head -50 components/shared/bottom-corner-nav.tsx

Repository: mrmps/SMRY

Length of output: 1418


🏁 Script executed:

rg "useTranslations" --type tsx --type ts -A 2 | head -40

Repository: mrmps/SMRY

Length of output: 81


🏁 Script executed:

find . -name "*translation*" -o -name "*locales*" -o -name "*i18n*" | grep -E "\.(json|ts|tsx)$"

Repository: mrmps/SMRY

Length of output: 225


🏁 Script executed:

find . -path ./node_modules -prune -o -type f \( -name "*.json" -o -name "*.ts" \) -print | xargs grep -l "translation\|i18n\|messages" | head -20

Repository: mrmps/SMRY

Length of output: 290


🏁 Script executed:

find . -path ./node_modules -prune -o -type d -name "messages" -o -type d -name "locales" | head -20

Repository: mrmps/SMRY

Length of output: 79


🏁 Script executed:

find . -path ./node_modules -prune -o -type f -name "*.json" | grep -E "(en|pt|de|zh|es|nl)" | head -20

Repository: mrmps/SMRY

Length of output: 200


🏁 Script executed:

git ls-files | grep -E "(message|locale|translation)" | head -30

Repository: mrmps/SMRY

Length of output: 444


🏁 Script executed:

cat -n i18n/routing.ts 2>/dev/null || cat -n src/i18n/routing.ts 2>/dev/null || find . -name "routing.ts" -path "*/i18n/*" -exec cat -n {} \;

Repository: mrmps/SMRY

Length of output: 766


🏁 Script executed:

rg "useTranslations\(" --type ts -B 2 -A 5 | head -80

Repository: mrmps/SMRY

Length of output: 5364


🏁 Script executed:

rg 'useTranslations\("nav"\)' --type ts

Repository: mrmps/SMRY

Length of output: 278


🏁 Script executed:

find . -path ./node_modules -prune -o -type f -name "en.json" -o -type f -name "messages.json" 2>/dev/null

Repository: mrmps/SMRY

Length of output: 87


🏁 Script executed:

git ls-files | grep -E "\.json$" | grep -v node_modules | head -30

Repository: mrmps/SMRY

Length of output: 276


🏁 Script executed:

cat messages/en.json | python3 -m json.tool | grep -A 20 '"nav"'

Repository: mrmps/SMRY

Length of output: 140


🏁 Script executed:

cat messages/en.json | python3 -m json.tool

Repository: mrmps/SMRY

Length of output: 140


🏁 Script executed:

rg '"nav"' messages/en.json -A 30

Repository: mrmps/SMRY

Length of output: 1088


🏁 Script executed:

wc -l messages/en.json

Repository: mrmps/SMRY

Length of output: 74


🏁 Script executed:

grep -o '"nav"' messages/en.json

Repository: mrmps/SMRY

Length of output: 59


🏁 Script executed:

sed -n '/"nav"/,/^  },/p' messages/en.json | head -50

Repository: mrmps/SMRY

Length of output: 721


Localize ARIA labels for the Language and Help buttons.

ARIA labels at lines 122 and 484 are hardcoded in English. Screen-reader text should be translated to support all locales. The "nav" namespace already contains the required keys ("language" and "help"), and other components in this file already follow this pattern with useTranslations("nav").

🔧 Suggested tweak
 function LanguagePopover() {
+  const t = useTranslations("nav");
   const locale = useLocale() as Locale;
   const rawPathname = usePathname();
@@
       <PopoverTrigger
@@
-        aria-label="Language"
+        aria-label={t("language")}
       >
@@
 export function BottomCornerNav() {
+  const t = useTranslations("nav");
   const isClient = useIsClient();
@@
         <PopoverTrigger
@@
-          aria-label="Help"
+          aria-label={t("help")}
         >
🤖 Prompt for AI Agents
In `@components/shared/bottom-corner-nav.tsx` around lines 114 - 123, The ARIA
labels for the Language and Help buttons are hardcoded in English; use the
existing localization hook to replace them by calling useTranslations("nav") and
passing the translated strings to the aria-label props: update the
PopoverTrigger (language button) to aria-label={t("language")} and update the
Help button (the other clickable element currently using "Help") to
aria-label={t("help")}; ensure useTranslations("nav") is imported/available in
the component and that the variable name (e.g., t) matches existing usage in
this file.

Comment on lines +223 to +255
function WhatsNewItem({
children,
href,
isNew,
}: {
children: React.ReactNode;
href?: string;
isNew?: boolean;
}) {
const Wrapper = href ? "a" : "div";
const wrapperProps = href
? { href, target: "_blank", rel: "noopener noreferrer" }
: {};

return (
<Wrapper
{...wrapperProps}
className={cn(
"flex items-start gap-2 py-1 text-[13px]",
href && "hover:text-foreground cursor-pointer"
)}
>
<div
className={cn(
"mt-[7px] size-[5px] rounded-full shrink-0",
isNew ? "bg-blue-400" : "bg-muted-foreground/50"
)}
/>
<span className="text-muted-foreground leading-snug">{children}</span>
{href && <ArrowUpRight className="size-3 text-muted-foreground/50 mt-0.5 shrink-0" />}
</Wrapper>
);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's understand the file structure and check for locale-related files
git ls-files | grep -E "(i18n|locale|lang)" | head -20

Repository: mrmps/SMRY

Length of output: 454


🏁 Script executed:

# Look at the imports and context in the bottom-corner-nav file
head -50 components/shared/bottom-corner-nav.tsx

Repository: mrmps/SMRY

Length of output: 1418


🏁 Script executed:

# Search for Link component imports and usage
rg "from.*Link" components/shared/bottom-corner-nav.tsx -A 2

Repository: mrmps/SMRY

Length of output: 36


🏁 Script executed:

# Search for any Link component in the codebase
fd -t f "link" --extension tsx --extension ts components/ | head -10

Repository: mrmps/SMRY

Length of output: 90


🏁 Script executed:

# Look for locale-aware routing patterns
rg "useRouter|usePathname" components/ -t tsx -l | head -10

Repository: mrmps/SMRY

Length of output: 81


🏁 Script executed:

# Search for WhatsNewItem usage in the file
rg "WhatsNewItem" components/shared/bottom-corner-nav.tsx -B 2 -A 2

Repository: mrmps/SMRY

Length of output: 326


🏁 Script executed:

# Look at the i18n/navigation.ts to understand the Link component
cat -n i18n/navigation.ts

Repository: mrmps/SMRY

Length of output: 283


🏁 Script executed:

# Search for where WhatsNewItem is called with internal paths
rg "WhatsNewItem" components/shared/bottom-corner-nav.tsx -B 5 -A 5 | grep -E "(WhatsNewItem|href|changelog|guide)"

Repository: mrmps/SMRY

Length of output: 192


🏁 Script executed:

# Look at the full WhatsNewItem implementation and its usage context
sed -n '223,300p' components/shared/bottom-corner-nav.tsx

Repository: mrmps/SMRY

Length of output: 2417


🏁 Script executed:

# Find where filteredWhatsNew is defined
rg "filteredWhatsNew" components/shared/bottom-corner-nav.tsx -B 10 -A 2

Repository: mrmps/SMRY

Length of output: 1045


🏁 Script executed:

# Look for the getRecentChanges function that's imported
rg "getRecentChanges" components/shared/bottom-corner-nav.tsx -B 3 -A 3

Repository: mrmps/SMRY

Length of output: 542


🏁 Script executed:

# Find the actual data/URLs being passed
sed -n '150,250p' components/shared/bottom-corner-nav.tsx | grep -E "(getRecentChanges|filteredWhatsNew|href|changelog|guide)"

Repository: mrmps/SMRY

Length of output: 458


🏁 Script executed:

# Check what getRecentChanges returns
cat -n lib/changelog.ts 2>/dev/null || cat -n lib/changelog.js 2>/dev/null || echo "File not found, searching..."

Repository: mrmps/SMRY

Length of output: 3762


🏁 Script executed:

# Search for changelog file
fd changelog lib/ -t f

Repository: mrmps/SMRY

Length of output: 70


🏁 Script executed:

# Check if there are other internal paths being passed to WhatsNewItem
rg "href.*/" components/shared/bottom-corner-nav.tsx | grep -v "http"

Repository: mrmps/SMRY

Length of output: 480


🏁 Script executed:

# Verify the routing configuration
cat i18n/routing.ts

Repository: mrmps/SMRY

Length of output: 633


🏁 Script executed:

# Check if there's any locale stripping happening currently
rg "stripLocaleFromPathname" components/shared/bottom-corner-nav.tsx -B 3 -A 3

Repository: mrmps/SMRY

Length of output: 493


Use locale-aware Link for internal "What's new" items.

WhatsNewItem currently renders all hrefs with <a> tags, causing internal links like /changelog to drop the locale prefix. A Spanish user at /es/* navigating to /changelog will lose their locale context and be redirected to the English version. Use the locale-aware Link component (already imported from @/i18n/navigation) for internal URLs and reserve <a> tags for external links.

✅ Suggested fix (internal vs external)
-function WhatsNewItem({
-  children,
-  href,
-  isNew,
-}: {
-  children: React.ReactNode;
-  href?: string;
-  isNew?: boolean;
-}) {
-  const Wrapper = href ? "a" : "div";
-  const wrapperProps = href
-    ? { href, target: "_blank", rel: "noopener noreferrer" }
-    : {};
-
-  return (
-    <Wrapper
-      {...wrapperProps}
-      className={cn(
-        "flex items-start gap-2 py-1 text-[13px]",
-        href && "hover:text-foreground cursor-pointer"
-      )}
-    >
-      ...
-    </Wrapper>
-  );
-}
+function WhatsNewItem({
+  children,
+  href,
+  isNew,
+}: {
+  children: React.ReactNode;
+  href?: string;
+  isNew?: boolean;
+}) {
+  if (href) {
+    const isExternal = /^https?:\/\//.test(href);
+    const Wrapper = isExternal ? "a" : Link;
+    const wrapperProps = isExternal
+      ? { href, target: "_blank", rel: "noopener noreferrer" }
+      : { href };
+
+    return (
+      <Wrapper
+        {...wrapperProps}
+        className={cn(
+          "flex items-start gap-2 py-1 text-[13px]",
+          "hover:text-foreground cursor-pointer"
+        )}
+      >
+        ...
+      </Wrapper>
+    );
+  }
+
+  return (
+    <div className="flex items-start gap-2 py-1 text-[13px]">
+      ...
+    </div>
+  );
+}
🤖 Prompt for AI Agents
In `@components/shared/bottom-corner-nav.tsx` around lines 223 - 255, WhatsNewItem
currently always renders an "a" tag for hrefs which loses locale for internal
routes; change rendering to use the locale-aware Link component from
"@/i18n/navigation" for internal hrefs (e.g. hrefs that start with "/" and are
not absolute/external) and only use a normal "a" tag for external URLs (absolute
URLs like "http(s)://"). Update the Wrapper selection logic in WhatsNewItem to
choose Link when internal, pass href and className to it, and keep
target="_blank" and rel="noopener noreferrer" only for external anchors; ensure
the ArrowUpRight icon and isNew dot rendering remain unchanged.

Comment on lines +105 to +112
export function getRecentChanges(limit: number = 3): Change[] {
const allChanges: Change[] = [];
for (const entry of changelog) {
for (const change of entry.changes) {
allChanges.push(change);
if (allChanges.length >= limit) {
return allChanges;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Guard getRecentChanges against non‑positive limits.

Right now, limit = 0 still returns the first change. An early return avoids unexpected results.

🐛 Suggested fix
 export function getRecentChanges(limit: number = 3): Change[] {
+  if (limit <= 0) return [];
   const allChanges: Change[] = [];
   for (const entry of changelog) {
     for (const change of entry.changes) {
       allChanges.push(change);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function getRecentChanges(limit: number = 3): Change[] {
const allChanges: Change[] = [];
for (const entry of changelog) {
for (const change of entry.changes) {
allChanges.push(change);
if (allChanges.length >= limit) {
return allChanges;
}
export function getRecentChanges(limit: number = 3): Change[] {
if (limit <= 0) return [];
const allChanges: Change[] = [];
for (const entry of changelog) {
for (const change of entry.changes) {
allChanges.push(change);
if (allChanges.length >= limit) {
return allChanges;
}
🤖 Prompt for AI Agents
In `@lib/changelog.ts` around lines 105 - 112, The getRecentChanges function
should guard against non-positive limits by returning an empty array immediately
when the incoming limit is <= 0; update getRecentChanges(limit: number = 3) to
check the limit variable at the start (before iterating over changelog) and
return [] for non‑positive values so that passing 0 or negative numbers doesn’t
yield unexpected results while preserving the existing behavior for positive
limits and the default of 3.

Comment on lines +169 to +172
"faqLegal": "Isso é legal?",
"faqLegalAnswer": "Sim. O smry acessa conteúdo publicamente disponível através de vários métodos legais. Não armazenamos nem redistribuímos conteúdo protegido por direitos autorais — simplesmente ajudamos você a ler artigos que encontrou.",
"faqValue": "Por que devo pagar quando há alternativas gratuitas?",
"faqValueAnswer": "Ferramentas gratuitas costumam ser lentas, não confiáveis ou cheias de anúncios. O smry Pro oferece acesso instantâneo, resumos de IA premium, detecção de bypass e uma experiência de leitura limpa. Por R$15/mês, se paga com um único artigo de qualquer publicação importante.",
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use consistent brand casing (“SMRY / SMRY Pro”).

These new FAQ answers still use lowercase “smry.”

💬 Suggested wording tweak
-    "faqLegalAnswer": "Sim. O smry acessa conteúdo publicamente disponível através de vários métodos legais. Não armazenamos nem redistribuímos conteúdo protegido por direitos autorais — simplesmente ajudamos você a ler artigos que encontrou.",
+    "faqLegalAnswer": "Sim. O SMRY acessa conteúdo publicamente disponível através de vários métodos legais. Não armazenamos nem redistribuímos conteúdo protegido por direitos autorais — simplesmente ajudamos você a ler artigos que encontrou.",
-    "faqValueAnswer": "Ferramentas gratuitas costumam ser lentas, não confiáveis ou cheias de anúncios. O smry Pro oferece acesso instantâneo, resumos de IA premium, detecção de bypass e uma experiência de leitura limpa. Por R$15/mês, se paga com um único artigo de qualquer publicação importante.",
+    "faqValueAnswer": "Ferramentas gratuitas costumam ser lentas, não confiáveis ou cheias de anúncios. O SMRY Pro oferece acesso instantâneo, resumos de IA premium, detecção de bypass e uma experiência de leitura limpa. Por R$15/mês, se paga com um único artigo de qualquer publicação importante.",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"faqLegal": "Isso é legal?",
"faqLegalAnswer": "Sim. O smry acessa conteúdo publicamente disponível através de vários métodos legais. Não armazenamos nem redistribuímos conteúdo protegido por direitos autorais — simplesmente ajudamos você a ler artigos que encontrou.",
"faqValue": "Por que devo pagar quando há alternativas gratuitas?",
"faqValueAnswer": "Ferramentas gratuitas costumam ser lentas, não confiáveis ou cheias de anúncios. O smry Pro oferece acesso instantâneo, resumos de IA premium, detecção de bypass e uma experiência de leitura limpa. Por R$15/mês, se paga com um único artigo de qualquer publicação importante.",
"faqLegal": "Isso é legal?",
"faqLegalAnswer": "Sim. O SMRY acessa conteúdo publicamente disponível através de vários métodos legais. Não armazenamos nem redistribuímos conteúdo protegido por direitos autorais — simplesmente ajudamos você a ler artigos que encontrou.",
"faqValue": "Por que devo pagar quando há alternativas gratuitas?",
"faqValueAnswer": "Ferramentas gratuitas costumam ser lentas, não confiáveis ou cheias de anúncios. O SMRY Pro oferece acesso instantâneo, resumos de IA premium, detecção de bypass e uma experiência de leitura limpa. Por R$15/mês, se paga com um único artigo de qualquer publicação importante.",
🤖 Prompt for AI Agents
In `@messages/pt.json` around lines 169 - 172, Update the brand casing in the
translated strings to use "SMRY" and "SMRY Pro" consistently: change the values
for "faqLegalAnswer" (replace "smry" with "SMRY") and "faqValueAnswer" (replace
"smry Pro" with "SMRY Pro" and adjust surrounding text casing if needed) so all
occurrences use the branded uppercase form "SMRY" and "SMRY Pro". Ensure
punctuation and spacing remain correct after the replacements.

Comment on lines +9 to +22
// Load .env.local manually for bun
const projectRoot = import.meta.dir.replace("/scripts", "");
const envFile = Bun.file(`${projectRoot}/.env`);
const envContent = await envFile.text();
for (const line of envContent.split("\n")) {
const trimmed = line.trim();
if (trimmed && !trimmed.startsWith("#")) {
const [key, ...valueParts] = trimmed.split("=");
if (key && valueParts.length > 0) {
const value = valueParts.join("=").replace(/^["']|["']$/g, "");
process.env[key] = value;
}
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n scripts/analyze-sources.ts

Repository: mrmps/SMRY

Length of output: 16389


🏁 Script executed:

find . -name ".env*" -o -name "*.env" | head -20

Repository: mrmps/SMRY

Length of output: 68


🏁 Script executed:

# Check for any documentation or config that mentions .env
rg "\.env" --type md --type json --type js --type ts -l | head -20

Repository: mrmps/SMRY

Length of output: 381


🏁 Script executed:

# Check if there's any reference to how env should be set up
rg "\.env\.local|\.env" . --type md -B 2 -A 2 | head -50

Repository: mrmps/SMRY

Length of output: 323


🏁 Script executed:

cat -n lib/env.ts

Repository: mrmps/SMRY

Length of output: 2625


🏁 Script executed:

cat -n server/env.ts

Repository: mrmps/SMRY

Length of output: 1948


🏁 Script executed:

cat -n scripts/check-env.ts

Repository: mrmps/SMRY

Length of output: 1856


🏁 Script executed:

cat -n .env.example

Repository: mrmps/SMRY

Length of output: 1024


🏁 Script executed:

# Check if .env file exists in repo root
ls -la | grep "\.env"

Repository: mrmps/SMRY

Length of output: 118


Fix env file loading to handle .env.local, fallback to .env, and avoid overwriting existing variables.

The code reads only .env (lines 11–22) but the comment says .env.local, and the README instructs users to copy .env.example to .env.local. More critically:

  1. Missing file will crash: Bun.file().text() throws if the file doesn't exist; the script has no error handling.
  2. Ignores .env.local: Should try .env.local first (the documented convention), then fall back to .env.
  3. Overwrites existing env vars: The code unconditionally sets all parsed variables, which can shadow values already set in the environment or CI/CD.

The project's scripts/check-env.ts already shows the correct pattern: check file existence, try .env.local first, fall back to .env, and only set undefined variables.

Suggested fix
-const envFile = Bun.file(`${projectRoot}/.env`);
-const envContent = await envFile.text();
-for (const line of envContent.split("\n")) {
-  const trimmed = line.trim();
-  if (trimmed && !trimmed.startsWith("#")) {
-    const [key, ...valueParts] = trimmed.split("=");
-    if (key && valueParts.length > 0) {
-      const value = valueParts.join("=").replace(/^["']|["']$/g, "");
-      process.env[key] = value;
-    }
-  }
-}
+const envPaths = [`${projectRoot}/.env.local`, `${projectRoot}/.env`];
+for (const envPath of envPaths) {
+  try {
+    const envContent = await Bun.file(envPath).text();
+    for (const line of envContent.split("\n")) {
+      const trimmed = line.trim();
+      if (trimmed && !trimmed.startsWith("#")) {
+        const [key, ...valueParts] = trimmed.split("=");
+        if (key && valueParts.length > 0 && process.env[key] === undefined) {
+          const value = valueParts.join("=").replace(/^["']|["']$/g, "");
+          process.env[key] = value;
+        }
+      }
+    }
+    break;
+  } catch {
+    // ignore missing file and continue
+  }
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Load .env.local manually for bun
const projectRoot = import.meta.dir.replace("/scripts", "");
const envFile = Bun.file(`${projectRoot}/.env`);
const envContent = await envFile.text();
for (const line of envContent.split("\n")) {
const trimmed = line.trim();
if (trimmed && !trimmed.startsWith("#")) {
const [key, ...valueParts] = trimmed.split("=");
if (key && valueParts.length > 0) {
const value = valueParts.join("=").replace(/^["']|["']$/g, "");
process.env[key] = value;
}
}
}
// Load .env.local manually for bun
const projectRoot = import.meta.dir.replace("/scripts", "");
const envPaths = [`${projectRoot}/.env.local`, `${projectRoot}/.env`];
for (const envPath of envPaths) {
try {
const envContent = await Bun.file(envPath).text();
for (const line of envContent.split("\n")) {
const trimmed = line.trim();
if (trimmed && !trimmed.startsWith("#")) {
const [key, ...valueParts] = trimmed.split("=");
if (key && valueParts.length > 0 && process.env[key] === undefined) {
const value = valueParts.join("=").replace(/^["']|["']$/g, "");
process.env[key] = value;
}
}
}
break;
} catch {
// ignore missing file and continue
}
}
🤖 Prompt for AI Agents
In `@scripts/analyze-sources.ts` around lines 9 - 22, The env-loading block uses
.env unconditionally and will crash if missing and also overwrites existing
vars; update the logic around projectRoot/envFile/envContent to first prefer
.env.local and fall back to .env, check file existence (or wrap
Bun.file(...).text() in a try/catch) before reading, and when parsing each line
only assign to process.env[key] if process.env[key] is undefined to avoid
clobbering existing environment values; refer to the variables projectRoot,
envFile, envContent and the parsing loop (trimmed / key / valueParts) to locate
and replace the current implementation.

Comment on lines +47 to +48
round(count() / (SELECT count() FROM url_outcomes) * 100, 2) as percentage
FROM url_outcomes
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, locate the SQL file
find . -name "source-analysis-queries.sql" -type f

# Get the total line count
wc -l scripts/source-analysis-queries.sql 2>/dev/null || echo "File not found at expected path"

Repository: mrmps/SMRY

Length of output: 131


🏁 Script executed:

#!/bin/bash
# Read the SQL file with line numbers
cat -n scripts/source-analysis-queries.sql

Repository: mrmps/SMRY

Length of output: 13583


Add NULLIF protection to all percentage calculations to prevent division by zero errors.

These queries divide by aggregated counts that can be zero when datasets are empty (no rows in the 7-day window, no matched conditions, etc.). In ClickHouse, division by zero throws an error.

Wrap all denominators with NULLIF(denominator, 0) to safely convert zero denominators to NULL, which will result in NULL percentages rather than errors.

Example fix
-  round(count() / (SELECT count() FROM url_outcomes) * 100, 2) as percentage
+  round(count() / NULLIF((SELECT count() FROM url_outcomes), 0) * 100, 2) as percentage

Applies to lines: 47-48, 70-71, 102-103, 171-172, 210-224, 281-282

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
round(count() / (SELECT count() FROM url_outcomes) * 100, 2) as percentage
FROM url_outcomes
round(count() / NULLIF((SELECT count() FROM url_outcomes), 0) * 100, 2) as percentage
FROM url_outcomes
🤖 Prompt for AI Agents
In `@scripts/source-analysis-queries.sql` around lines 47 - 48, The percentage
expressions (e.g., round(count() / (SELECT count() FROM url_outcomes) * 100, 2)
as percentage and other similar calculations) can divide by zero; wrap each
denominator in NULLIF(denominator, 0) so zero becomes NULL and the division
returns NULL instead of error. Locate the percentage calculations referencing
aggregated counts or subqueries (for example the SELECT count() FROM
url_outcomes subquery and other denominators used for percentage aliases) and
replace each plain denominator with NULLIF(..., 0) inside the division before
multiplying by 100 and rounding.

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

2 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Grid3X3,
List,
LayoutGrid,
Sparkles,
Copy link

Choose a reason for hiding this comment

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

Sparkles icon violates design system per AGENTS.md

Suggested change
Sparkles,
BookMarked,

Context Used: Context from dashboard - AGENTS.md (source)

<span className="text-[11px] font-semibold uppercase tracking-wider text-primary">
Revisit this
</span>
<Sparkles className="size-3 text-primary" />
Copy link

Choose a reason for hiding this comment

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

Replace banned Sparkles with allowed icon

Suggested change
<Sparkles className="size-3 text-primary" />
<BookMarked className="size-3 text-primary" />

Context Used: Context from dashboard - AGENTS.md (source)

… params

With localePrefix: as-needed, cookie-based switching does not work.
Instead, navigate to the locale-prefixed URL while preserving all
query parameters (url, source, view, sidebar).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Use useSyncExternalStore for consistent SSR/client rendering.
href and title now use mounted check to avoid server/client mismatch.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@railway-app railway-app bot temporarily deployed to smry / SMRY-pr-45 January 26, 2026 03:22 Destroyed
@railway-app railway-app bot temporarily deployed to smry / SMRY-pr-45 January 26, 2026 03:23 Destroyed
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@components/features/proxy-content.tsx`:
- Around line 254-289: The three icon-only theme buttons (the ones calling
setTheme and rendering Sun, Moon, Laptop) need accessible labels and pressed
state for screen readers: add an appropriate aria-label (e.g., "Light theme",
"Dark theme", "System theme") to each button and add aria-pressed set to the
same boolean logic already used for the visual active class (for light: theme
=== "light" || (theme === "system" && !isDark); for dark: theme === "dark" ||
theme === "magic-blue" || theme === "classic-dark"; for system: theme ===
"system"). Keep the existing onClick handlers and className logic unchanged.

Comment on lines +254 to +289
<button
onClick={() => setTheme("light")}
className={cn(
"flex items-center justify-center rounded-md p-1.5 transition-colors",
theme === "light" || (theme === "system" && !isDark)
? "bg-accent text-accent-foreground"
: "hover:bg-accent/50 text-muted-foreground"
)}
title="Light"
>
<Sun className="size-4" />
</button>
<button
onClick={() => setTheme("dark")}
className={cn(
"flex items-center justify-center rounded-md p-1.5 transition-colors",
theme === "dark" || theme === "magic-blue" || theme === "classic-dark"
? "bg-accent text-accent-foreground"
: "hover:bg-accent/50 text-muted-foreground"
)}
title="Dark"
>
<Moon className="size-4" />
</button>
<button
onClick={() => setTheme("system")}
className={cn(
"flex items-center justify-center rounded-md p-1.5 transition-colors",
theme === "system"
? "bg-accent text-accent-foreground"
: "hover:bg-accent/50 text-muted-foreground"
)}
title="System"
>
<Laptop className="size-4" />
</button>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add ARIA labeling for icon-only theme buttons.

Screen readers need labels and pressed state for the icon-only controls.

Proposed patch
          <button
            onClick={() => setTheme("light")}
            className={cn(
              "flex items-center justify-center rounded-md p-1.5 transition-colors",
              theme === "light" || (theme === "system" && !isDark)
                ? "bg-accent text-accent-foreground"
                : "hover:bg-accent/50 text-muted-foreground"
            )}
            title="Light"
+           aria-label="Light theme"
+           aria-pressed={theme === "light" || (theme === "system" && !isDark)}
          >
            <Sun className="size-4" />
          </button>
          <button
            onClick={() => setTheme("dark")}
            className={cn(
              "flex items-center justify-center rounded-md p-1.5 transition-colors",
              theme === "dark" || theme === "magic-blue" || theme === "classic-dark"
                ? "bg-accent text-accent-foreground"
                : "hover:bg-accent/50 text-muted-foreground"
            )}
            title="Dark"
+           aria-label="Dark theme"
+           aria-pressed={theme === "dark" || theme === "magic-blue" || theme === "classic-dark"}
          >
            <Moon className="size-4" />
          </button>
          <button
            onClick={() => setTheme("system")}
            className={cn(
              "flex items-center justify-center rounded-md p-1.5 transition-colors",
              theme === "system"
                ? "bg-accent text-accent-foreground"
                : "hover:bg-accent/50 text-muted-foreground"
            )}
            title="System"
+           aria-label="System theme"
+           aria-pressed={theme === "system"}
          >
            <Laptop className="size-4" />
          </button>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button
onClick={() => setTheme("light")}
className={cn(
"flex items-center justify-center rounded-md p-1.5 transition-colors",
theme === "light" || (theme === "system" && !isDark)
? "bg-accent text-accent-foreground"
: "hover:bg-accent/50 text-muted-foreground"
)}
title="Light"
>
<Sun className="size-4" />
</button>
<button
onClick={() => setTheme("dark")}
className={cn(
"flex items-center justify-center rounded-md p-1.5 transition-colors",
theme === "dark" || theme === "magic-blue" || theme === "classic-dark"
? "bg-accent text-accent-foreground"
: "hover:bg-accent/50 text-muted-foreground"
)}
title="Dark"
>
<Moon className="size-4" />
</button>
<button
onClick={() => setTheme("system")}
className={cn(
"flex items-center justify-center rounded-md p-1.5 transition-colors",
theme === "system"
? "bg-accent text-accent-foreground"
: "hover:bg-accent/50 text-muted-foreground"
)}
title="System"
>
<Laptop className="size-4" />
</button>
<button
onClick={() => setTheme("light")}
className={cn(
"flex items-center justify-center rounded-md p-1.5 transition-colors",
theme === "light" || (theme === "system" && !isDark)
? "bg-accent text-accent-foreground"
: "hover:bg-accent/50 text-muted-foreground"
)}
title="Light"
aria-label="Light theme"
aria-pressed={theme === "light" || (theme === "system" && !isDark)}
>
<Sun className="size-4" />
</button>
<button
onClick={() => setTheme("dark")}
className={cn(
"flex items-center justify-center rounded-md p-1.5 transition-colors",
theme === "dark" || theme === "magic-blue" || theme === "classic-dark"
? "bg-accent text-accent-foreground"
: "hover:bg-accent/50 text-muted-foreground"
)}
title="Dark"
aria-label="Dark theme"
aria-pressed={theme === "dark" || theme === "magic-blue" || theme === "classic-dark"}
>
<Moon className="size-4" />
</button>
<button
onClick={() => setTheme("system")}
className={cn(
"flex items-center justify-center rounded-md p-1.5 transition-colors",
theme === "system"
? "bg-accent text-accent-foreground"
: "hover:bg-accent/50 text-muted-foreground"
)}
title="System"
aria-label="System theme"
aria-pressed={theme === "system"}
>
<Laptop className="size-4" />
</button>
🤖 Prompt for AI Agents
In `@components/features/proxy-content.tsx` around lines 254 - 289, The three
icon-only theme buttons (the ones calling setTheme and rendering Sun, Moon,
Laptop) need accessible labels and pressed state for screen readers: add an
appropriate aria-label (e.g., "Light theme", "Dark theme", "System theme") to
each button and add aria-pressed set to the same boolean logic already used for
the visual active class (for light: theme === "light" || (theme === "system" &&
!isDark); for dark: theme === "dark" || theme === "magic-blue" || theme ===
"classic-dark"; for system: theme === "system"). Keep the existing onClick
handlers and className logic unchanged.

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

2 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Grid3X3,
List,
LayoutGrid,
Sparkles,
Copy link

Choose a reason for hiding this comment

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

Sparkles icon violates design system per AGENTS.md

Suggested change
Sparkles,
BookMarked,

Context Used: Context from dashboard - AGENTS.md (source)

<span className="text-[11px] font-semibold uppercase tracking-wider text-primary">
Revisit this
</span>
<Sparkles className="size-3 text-primary" />
Copy link

Choose a reason for hiding this comment

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

Use BookMarked, TrendingUp, or Crown instead per AGENTS.md

Suggested change
<Sparkles className="size-3 text-primary" />
<BookMarked className="size-3 text-primary" />

Context Used: Context from dashboard - AGENTS.md (source)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@components/features/proxy-content.tsx`:
- Around line 122-154: HistoryMenuItem can cause a hydration mismatch because it
computes href from isSignedIn immediately; update HistoryMenuItem to mirror
HistoryButton by waiting for mount and auth readiness (use the mounted value
from useSyncExternalStore and isLoaded) before deriving href and showBadge: only
set href to "/history" when mounted && isLoaded && isSignedIn, otherwise use the
fallback "/pricing", and compute showBadge similarly; make this change where
HistoryMenuItem builds href/showBadge (inside the component and before the
MenuItem render) so the MenuItem/Link output matches SSR and client during
hydration.
♻️ Duplicate comments (2)
components/article/content.tsx (1)

198-222: Add keyboard accessibility for expandable images.

The image expand/collapse functionality is click-only, which excludes keyboard users. This was flagged in a previous review but remains unaddressed. Add tabIndex="0", role="button", aria-expanded state tracking, and a keydown handler for Enter/Space keys to make these images accessible.

components/features/proxy-content.tsx (1)

259-294: Add ARIA labeling for icon-only theme buttons.

The theme toggle buttons (Light/Dark/System) only have title attributes but lack aria-label and aria-pressed for screen reader accessibility. This was flagged in a previous review.

🧹 Nitpick comments (3)
components/marketing/upgrade-cta.tsx (1)

38-48: Upgrade button uses fixed white background regardless of theme.

The upgrade button uses hardcoded bg-white text-slate-900, which may be intentional for CTA prominence. However, in light mode with a white page background, the button might lack sufficient contrast against the subtle container background (from-black/[0.03]).

Consider adding a subtle border or dark-mode-specific styling for consistency with the theme-aware approach used elsewhere in this component.

♻️ Optional: Add subtle border for better light-mode visibility
       <Link
         href="/pricing"
         className={cn(
           "shrink-0 rounded-md px-3 py-1 text-[13px] font-medium",
           "bg-white text-slate-900",
-          "shadow-[0_1px_2px_rgba(0,0,0,0.1)]",
+          "shadow-[0_1px_2px_rgba(0,0,0,0.1)] ring-1 ring-black/[0.08]",
           "transition-all hover:shadow-[0_2px_4px_rgba(0,0,0,0.15)] hover:brightness-105"
         )}
       >
components/features/share-button.tsx (1)

39-66: Remove unused props from ShareContent.

viewMode and sidebarOpen are defined in ShareButtonDataProps (Lines 43-44) and passed to ShareContent via ShareModalContent, but ShareContent doesn't use them—it only destructures url, articleTitle, source, and onActionComplete (Lines 61-66). Consider removing these unused props from the interface to reduce API surface.

Suggested cleanup
 interface ShareButtonDataProps {
   url: string;
   articleTitle?: string;
   source?: Source;
-  viewMode?: string;
-  sidebarOpen?: boolean;
 }
app/layout.tsx (1)

6-14: Consider reorganizing imports for clarity.

The Syne font import (Line 6) and its configuration (Lines 9-13) are placed between other imports, breaking the typical import grouping pattern. Consider moving the font configuration after all imports for better readability.

Suggested reorganization
 import { ReactScan } from "@/components/shared/react-scan";
 import { ReactGrab } from "@/components/shared/react-grab";
 import type { Metadata } from "next";
 import { GeistSans } from "geist/font/sans";
 import { Syne } from "next/font/google";
-import "./globals.css";
-
-const syne = Syne({
-  subsets: ["latin"],
-  variable: "--font-syne",
-  display: "swap",
-});
 import { NuqsAdapter } from 'nuqs/adapters/next/app'
 import { GoogleAnalytics } from '@next/third-parties/google'
 import { QueryProvider } from "@/components/shared/query-provider";
 import { ThemeProvider } from "@/components/theme-provider";
 import { ClerkProvider } from "@clerk/nextjs";
 import { getLocale } from 'next-intl/server';
+import "./globals.css";
+
+const syne = Syne({
+  subsets: ["latin"],
+  variable: "--font-syne",
+  display: "swap",
+});

Comment on lines +122 to +154
// History menu item for the More dropdown
function HistoryMenuItem() {
const { isSignedIn, isLoaded } = useAuth();
const mounted = useSyncExternalStore(emptySubscribe, getClientSnapshot, getServerSnapshot);

const href = isSignedIn ? "/history" : "/pricing";
const showBadge = mounted && isLoaded && !isSignedIn;

return (
<MenuItem
render={(props) => {
const { key, className, ...rest } = props as typeof props & {
key?: React.Key;
className?: string;
};
return (
<Link
key={key}
{...rest}
href={href}
className={cn(className, "flex items-center gap-2 w-full px-3")}
>
<HistoryIcon className="size-4" />
<span className="flex-1">History</span>
{showBadge && (
<span className="text-[10px] font-medium text-amber-500">PRO</span>
)}
</Link>
);
}}
/>
);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential hydration mismatch in HistoryMenuItem.

Unlike HistoryButton which guards href and title with mounted && isLoaded && isSignedIn (Line 83-85), HistoryMenuItem derives href directly from isSignedIn (Line 127) without waiting for mount. Since isSignedIn may differ between SSR (undefined/false) and client, the rendered href could mismatch during hydration.

Suggested fix
 function HistoryMenuItem() {
   const { isSignedIn, isLoaded } = useAuth();
   const mounted = useSyncExternalStore(emptySubscribe, getClientSnapshot, getServerSnapshot);
 
-  const href = isSignedIn ? "/history" : "/pricing";
+  const isAuthed = mounted && isLoaded && isSignedIn;
+  const href = isAuthed ? "/history" : "/pricing";
   const showBadge = mounted && isLoaded && !isSignedIn;
🤖 Prompt for AI Agents
In `@components/features/proxy-content.tsx` around lines 122 - 154,
HistoryMenuItem can cause a hydration mismatch because it computes href from
isSignedIn immediately; update HistoryMenuItem to mirror HistoryButton by
waiting for mount and auth readiness (use the mounted value from
useSyncExternalStore and isLoaded) before deriving href and showBadge: only set
href to "/history" when mounted && isLoaded && isSignedIn, otherwise use the
fallback "/pricing", and compute showBadge similarly; make this change where
HistoryMenuItem builds href/showBadge (inside the component and before the
MenuItem render) so the MenuItem/Link output matches SSR and client during
hydration.

@railway-app railway-app bot temporarily deployed to smry / SMRY-pr-45 January 26, 2026 03:27 Destroyed
@mrmps mrmps merged commit 84f5d8f into main Jan 26, 2026
4 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants