diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..068c41d3 --- /dev/null +++ b/AGENTS.md @@ -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__`. + +## 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. diff --git a/src/components/navigation-bar/authed.tsx b/src/components/navigation-bar/authed.tsx index f54f99ec..eb233fd4 100644 --- a/src/components/navigation-bar/authed.tsx +++ b/src/components/navigation-bar/authed.tsx @@ -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(null) - - const toggleDropdown = () => setIsOpen(!isOpen) + const menuId = useId() useEffect(() => { const handleClickOutside = (event: MouseEvent) => { @@ -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 (
-
{trigger}
- - {isOpen && ( + + + {isOpen ? (
- -
{children}
- )} + ) : null}
) } -// Custom Divider component -const Divider = ({ className = '' }: { className?: string }) => ( -
-) +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 ( + + {icon} + {children} + + ) + } + + return ( + + ) +} type LoggedNavigationBarProps = { profile: { @@ -106,132 +140,80 @@ function LoggedNavigationBar(props: LoggedNavigationBarProps) { }, [router, t]) return ( - + {t('app.menu.settings')} + + + } + > + {t('app.menu.loginByQRCode.title')} + + +
+ + } + > + {t('app.menu.logout.title')} + +
+ + + ) } diff --git a/src/components/navigation-bar/avatar.tsx b/src/components/navigation-bar/avatar.tsx index fa8d1d54..ef54315c 100644 --- a/src/components/navigation-bar/avatar.tsx +++ b/src/components/navigation-bar/avatar.tsx @@ -17,8 +17,10 @@ function AvatarOnNavigationBar(props: AvatarOnNavigationBarProps) { return (
avatar
) diff --git a/src/components/navigation-bar/navigation-bar.module.css b/src/components/navigation-bar/navigation-bar.module.css deleted file mode 100644 index d6b159c1..00000000 --- a/src/components/navigation-bar/navigation-bar.module.css +++ /dev/null @@ -1,22 +0,0 @@ -.navbar { -} -.navbar + div { - flex: 1; -} - -.navbar::before { - content: ''; - /* width: calc(90% + 10px); */ - height: 100%; - position: absolute; - top: 0; - left: 0; - z-index: -1; - background-color: rgba(272, 242, 242, 0.5); - /* background-color: blur-background; */ - filter: blur(5px); - transform: scale(1.1); - left: -5px; - top: -5px; - overflow: hidden; -} diff --git a/src/components/navigation-bar/navigation-bar.tsx b/src/components/navigation-bar/navigation-bar.tsx index 2f9851ee..c2034ac4 100644 --- a/src/components/navigation-bar/navigation-bar.tsx +++ b/src/components/navigation-bar/navigation-bar.tsx @@ -14,38 +14,29 @@ import { cn } from '@/lib/utils' import type { ProfileQuery } from '@/schema/generated' import { getUserSlug } from '@/utils/profile.utils' -import LinkIndicator from '../link-indicator' -import { useCtrlP } from '../searchbar/hooks' import SearchBar from '../searchbar/searchbar' import LoggedNavigationBar from './authed' import LoginByQRCode from './login-by-qrcode' -import styles from './navigation-bar.module.css' - const leftMenu = [ { - emoji: () => , + icon: BookOpen, alt: 'read', dest: (id: number | string) => `/dash/${id}/home`, targetSegment: 'home', }, { - emoji: () => , + icon: LayoutGrid, alt: 'square', dest: (id: number | string) => `/dash/${id}/square`, targetSegment: 'square', }, { - emoji: () => , + icon: Upload, alt: 'upload', dest: (id: number | string) => `/dash/${id}/upload`, targetSegment: 'upload', }, - // { - // emoji: () => , - // alt: 'my', - // dest: (id: number | string) => `/dash/${id}/profile`, - // }, ] type NavigationBarProps = { @@ -54,83 +45,105 @@ type NavigationBarProps = { function NavigationBar(props: NavigationBarProps) { const { myProfile: profile } = props - - const { visible, setVisible } = useCtrlP() - const onSearchbarClose = useCallback(() => { - setVisible(false) - }, [setVisible]) - + const { t } = useTranslation(undefined, 'navigation') + const activeSegment = useSelectedLayoutSegment() + const [searchVisible, setSearchVisible] = useState(false) const [loginByQRCodeModalVisible, setLoginByQRCodeModalVisible] = useState(false) - const { t } = useTranslation(undefined, 'navigation') - // const homeLink = getMyHomeLink(profile) - const activeSegment = useSelectedLayoutSegment() + const onSearchbarClose = useCallback(() => { + setSearchVisible(false) + }, []) + + const profileSlug = profile ? getUserSlug(profile) : null + const brandHref = profileSlug ? `/dash/${profileSlug}/home` : '/' return ( - + - + ) }