How to work in this codebase. Read this before writing code.
For visual / styling work, defer to the uxcore-style skill at .claude/skills/uxcore-style/SKILL.md. It auto-loads in Claude Code; in other agents (Cursor), open it manually whenever you write or modify styles.
- Next.js 15.0.5 — Pages Router (
src/pages/), not App Router - React 19, TypeScript 5.2 (
strict: false,noImplicitAny: false) - Sass + CSS Modules (
*.module.scss). No Tailwind, no CSS-in-JS, no styled-components. - yarn is the package manager. Never use npm.
- No state library — global state lives in custom listener-based hooks (
useGlobals,useUXCoreGlobals,useUXCatGlobals,useUXCGGlobals) andGlobalContext. - No data-fetching library — plain
fetchinsrc/api/. - Cypress 14 for E2E. No unit tests, no Jest, no Vitest.
- i18n is custom — Next.js built-in locales (
en,ru,hy), no next-i18next or react-intl. - SVGs are written manually as
.tsxcomponents insrc/assets/icons/. No@svgr/webpack.
src/
├── pages/ # Next.js routes (Pages Router)
├── components/ # PascalCase dirs, one component per dir
│ ├── _biases/ # bias-feature-only components
│ ├── _uxcp/ # UXCP-feature-only components
│ └── uxcg/ # UXCG-feature-only components
├── layouts/ # page-level layout wrappers (PascalCase dirs)
├── data/ # i18n static content (TS only, never JSON)
├── hooks/ # flat list of custom hooks (use*.ts)
├── lib/ # shared utilities by domain
├── utils/ # additional utilities (same conventions as lib/)
├── api/ # API clients (one file per domain)
├── constants/ # app-wide constants — NOTE: this folder may not exist; see "Path aliases" below
├── local-types/ # shared TS types
├── styles/ # globals.scss, _variables.scss, _animations.scss
└── assets/icons/ # SVG icon components written as .tsx
Underscore / lowercase prefix: Components used by only one feature go in components/_biases/, components/_uxcp/, or components/uxcg/. Shared components stay flat in components/. Convention, not lint-enforced — follow it anyway.
Every component is its own folder with:
ComponentName/
├── index.ts # barrel
├── ComponentName.tsx
└── ComponentName.module.scss
index.ts is always:
import ComponentName from './ComponentName';
export default ComponentName;- Default export only. Named exports break the barrel pattern.
- Props type is
ComponentNamePropsinterface, declared inline above the component. NoT-prefix. No.types.tsfile unless the type is large (10+ fields) or shared across files. - Styles colocated as
ComponentName.module.scss, imported asimport styles from './ComponentName.module.scss'. - Use
cn(classnames) for conditional classes:cn(styles.Button, { [styles.Primary]: variant === 'primary' }). - Sub-components nest with the same structure:
Table/TableSearch/TableSearch.tsx. - Function components only. No class components.
data-cyattributes on interactive elements for Cypress (data-cy="open-close-accordion-button").
import cn from 'classnames';
import styles from './ComponentName.module.scss';
interface ComponentNameProps {
label: string;
variant?: 'primary' | 'secondary';
}
const ComponentName = ({ label, variant = 'primary' }: ComponentNameProps) => {
return (
<div
className={cn(styles.ComponentName, {
[styles.Primary]: variant === 'primary',
})}
data-cy="component-name"
>
{label}
</div>
);
};
export default ComponentName;The codebase has 35 components still using
T-prefix (TButton,TInput,TTooltip, etc.). That is legacy. New components and modifications useComponentNamePropsinterface.
Layouts live in src/layouts/ (PascalCase dirs, same structure as components). They receive data from a page and compose components.
Existing layouts: UXCoreLayout, UXCatLayout, UXCGLayout, UXCPLayout, CoreViewLayout, FolderViewLayout, OngoingLayout, StartTestLayout, TestResult, UserProfile, ApiLayout, CertificateLayout, CalculatingResults, Layout.
The root Layout at src/layouts/Layout.tsx wraps all pages (rendered in _app.tsx). It provides Header, cookie box, and app-wide chrome. Feature layouts nest inside it.
Layouts use ComponentName.types.ts more often than components do, because their props tend to be large page-data shapes.
All static content lives in src/data/ as TypeScript. Never JSON, never MDX.
src/data/featureName/
├── en.ts
├── ru.ts
├── hy.ts # optional — see locales below
└── index.ts
import en from './en';
import hy from './hy';
import ru from './ru';
const locales = { en, ru, hy } as const satisfies {
en: typeof en;
ru: typeof ru;
hy: typeof hy;
};
export default locales;import biasesLocalization from '@data/biases';
const { locale } = useRouter() as TRouter;
const { heading, mainTitle } = biasesLocalization[locale];Three locales: en (default), ru, hy.
Armenian (hy) is treated as an English fallback at the application level. 12 of 44 data directories are missing hy.ts entirely (completionBar, downloadButton, formPopup, fullscreenButton, genderModalData, imageModule, leaderboard, seo, startTest, statistics, uxcgQuestions, uxcoreApi).
Don't require hy.ts in new data dirs — if missing, the runtime falls back to en. If you do create hy.ts, copy from en.ts so the shape matches and add a // TODO HY translation comment.
Two cross-module shims exist (completionBar/index.ts and formPopup/index.ts import hy from unrelated modules). Don't add new shims — just omit hy.ts.
Strapi-fetched data uses field-per-locale columns (titleEn, titleRu, titleHy), not the locale-keyed object pattern. Don't mix the two models.
Use these instead of relative imports past one level:
| Alias | Path |
|---|---|
@components/* |
src/components/* |
@data/* |
src/data/* |
@hooks/* |
src/hooks/* |
@layouts/* |
src/layouts/* |
@lib/* |
src/lib/* |
@api/* |
src/api/* |
@styles/* |
src/styles/* |
@local-types/* |
src/local-types/* |
@icons/* |
src/assets/icons/* |
Mismatch warning:
@constantsappears in the ESLint import-sort config but is not defined intsconfig.json. Do not use@constants/*imports. If you need a constants folder, define the alias intsconfig.jsonfirst.@utilsis in neither — also unavailable.
Import order is auto-fixed by eslint-plugin-simple-import-sort on commit:
side effects → node builtins → third-party → @styles → @constants → @local-types → @hooks → @lib → @api → @data → @icons → @components → @layouts → relative → .scss style imports.
Only 3 deep relative imports exist in the codebase. Don't add more — always use aliases.
- Pages Router only (
src/pages/). Never add App Router files (app/,'use client',next/navigation). - Adding a page: create
src/pages/{path}.tsxorsrc/pages/{path}/index.tsx. The root<Layout>from_app.tsxapplies automatically. - Data fetching: prefer
getStaticPropswith ISR (revalidate: 5orrevalidate: 10). The site is statically generated. Don't introducegetServerSidePropsunless data must be fresh per-request. - Dynamic routes:
[slug].tsxfor biases/UXCG,[userId]for profiles,[name]/[hash]/[isTeamMember]for UXCP personas. UsegetStaticPathswithfallback: 'blocking'. - SEO: use the existing
SeoGeneratorcomponent. Don't addnext-seo.
Anything that touches localStorage, sessionStorage, or window at module top level will break SSR with hydration mismatches. Wrap with dynamic(() => import('...'), { ssr: false }) or move into useEffect.
The codebase uses dynamic(..., { ssr: false }) extensively (18 instances across 10 files, mostly in _app.tsx, UXCoreLayout.tsx, UXCPLayout.tsx, and achievement components). Match those patterns.
Source of truth:
- If the user gives explicit visual instructions (colors, spacing, sizes), follow them exactly.
- If they don't, defer to the uxcore-style skill (
.claude/skills/uxcore-style/SKILL.md) and read it before writing styles. - Do not invent values. Do not copy hardcoded hex / px from neighboring components just because they exist.
The codebase has hardcoded colors, breakpoints, and spacing scattered across SCSS files. That is legacy, not a pattern to extend. New code aligns to the design skill; old code stays as-is until touched.
- CSS Modules + SCSS, colocated per component.
_variables.scsscontains only@font-facedeclarations — no color or spacing tokens._uxcore-labels.scssis the only SCSS partial with reusable color variables (4 bias-category color pairs).- Dark theme: toggled by adding
darkThemeclass todocument.bodyviauseGlobals(). Components apply{ [styles.darkTheme]: isDarkTheme }. - Animations live in
_animations.scssas utility classes (.animate-fadeIn,.animate-slideDown, etc.) and 13 keyframe animations. - Responsive design: per-component media queries in each
.module.scss. Not centralized. - SCSS class naming: PascalCase (
.PreloaderContainer,.Accordion,.Title). The codebase mixes PascalCase and camelCase — prefer PascalCase for new code.
- No Redux, Zustand, Jotai, SWR, React Query. Don't add one.
- Global state hooks:
useGlobals(dark theme, sidebar, fullscreen),useUXCoreGlobals,useUXCatGlobals,useUXCGGlobals. Each returns a[actions, state]tuple. - App-level state: managed in
_app.tsxviauseState, passed throughGlobalContext(fromsrc/components/Context/GlobalContext). - Authentication:
next-authSessionProviderwraps the app.
- Cypress E2E only. Specs in
cypress/e2e/{feature}/. - Naming:
{feature}.cy.ts,{feature}-mobile.cy.ts,{feature}-{locale}.cy.ts, special tests as.spec.cy.ts. - Use existing custom commands from
cypress/support/commands.tsbefore writing new ones:
| Command | Purpose |
|---|---|
loginBySession |
Authenticates test user via POST, stores session cookie |
checkExternalLinks |
Validates external URL format and reachability |
checkAllLinks |
Validates all links across multiple routes |
checkSocialMediaLink |
Validates social link domain/href/target |
checkH1 |
Asserts H1 contains expected text |
validateAllImages |
Validates images return successful status codes |
scrollToSection |
Scrolls via button click, verifies position |
uxcoreSearchBehavior |
Tests UX Core search input + results |
uxcgTestSearchBehavior |
Tests UXCG search with valid/invalid words |
uxcpSearchBehavior |
Tests UXCP search with valid/invalid words |
uxcpAddBiases |
Tests adding/removing biases in UXCP |
openLoginModalByButtonClick |
Opens login modal, closes by clicking outside |
showCopiedTooltip |
Tests copy button → "Copied!" tooltip |
showMoreAndLess |
Tests expand/collapse buttons |
playAudio |
Plays/pauses audio via pyramid play icon |
checkPyramidChange |
Tests pyramid color changes on click |
checkSwiperSlide |
Tests swiper navigation arrows |
clickArrowWhenReady |
Clicks nav arrows when enabled, verifies URL change |
- Don't add Jest, Vitest, or component tests. Not wired up.
- CI only runs
tsc --noEmiton PRs — Cypress is not gated. Run E2E locally before pushing.
- Components / files / folders: PascalCase (
Accordion,Modal,Spinner/Spinner.tsx). - Feature groupings: lowercase or underscore-prefixed (
uxcg/,_biases/,_uxcp/). - Hooks:
use*prefix, one hook per file,src/hooks/flat. - Event handlers:
handle*prefix (handleClick,handleMouseEnter). - Boolean props:
is*,has*,show*(isOpen,isDarkTheme,hasBorder,showLeftIcon). - Props types:
ComponentNamePropsinterface (noIprefix, noTprefix for new code). - API files: lowercase (
biases.ts,strapi.ts). - Lib / utils files: lowercase (
helpers.ts,cookies.ts,uxcat-helpers.ts).
| File | Generated by | Trigger |
|---|---|---|
public/uxcore_/llms.txt |
scripts/generate-llms.ts |
yarn generate:llms |
public/uxcore_/llms-full.txt |
scripts/generate-llms-full.ts |
yarn generate:llms:full |
public/uxcore_/llms-full-pages/**/*.md (168 files) |
scripts/generate-llms-pages.ts |
yarn generate:llms:pages; CI: .github/workflows/generate-llms.yml (manual workflow_dispatch) |
Edit the generator scripts, not the output.
.env.example is the contributor-facing template (with inline comments). Copy to .env.local before running yarn dev.
Client-side (NEXT_PUBLIC_*): NEXT_PUBLIC_ENV, NEXT_PUBLIC_INDEXING, NEXT_PUBLIC_MIXPANEL_TOKEN, NEXT_PUBLIC_GA_MEASUREMENT_ID, NEXT_PUBLIC_AHREFS_ANALYTICS_KEY, NEXT_PUBLIC_DOMAIN, NEXT_PUBLIC_STRAPI, NEXT_PUBLIC_UXCAT_API.
Server-only: NEXTAUTH_SECRET, NEXTAUTH_URL, STRAPI_URL, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, LINKEDIN_CLIENT_ID, LINKEDIN_CLIENT_SECRET, DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET.
NEXT_PUBLIC_INDEXING toggles GA tracking. NEXT_PUBLIC_ENV is local / staging / prod.
assetPrefix:''in dev,/uxcore_nextotherwise. Affects Next.js internal assets only — not files inpublic/.- Rewrites:
/assets/*,/fonts/*,/audio/*,/static/*,/robots.txt,/llms.txt,/llms-full.txt,/llms-full-pages/*all map to/uxcore_/.... Both paths work; canonical form is/uxcore_/. images.domains:lh3.googleusercontent.com,cdn.discordapp.com,strapi.keepsimple.io,staging-strapi.keepsimple.io. To allow new image hosts, add here.- i18n: locales
['en', 'ru', 'hy'], defaulten. - Production builds strip
console.*(exceptconsole.error) viacompiler.removeConsole. Don't rely on logs in prod.
- ❌ Use npm (use yarn)
- ❌ Add App Router files (
app/,'use client',next/navigation) - ❌ Add a state library (Redux, Zustand, Jotai, SWR, React Query)
- ❌ Use named exports for components / layouts
- ❌ Use
T-prefix for new prop types (useComponentNamePropsinterface) - ❌ Use JSON or MDX for static content (TS only)
- ❌ Use
@svgr/webpackor<img src={svg}>— SVGs are hand-written.tsxcomponents - ❌ Use
@constants/*or@utils/*aliases — not defined intsconfig.json - ❌ Copy hardcoded hex / px from neighboring components — defer to the uxcore-style skill
- ❌ Add Jest, Vitest,
next-i18next,next-seo - ❌ Read
localStorage/sessionStorage/windowat module top level - ❌ Skip the
index.tsbarrel when creating a component - ❌ Add cross-module
hy.tsshims (import hy from '@data/otherFeature/hy') — just omithy.ts - ❌ Edit
public/uxcore_/llms*.txtorllms-full-pages/*by hand
These barrels are 0 bytes or missing the default re-export. If you touch any of these components, add the proper barrel:
src/components/_biases/BiasEnvironment/index.ts(0 bytes)src/components/CalculatingResults/index.ts(0 bytes)src/components/NewUpdateModal/index.ts(0 bytes)src/components/ContentGenerator/elements/index.ts(no default export — re-exports multiple sub-components, intentional)src/components/CustomModal/contentTypes/index.ts(no default export — re-exports multiple sub-components, intentional)
src/hooks/ has no index.ts files at all — that's the pattern, hooks are imported by full path.
These are tracked. Don't refactor them away as part of unrelated work:
src/api/rating.ts:7— keep data to avoid multiple requestssrc/lib/helpers.ts:222— passive VH functionalitysrc/layouts/FolderViewLayout/FolderViewLayout.tsx:121— unnecessary state checksrc/layouts/UXCatLayout/UXCatLayout.tsx:282— Badge problemsrc/layouts/TestResult/TestResultLayout.tsx:68— incorrectAnswers typesrc/layouts/UserProfile/UserProfile.types.ts:20— add typesrc/components/SelectImageModal/SelectImageModal.tsx:21— handle imagessrc/components/UXCoreFeedbackModal/UXCoreFeedbackModal.tsx:38— move to api pagesrc/components/Result/Result.tsx:93— slug from backendsrc/pages/user/[userId]/index.tsx:186— stat error
HYTranslation TODOs (10 total — Armenian translation gaps): src/local-types/data.ts:8,74,132; src/lib/helpers.ts:70; src/pages/uxcg/index.tsx:81; src/components/SeoGenerator/SeoGenerator.tsx:98,144; src/components/UXCGModalMobile/UXCGModalMobile.tsx:42; src/components/FormPopup/FormPopup.tsx:20; src/components/_biases/ZoomBox/ZoomBox.tsx:59.
_app.tsxis a multi-hundred-line monolith — handles SessionProvider, GlobalContext, account/biases/questions/settings fetching, GA + Mixpanel init, route-change spinner (500ms debounce), modal timing for feedback / share / help-to-help / new-update / christmas popups, cookie consent, body class theme toggling, scroll style toggling for articles. Any new global concern goes here. Touch carefully.GlobalContextlives atsrc/components/Context/GlobalContext— not in a top-levelcontext/folder. It's imported directly (bypassing barrels) in 8 files; that's the established pattern for context.- 55 imports bypass component barrels (importing
.tsxdirectly through aliases). Worst offenders:@components/Context/GlobalContext(8x),UXCPLayout.tsx(12 such imports). Use barrels for new imports. forceConsistentCasingInFileNames— match existing casing exactly; case mistakes work locally but break Linux CI.strict: false+noImplicitAny: false+@typescript-eslint/no-explicit-any: off— type safety is loose.anyis allowed but discouraged for new code.@ts-ignoreis allowed; ESLint warns on@ts-expect-error.- Asset paths: prefer
/uxcore_/...form. Both/assets/foo.pngand/uxcore_/assets/foo.pngwork via rewrites, but the canonical form is the latter.
- Find the closest existing component or layout and match its structure.
- If a pattern in this file conflicts with what you see in the codebase, this file wins for new code. Old code stays as-is until touched.
- If a pattern isn't covered here at all, stop and ask before inventing one.