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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Repository Guidelines

## Project Structure & Module Organization
This repository is a Next.js 16 app using the App Router and TypeScript. Primary code lives in `src/`: routes in `src/app`, reusable UI in `src/components`, server/client integrations in `src/services`, shared hooks in `src/hooks`, and utility helpers in `src/lib` and `src/utils`. GraphQL operations and generated types live under `src/schema` and `src/gql`. Static files are in `public/`, Jest setup and mocks are in `test/`, and parser-focused tests also live beside source in `src/store/clippings/__tests__`.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Next.js 16 has not been released yet (the current version is 15). This is likely a typo for Next.js 14 or 15.


## Build, Test, and Development Commands
Use `pnpm` for all local work.

- `pnpm dev`: run the app locally on port `3101` with Turbo mode.
- `pnpm build`: run GraphQL codegen, then create a production build.
- `pnpm start`: serve the built app with `PORT`.
- `pnpm test`: run the Jest suite in `jest-environment-jsdom`.
- `pnpm test:update`: refresh snapshots, for example `parser.test.ts.snap`.
- `pnpm lint` / `pnpm lint:fix`: check or fix issues with `oxlint`.
- `pnpm format` / `pnpm format:check`: apply or verify formatting with `oxfmt`.
- `pnpm codegen`: regenerate GraphQL client artifacts after schema or `.graphql` changes.

## Coding Style & Naming Conventions
Follow the existing TypeScript + React style: 2-space indentation, single quotes, and semicolons omitted. Prefer PascalCase for React components (`AuthPageShell.tsx`), camelCase for utilities and hooks (`use-screen-size.ts` is an existing exception), and route folders that match URL structure such as `src/app/dash/[userid]/...`. Keep generated files like `src/schema/generated.tsx` and `src/gql/*` out of manual edits.

## Testing Guidelines
Jest collects coverage from `src/**/*.{js,ts,jsx,tsx}` except generated schema output. Name tests `*.test.ts` or place them in `__tests__/`. Keep fast unit tests close to the code when practical, and use `test/setup.ts` plus `test/mocks/` for shared setup. Run `pnpm test` before opening a PR; run `pnpm test:update` only when snapshot changes are intentional.

## Commit & Pull Request Guidelines
Recent history favors Conventional Commit-style messages such as `refactor(auth): extract AuthPageShell...` and `refactor(ui): ...`. Use a clear type/scope prefix like `feat(dash):`, `fix(upload):`, or `refactor(search):`. PRs should explain the user-visible change, note any schema or config updates, link the issue when relevant, and include screenshots for UI work in `src/app` or `src/components`.

## Configuration Tips
Runtime behavior depends on environment variables such as `PORT`, `GIT_COMMIT`, `NEXT_PUBLIC_PP_TOKEN`, and Redis/telemetry values used in deployment. Avoid committing secrets, and document any new env var in the PR description.
304 changes: 143 additions & 161 deletions src/components/navigation-bar/authed.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,28 @@
'use client'

import Tooltip from '@annatarhe/lake-ui/tooltip'
import { LogOut, Search, Settings, Smartphone, User } from 'lucide-react'
import { Ellipsis, LogOut, QrCode, Search, Settings, User } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useCallback, useEffect, useId, useRef, useState } from 'react'
import toast from 'react-hot-toast'

import { checkIsPremium } from '@/compute/user'
import { useTranslation } from '@/i18n/client'
import { cn } from '@/utils/cn'
import { cn } from '@/lib/utils'
import profile from '@/utils/profile'

import LinkIndicator from '../link-indicator'
import AvatarOnNavigationBar from './avatar'
import { onCleanServerCookie } from './logout'

// Custom Dropdown component
type DropdownProps = {
trigger: React.ReactNode
children: React.ReactNode
}

const Dropdown = ({ trigger, children }: DropdownProps) => {
function Dropdown({ children }: DropdownProps) {
const [isOpen, setIsOpen] = useState(false)
const dropdownRef = useRef<HTMLDivElement>(null)

const toggleDropdown = () => setIsOpen(!isOpen)
const menuId = useId()

useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
Expand All @@ -38,48 +34,86 @@ const Dropdown = ({ trigger, children }: DropdownProps) => {
}
}

const handleEscape = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
setIsOpen(false)
}
}

document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
document.addEventListener('keydown', handleEscape)

return () => {
document.removeEventListener('mousedown', handleClickOutside)
document.removeEventListener('keydown', handleEscape)
}
}, [])

return (
<div className="relative" ref={dropdownRef}>
<div onClick={toggleDropdown}>{trigger}</div>

{isOpen && (
<button
type="button"
onClick={() => setIsOpen((current) => !current)}
aria-expanded={isOpen}
aria-controls={menuId}
aria-label="More options"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Hardcoded English "More options" aria-label violates i18n rule

The .windsurfrules mandate: "Supporting english, simplified chinese and korean by default." The aria-label="More options" on the Dropdown trigger button (line 59) is hardcoded in English instead of using a translated string via t(). This means non-English screen-reader users will hear the label in English.

Open in Devin Review

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

className="inline-flex h-8 w-8 items-center justify-center rounded-xl text-slate-600 transition-colors hover:bg-slate-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-500 dark:text-slate-300 dark:hover:bg-slate-800"
>
<Ellipsis className="h-4 w-4" />
</button>

{isOpen ? (
<div
className="absolute right-0 z-50 mt-2 w-72 origin-top-right rounded-xl border border-slate-200 bg-white/90 p-4 shadow-2xl backdrop-blur-xl dark:border-slate-700 dark:bg-slate-900/90"
style={{
transformOrigin: 'top right',
animation: 'scaleIn 200ms ease-out forwards',
}}
id={menuId}
className="absolute right-0 z-50 mt-2 w-52 rounded-xl border border-slate-200 bg-white p-1.5 shadow-lg dark:border-slate-800 dark:bg-slate-950"
>
<style jsx global>{`
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
`}</style>
<div className="absolute -top-2 right-4 h-4 w-4 rotate-45 border-t border-l border-slate-200 bg-white dark:border-slate-700 dark:bg-slate-900"></div>
{children}
</div>
)}
) : null}
</div>
)
}

// Custom Divider component
const Divider = ({ className = '' }: { className?: string }) => (
<hr
className={`border-t border-slate-200 dark:border-slate-700 ${className}`}
/>
)
type MenuItemProps = {
href?: string
onClick?: () => void
tone?: 'default' | 'danger'
icon: React.ReactNode
children: React.ReactNode
}

function MenuItem(props: MenuItemProps) {
const { href, onClick, tone = 'default', icon, children } = props
const className = cn(
'flex w-full items-center gap-2.5 rounded-lg px-2.5 py-2 text-left text-sm transition-colors',
tone === 'danger'
? 'text-red-600 hover:bg-red-50 dark:text-red-300 dark:hover:bg-red-500/10'
: 'text-slate-700 hover:bg-slate-100 dark:text-slate-200 dark:hover:bg-slate-900'
)

const iconClassName = cn(
'shrink-0',
tone === 'danger'
? 'text-red-500 dark:text-red-300'
: 'text-slate-500 dark:text-slate-400'
)

if (href) {
return (
<Link href={href as any} className={className}>
<span className={iconClassName}>{icon}</span>
<span>{children}</span>
</Link>
)
}

return (
<button type="button" className={className} onClick={onClick}>
<span className={iconClassName}>{icon}</span>
<span>{children}</span>
</button>
)
}

type LoggedNavigationBarProps = {
profile: {
Expand All @@ -106,132 +140,80 @@ function LoggedNavigationBar(props: LoggedNavigationBarProps) {
}, [router, t])

return (
<nav aria-label="User navigation">
<ul className="with-slide-in flex items-center gap-4">
{/* Search Button */}
<li>
<button
onClick={onSearch}
className="flex items-center gap-2 rounded-full bg-gradient-to-r from-indigo-500 to-purple-600 px-4 py-2 text-white shadow-lg backdrop-blur-sm transition-all duration-300 hover:from-indigo-600 hover:to-purple-700 hover:shadow-xl"
aria-label={t('app.menu.search.title')}
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-end">
<div className="flex items-center gap-2">
<button
type="button"
onClick={onSearch}
className="inline-flex h-8 items-center gap-2 rounded-xl border border-slate-200 bg-white px-2.5 text-sm font-medium text-slate-700 transition-colors hover:bg-slate-100 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-200 dark:hover:bg-slate-800"
aria-label={t('app.menu.search.title')}
>
<Search className="h-4 w-4" />
<span className="hidden sm:inline">{t('app.menu.search.title')}</span>
</button>

<Tooltip content={t('app.menu.settings')}>
<Link
href={`/dash/${uidOrDomain}/settings/web`}
className="inline-flex h-8 w-8 items-center justify-center rounded-xl border border-slate-200 bg-white text-slate-600 transition-colors hover:bg-slate-100 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-300 dark:hover:bg-slate-800"
aria-label={t('app.menu.settings')}
>
<Search size={18} />
<span>{t('app.menu.search.title')}</span>
</button>
</li>

{/* Settings Button */}
<li>
<Tooltip content={t('app.menu.settings')}>
<Link
<Settings className="h-4 w-4" />
</Link>
</Tooltip>
</div>

<div className="flex items-center gap-2">
<Link
href={`/dash/${uidOrDomain}/profile`}
className="flex min-w-0 items-center gap-2 rounded-lg px-1.5 py-1 transition-colors hover:bg-slate-100 dark:hover:bg-slate-800"
>
<AvatarOnNavigationBar
avatarUrl={profileData.avatar}
size={28}
isPremium={isPremium}
/>
<span className="min-w-0 max-w-28 truncate text-sm font-medium text-slate-900 dark:text-white">
{profileData.name}
</span>
</Link>

<Dropdown>
<div className="flex flex-col gap-1">
<MenuItem
href={`/dash/${uidOrDomain}/profile`}
icon={<User className="h-4 w-4" />}
>
{t('app.menu.my')}
</MenuItem>

<MenuItem
href={`/dash/${uidOrDomain}/settings/web`}
className="flex items-center justify-center rounded-full bg-slate-200 p-2 transition-colors duration-200 hover:bg-slate-300 dark:bg-slate-800 dark:hover:bg-slate-700"
aria-label={t('app.menu.settings')}
icon={<Settings className="h-4 w-4" />}
>
<LinkIndicator>
<Settings
size={20}
className="text-slate-700 dark:text-slate-200"
/>
</LinkIndicator>
</Link>
</Tooltip>
</li>

{/* User Avatar with Dropdown */}
<li>
<Dropdown
trigger={
<button className="relative rounded-full transition-transform hover:scale-105 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-none">
<AvatarOnNavigationBar
avatarUrl={profileData.avatar}
isPremium={isPremium}
/>
</button>
}
>
<div className="flex flex-col gap-2">
{/* User Profile */}
<div className="mb-2 flex items-center gap-3 p-2">
<AvatarOnNavigationBar
avatarUrl={profileData.avatar}
size={40}
isPremium={isPremium}
/>
<div className="flex flex-col gap-4">
<Tooltip content={profileData.name} noWrap>
<h3 className="max-w-48 overflow-hidden text-lg font-bold text-ellipsis dark:text-white">
{profileData.name}
</h3>
</Tooltip>
<Link
href={`/dash/${uidOrDomain}/profile`}
className="text-sm text-indigo-600 hover:underline dark:text-indigo-400 dark:hover:text-indigo-300"
>
{t('app.menu.viewProfile')}
</Link>
</div>
</div>

<Divider className="my-1" />

{/* Menu Items */}
<div className="flex flex-col">
<Link
href={`/dash/${uidOrDomain}/profile`}
className="flex items-center gap-2 rounded-lg p-3 transition-all duration-200 hover:bg-slate-100 dark:text-white dark:hover:bg-slate-800/50"
>
<LinkIndicator>
<User
size={18}
className="text-slate-700 dark:text-slate-200"
/>
</LinkIndicator>
<span>{t('app.menu.my')}</span>
</Link>

<button
className="flex cursor-pointer items-center gap-2 rounded-lg p-3 transition-all duration-200 hover:bg-slate-100 dark:text-white dark:hover:bg-slate-800/50"
onClick={onPhoneLogin}
>
<Smartphone
size={18}
className="text-slate-700 dark:text-slate-200"
/>
<span>{t('app.menu.loginByQRCode.title')}</span>
</button>

<Link
href={`/dash/${uidOrDomain}/settings/web`}
className="flex items-center gap-2 rounded-lg p-3 transition-all duration-200 hover:bg-slate-100 dark:text-white dark:hover:bg-slate-800/50"
>
<LinkIndicator>
<Settings
size={18}
className="text-slate-700 dark:text-slate-200"
/>
</LinkIndicator>
<span>{t('app.menu.settings')}</span>
</Link>

<Divider className="my-2" />

<button
className={cn(
'flex cursor-pointer items-center gap-2 rounded-lg p-3 transition-all duration-200 hover:bg-slate-100 dark:hover:bg-slate-800/50',
'text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20'
)}
onClick={handleLogout}
>
<LogOut size={18} className="text-red-400" />
<span>{t('app.menu.logout.title')}</span>
</button>
</div>
</div>
</Dropdown>
</li>
</ul>
</nav>
{t('app.menu.settings')}
</MenuItem>

<MenuItem
onClick={onPhoneLogin}
icon={<QrCode className="h-4 w-4" />}
>
{t('app.menu.loginByQRCode.title')}
</MenuItem>

<div className="my-1 h-px bg-slate-200 dark:bg-slate-800" />

<MenuItem
onClick={handleLogout}
tone="danger"
icon={<LogOut className="h-4 w-4" />}
>
{t('app.menu.logout.title')}
</MenuItem>
</div>
</Dropdown>
</div>
</div>
)
}

Expand Down
Loading
Loading