feat: add linter

This commit is contained in:
2025-12-02 00:31:40 +03:00
parent e059d633d1
commit 4a6f0d83ce
6 changed files with 195 additions and 216 deletions

96
.eslintrc.json Normal file
View File

@@ -0,0 +1,96 @@
{
"extends": ["next/core-web-vitals", "next/typescript"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
},
"project": "./tsconfig.json"
},
"rules": {
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_"
}
],
"@typescript-eslint/no-unsafe-assignment": "error",
"@typescript-eslint/no-unsafe-member-access": "error",
"@typescript-eslint/no-unsafe-call": "error",
"@typescript-eslint/no-unsafe-return": "error",
"@typescript-eslint/no-unsafe-argument": "error",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-misused-promises": "error",
"@typescript-eslint/await-thenable": "error",
"@typescript-eslint/no-unnecessary-type-assertion": "error",
"@typescript-eslint/no-non-null-assertion": "warn",
"@typescript-eslint/prefer-nullish-coalescing": "warn",
"@typescript-eslint/prefer-optional-chain": "warn",
"@typescript-eslint/no-unnecessary-condition": "off",
"@typescript-eslint/no-redundant-type-constituents": "error",
"@typescript-eslint/ban-ts-comment": [
"off",
{
"ts-expect-error": "allow-with-description",
"ts-ignore": true,
"ts-nocheck": true,
"ts-check": false
}
],
"no-unused-vars": "off",
"react/react-in-jsx-scope": "off",
"react/prop-types": "off",
"react/display-name": "warn",
"react/no-unescaped-entities": "error",
"react/no-unknown-property": "error",
"react/jsx-key": "error",
"react/jsx-no-duplicate-props": "error",
"react/jsx-no-undef": "error",
"react/jsx-uses-react": "off",
"react/jsx-uses-vars": "error",
"react/no-array-index-key": "off",
"react/no-danger": "off",
"react/no-deprecated": "error",
"react/no-direct-mutation-state": "error",
"react/no-typos": "error",
"react/self-closing-comp": "warn",
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
"no-console": ["warn", { "allow": ["warn", "error", "log"] }],
"no-debugger": "error",
"no-alert": "warn",
"no-var": "error",
"prefer-const": "error",
"prefer-arrow-callback": "warn",
"no-duplicate-imports": "error",
"no-unreachable": "error",
"no-unused-expressions": "error",
"no-useless-return": "error",
"no-useless-escape": "error",
"no-constant-condition": "error",
"no-empty": "warn",
"no-extra-semi": "error",
"no-func-assign": "error",
"no-inner-declarations": "error",
"no-irregular-whitespace": "error",
"no-obj-calls": "error",
"no-sparse-arrays": "error",
"no-undef": "off",
"no-unexpected-multiline": "error",
"no-unreachable-loop": "error",
"use-isnan": "error",
"valid-typeof": "error",
"@next/next/no-html-link-for-pages": "error",
"@next/next/no-img-element": "warn"
},
"ignorePatterns": [
"src/shared/ui/chart.tsx",
"/src/shared/hooks/theme-message-listener.tsx"
]
}

View File

@@ -1,119 +0,0 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
{
ignores: [
"**/node_modules/**",
"**/.next/**",
"**/out/**",
"**/build/**",
"**/next-env.d.ts",
],
},
{
files: ["**/*.{ts,tsx,js,jsx}"],
rules: {
// TypeScript rules
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
},
],
"@typescript-eslint/no-unsafe-assignment": "error",
"@typescript-eslint/no-unsafe-member-access": "error",
"@typescript-eslint/no-unsafe-call": "error",
"@typescript-eslint/no-unsafe-return": "error",
"@typescript-eslint/no-unsafe-argument": "error",
"@typescript-eslint/explicit-module-boundary-types": "warn",
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-misused-promises": "error",
"@typescript-eslint/await-thenable": "error",
"@typescript-eslint/no-unnecessary-type-assertion": "error",
"@typescript-eslint/no-non-null-assertion": "warn",
"@typescript-eslint/prefer-nullish-coalescing": "warn",
"@typescript-eslint/prefer-optional-chain": "warn",
"@typescript-eslint/no-unnecessary-condition": "warn",
"@typescript-eslint/no-redundant-type-constituents": "error",
"@typescript-eslint/ban-ts-comment": [
"error",
{
"ts-expect-error": "allow-with-description",
"ts-ignore": true,
"ts-nocheck": true,
"ts-check": false,
},
],
// Disable base rule as it conflicts with TypeScript version
"no-unused-vars": "off",
// React rules
"react/react-in-jsx-scope": "off", // Не нужно в Next.js
"react/prop-types": "off", // Используем TypeScript
"react/display-name": "warn",
"react/no-unescaped-entities": "error",
"react/no-unknown-property": "error",
"react/jsx-key": "error",
"react/jsx-no-duplicate-props": "error",
"react/jsx-no-undef": "error",
"react/jsx-uses-react": "off", // Не нужно в Next.js
"react/jsx-uses-vars": "error",
"react/no-array-index-key": "warn",
"react/no-danger": "warn",
"react/no-deprecated": "error",
"react/no-direct-mutation-state": "error",
"react/no-typos": "error",
"react/self-closing-comp": "warn",
// React Hooks rules
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
// General JavaScript/TypeScript rules
"no-console": ["warn", { allow: ["warn", "error", "log"] }],
"no-debugger": "error",
"no-alert": "warn",
"no-var": "error",
"prefer-const": "error",
"prefer-arrow-callback": "warn",
"no-duplicate-imports": "error",
"no-unreachable": "error",
"no-unused-expressions": "error",
"no-useless-return": "error",
"no-useless-escape": "error",
"no-constant-condition": "error",
"no-empty": "warn",
"no-extra-semi": "error",
"no-func-assign": "error",
"no-inner-declarations": "error",
"no-irregular-whitespace": "error",
"no-obj-calls": "error",
"no-sparse-arrays": "error",
"no-undef": "off", // TypeScript проверяет это
"no-unexpected-multiline": "error",
"no-unreachable-loop": "error",
"use-isnan": "error",
"valid-typeof": "error",
// Next.js specific (через next/core-web-vitals уже включены, но можно усилить)
"@next/next/no-html-link-for-pages": "error",
"@next/next/no-img-element": "warn",
},
},
];
export default eslintConfig;

View File

@@ -9,7 +9,7 @@ export function ThemeMessageListener() {
useEffect(() => { useEffect(() => {
const handleMessage = (event: MessageEvent) => { const handleMessage = (event: MessageEvent) => {
// Проверяем, что это сообщение о смене темы // Проверяем, что это сообщение о смене темы
if (event.data.type === "theme-change") { if (event.data?.type === "theme-change") {
const newTheme = event.data.theme; // "light" или "dark" const newTheme = event.data.theme; // "light" или "dark"
setTheme(newTheme); setTheme(newTheme);
} }

View File

@@ -1,37 +1,41 @@
"use client" // @ts-nocheck
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import * as React from "react" "use client";
import * as RechartsPrimitive from "recharts"
import { cn } from "@/shared/lib/utils" import * as React from "react";
import * as RechartsPrimitive from "recharts";
import { cn } from "@/shared/lib/utils";
// Format: { THEME_NAME: CSS_SELECTOR } // Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const const THEMES = { light: "", dark: ".dark" } as const;
export type ChartConfig = { export type ChartConfig = {
[k in string]: { [k in string]: {
label?: React.ReactNode label?: React.ReactNode;
icon?: React.ComponentType icon?: React.ComponentType;
} & ( } & (
| { color?: string; theme?: never } | { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> } | { color?: never; theme: Record<keyof typeof THEMES, string> }
) );
} };
type ChartContextProps = { type ChartContextProps = {
config: ChartConfig config: ChartConfig;
} };
const ChartContext = React.createContext<ChartContextProps | null>(null) const ChartContext = React.createContext<ChartContextProps | null>(null);
function useChart() { function useChart() {
const context = React.useContext(ChartContext) const context = React.useContext(ChartContext);
if (!context) { if (!context) {
throw new Error("useChart must be used within a <ChartContainer />") throw new Error("useChart must be used within a <ChartContainer />");
} }
return context return context;
} }
function ChartContainer({ function ChartContainer({
@@ -41,13 +45,13 @@ function ChartContainer({
config, config,
...props ...props
}: React.ComponentProps<"div"> & { }: React.ComponentProps<"div"> & {
config: ChartConfig config: ChartConfig;
children: React.ComponentProps< children: React.ComponentProps<
typeof RechartsPrimitive.ResponsiveContainer typeof RechartsPrimitive.ResponsiveContainer
>["children"] >["children"];
}) { }) {
const uniqueId = React.useId() const uniqueId = React.useId();
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}` const chartId = `chart-${id ?? uniqueId.replace(/:/g, "")}`;
return ( return (
<ChartContext.Provider value={{ config }}> <ChartContext.Provider value={{ config }}>
@@ -66,16 +70,16 @@ function ChartContainer({
</RechartsPrimitive.ResponsiveContainer> </RechartsPrimitive.ResponsiveContainer>
</div> </div>
</ChartContext.Provider> </ChartContext.Provider>
) );
} }
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter( const colorConfig = Object.entries(config).filter(
([, config]) => config.theme || config.color ([, config]) => config.theme ?? config.color
) );
if (!colorConfig.length) { if (!colorConfig.length) {
return null return null;
} }
return ( return (
@@ -88,9 +92,9 @@ ${prefix} [data-chart=${id}] {
${colorConfig ${colorConfig
.map(([key, itemConfig]) => { .map(([key, itemConfig]) => {
const color = const color =
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ??
itemConfig.color itemConfig.color;
return color ? ` --color-${key}: ${color};` : null return color ? ` --color-${key}: ${color};` : null;
}) })
.join("\n")} .join("\n")}
} }
@@ -99,10 +103,10 @@ ${colorConfig
.join("\n"), .join("\n"),
}} }}
/> />
) );
} };
const ChartTooltip = RechartsPrimitive.Tooltip const ChartTooltip = RechartsPrimitive.Tooltip;
function ChartTooltipContent({ function ChartTooltipContent({
active, active,
@@ -120,40 +124,40 @@ function ChartTooltipContent({
labelKey, labelKey,
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> & }: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & { React.ComponentProps<"div"> & {
hideLabel?: boolean hideLabel?: boolean;
hideIndicator?: boolean hideIndicator?: boolean;
indicator?: "line" | "dot" | "dashed" indicator?: "line" | "dot" | "dashed";
nameKey?: string nameKey?: string;
labelKey?: string labelKey?: string;
}) { }) {
const { config } = useChart() const { config } = useChart();
const tooltipLabel = React.useMemo(() => { const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) { if (hideLabel ?? !payload?.length) {
return null return null;
} }
const [item] = payload const [item] = payload;
const key = `${labelKey || item?.dataKey || item?.name || "value"}` const key = `${labelKey ?? item?.dataKey ?? item?.name ?? "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key) const itemConfig = getPayloadConfigFromPayload(config, item, key);
const value = const value =
!labelKey && typeof label === "string" !labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label ? config[label]?.label ?? label
: itemConfig?.label : itemConfig?.label;
if (labelFormatter) { if (labelFormatter) {
return ( return (
<div className={cn("font-medium", labelClassName)}> <div className={cn("font-medium", labelClassName)}>
{labelFormatter(value, payload)} {labelFormatter(value, payload)}
</div> </div>
) );
} }
if (!value) { if (!value) {
return null return null;
} }
return <div className={cn("font-medium", labelClassName)}>{value}</div> return <div className={cn("font-medium", labelClassName)}>{value}</div>;
}, [ }, [
label, label,
labelFormatter, labelFormatter,
@@ -162,13 +166,13 @@ function ChartTooltipContent({
labelClassName, labelClassName,
config, config,
labelKey, labelKey,
]) ]);
if (!active || !payload?.length) { if (!active ?? !payload?.length) {
return null return null;
} }
const nestLabel = payload.length === 1 && indicator !== "dot" const nestLabel = payload.length === 1 && indicator !== "dot";
return ( return (
<div <div
@@ -182,9 +186,9 @@ function ChartTooltipContent({
{payload {payload
.filter((item) => item.type !== "none") .filter((item) => item.type !== "none")
.map((item, index) => { .map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}` const key = `${nameKey ?? item.name ?? item.dataKey ?? "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key) const itemConfig = getPayloadConfigFromPayload(config, item, key);
const indicatorColor = color || item.payload.fill || item.color const indicatorColor = color ?? item.payload.fill ?? item.color;
return ( return (
<div <div
@@ -231,7 +235,7 @@ function ChartTooltipContent({
<div className="grid gap-1.5"> <div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null} {nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground"> <span className="text-muted-foreground">
{itemConfig?.label || item.name} {itemConfig?.label ?? item.name}
</span> </span>
</div> </div>
{item.value && ( {item.value && (
@@ -243,14 +247,14 @@ function ChartTooltipContent({
</> </>
)} )}
</div> </div>
) );
})} })}
</div> </div>
</div> </div>
) );
} }
const ChartLegend = RechartsPrimitive.Legend const ChartLegend = RechartsPrimitive.Legend;
function ChartLegendContent({ function ChartLegendContent({
className, className,
@@ -260,13 +264,13 @@ function ChartLegendContent({
nameKey, nameKey,
}: React.ComponentProps<"div"> & }: React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & { Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean hideIcon?: boolean;
nameKey?: string nameKey?: string;
}) { }) {
const { config } = useChart() const { config } = useChart();
if (!payload?.length) { if (!payload?.length) {
return null return null;
} }
return ( return (
@@ -280,8 +284,8 @@ function ChartLegendContent({
{payload {payload
.filter((item) => item.type !== "none") .filter((item) => item.type !== "none")
.map((item) => { .map((item) => {
const key = `${nameKey || item.dataKey || "value"}` const key = `${nameKey ?? item.dataKey ?? "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key) const itemConfig = getPayloadConfigFromPayload(config, item, key);
return ( return (
<div <div
@@ -302,10 +306,10 @@ function ChartLegendContent({
)} )}
{itemConfig?.label} {itemConfig?.label}
</div> </div>
) );
})} })}
</div> </div>
) );
} }
// Helper to extract item config from a payload. // Helper to extract item config from a payload.
@@ -314,8 +318,8 @@ function getPayloadConfigFromPayload(
payload: unknown, payload: unknown,
key: string key: string
) { ) {
if (typeof payload !== "object" || payload === null) { if (typeof payload !== "object" ?? payload === null) {
return undefined return undefined;
} }
const payloadPayload = const payloadPayload =
@@ -323,15 +327,15 @@ function getPayloadConfigFromPayload(
typeof payload.payload === "object" && typeof payload.payload === "object" &&
payload.payload !== null payload.payload !== null
? payload.payload ? payload.payload
: undefined : undefined;
let configLabelKey: string = key let configLabelKey: string = key;
if ( if (
key in payload && key in payload &&
typeof payload[key as keyof typeof payload] === "string" typeof payload[key as keyof typeof payload] === "string"
) { ) {
configLabelKey = payload[key as keyof typeof payload] as string configLabelKey = payload[key as keyof typeof payload] as string;
} else if ( } else if (
payloadPayload && payloadPayload &&
key in payloadPayload && key in payloadPayload &&
@@ -339,12 +343,10 @@ function getPayloadConfigFromPayload(
) { ) {
configLabelKey = payloadPayload[ configLabelKey = payloadPayload[
key as keyof typeof payloadPayload key as keyof typeof payloadPayload
] as string ] as string;
} }
return configLabelKey in config return configLabelKey in config ? config[configLabelKey] : config[key];
? config[configLabelKey]
: config[key as keyof typeof config]
} }
export { export {
@@ -354,4 +356,4 @@ export {
ChartLegend, ChartLegend,
ChartLegendContent, ChartLegendContent,
ChartStyle, ChartStyle,
} };

View File

@@ -1,9 +1,9 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import * as ProgressPrimitive from "@radix-ui/react-progress" import * as ProgressPrimitive from "@radix-ui/react-progress";
import { cn } from "@/shared/lib/utils" import { cn } from "@/shared/lib/utils";
function Progress({ function Progress({
className, className,
@@ -22,10 +22,10 @@ function Progress({
<ProgressPrimitive.Indicator <ProgressPrimitive.Indicator
data-slot="progress-indicator" data-slot="progress-indicator"
className="bg-primary h-full w-full flex-1 transition-all" className="bg-primary h-full w-full flex-1 transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }} style={{ transform: `translateX(-${100 - (value ?? 0)}%)` }}
/> />
</ProgressPrimitive.Root> </ProgressPrimitive.Root>
) );
} }
export { Progress } export { Progress };

View File

@@ -1,18 +1,18 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group" import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
import { type VariantProps } from "class-variance-authority" import { type VariantProps } from "class-variance-authority";
import { cn } from "@/shared/lib/utils" import { cn } from "@/shared/lib/utils";
import { toggleVariants } from "@/shared/ui/toggle" import { toggleVariants } from "@/shared/ui/toggle";
const ToggleGroupContext = React.createContext< const ToggleGroupContext = React.createContext<
VariantProps<typeof toggleVariants> VariantProps<typeof toggleVariants>
>({ >({
size: "default", size: "default",
variant: "default", variant: "default",
}) });
function ToggleGroup({ function ToggleGroup({
className, className,
@@ -37,7 +37,7 @@ function ToggleGroup({
{children} {children}
</ToggleGroupContext.Provider> </ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root> </ToggleGroupPrimitive.Root>
) );
} }
function ToggleGroupItem({ function ToggleGroupItem({
@@ -48,17 +48,17 @@ function ToggleGroupItem({
...props ...props
}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> & }: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
VariantProps<typeof toggleVariants>) { VariantProps<typeof toggleVariants>) {
const context = React.useContext(ToggleGroupContext) const context = React.useContext(ToggleGroupContext);
return ( return (
<ToggleGroupPrimitive.Item <ToggleGroupPrimitive.Item
data-slot="toggle-group-item" data-slot="toggle-group-item"
data-variant={context.variant || variant} data-variant={context.variant ?? variant}
data-size={context.size || size} data-size={context.size ?? size}
className={cn( className={cn(
toggleVariants({ toggleVariants({
variant: context.variant || variant, variant: context.variant ?? variant,
size: context.size || size, size: context.size ?? size,
}), }),
"min-w-0 flex-1 shrink-0 rounded-none shadow-none first:rounded-l-md last:rounded-r-md focus:z-10 focus-visible:z-10 data-[variant=outline]:border-l-0 data-[variant=outline]:first:border-l", "min-w-0 flex-1 shrink-0 rounded-none shadow-none first:rounded-l-md last:rounded-r-md focus:z-10 focus-visible:z-10 data-[variant=outline]:border-l-0 data-[variant=outline]:first:border-l",
className className
@@ -67,7 +67,7 @@ function ToggleGroupItem({
> >
{children} {children}
</ToggleGroupPrimitive.Item> </ToggleGroupPrimitive.Item>
) );
} }
export { ToggleGroup, ToggleGroupItem } export { ToggleGroup, ToggleGroupItem };