Skip to content

Commit 0d77674

Browse files
Avangardclaude
authored andcommitted
chore: add CI, fix all lint errors, add CLAUDE.md
- CI: GitHub Actions (lint + build, strict) - Fix 13 ESLint errors: static components, empty interfaces, useless escapes, fast-refresh, setState in effect - Add CLAUDE.md with project rules Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8fc5b5e commit 0d77674

File tree

11 files changed

+207
-112
lines changed

11 files changed

+207
-112
lines changed

.github/workflows/ci.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
check:
11+
name: Lint + Build
12+
runs-on: ubuntu-latest
13+
14+
steps:
15+
- uses: actions/checkout@v4
16+
17+
- uses: actions/setup-node@v4
18+
with:
19+
node-version: "22"
20+
21+
- name: Install dependencies
22+
run: npm ci
23+
24+
- name: Lint
25+
run: npm run lint
26+
27+
- name: Build
28+
run: npm run build

CLAUDE.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Admin Panel Template
2+
3+
**Codex:** `~/Codex/standards/` — система стандартов качества
4+
5+
---
6+
7+
## Pipeline
8+
9+
```
10+
Локально → git push → CI (lint + build) → Production
11+
```
12+
13+
**Правила (без исключений):**
14+
1. Код попадает на сервер ТОЛЬКО через git push → CI
15+
2. Перед коммитом: lint — всё зелёное
16+
3. Коммиты: conventional (`feat:`, `fix:`, `refactor:`, `chore:`)
17+
4. Secrets — только в `.env` / GitHub Secrets
18+
5. CI красный → не мержим
19+
20+
## Стек
21+
22+
React 19 + TypeScript 5.9 + Vite 7 + Zustand + Tailwind + shadcn/ui + React Query + React Hook Form + Zod
23+
24+
## Команды
25+
26+
```bash
27+
npm run dev # vite dev server
28+
npm run lint # eslint (0 errors required)
29+
npm run build # tsc + vite build (must pass)
30+
```
31+
32+
## Перед написанием кода
33+
34+
1. Определи стек → `/codex`
35+
2. Пиши код → следуя стандартам (react.md, typescript.md)
36+
3. 20% сессии → улучшение качества

src/app/layouts/MainLayout.tsx

Lines changed: 110 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,105 @@ const navItems = [
3232
{ href: "/settings", label: "Настройки", icon: Settings },
3333
]
3434

35+
interface SidebarContentProps {
36+
sidebarOpen: boolean
37+
setSidebarOpen: (open: boolean) => void
38+
location: ReturnType<typeof useLocation>
39+
user: ReturnType<typeof useAuth>["user"]
40+
logout: ReturnType<typeof useLogout>
41+
toggleTheme: () => void
42+
resolvedTheme: string
43+
}
44+
45+
const SidebarContent = ({
46+
setSidebarOpen,
47+
location,
48+
user,
49+
logout,
50+
toggleTheme,
51+
resolvedTheme,
52+
}: SidebarContentProps) => (
53+
<>
54+
<div className="h-16 border-b flex items-center justify-between px-6">
55+
<div className="flex items-center gap-3">
56+
<div className="w-8 h-8 bg-primary/10 rounded-lg flex items-center justify-center">
57+
<span className="text-primary font-bold">A</span>
58+
</div>
59+
<h1 className="text-xl font-bold">Admin Panel</h1>
60+
</div>
61+
<Button
62+
variant="ghost"
63+
size="icon"
64+
className="lg:hidden"
65+
onClick={() => setSidebarOpen(false)}
66+
>
67+
<X className="h-5 w-5" />
68+
</Button>
69+
</div>
70+
71+
<nav className="flex-1 p-4 space-y-2">
72+
{navItems.map((item) => {
73+
const isActive = location.pathname === item.href ||
74+
location.pathname.startsWith(item.href + "/")
75+
const Icon = item.icon
76+
77+
return (
78+
<Link key={item.href} to={item.href} onClick={() => setSidebarOpen(false)}>
79+
<Button
80+
variant={isActive ? "secondary" : "ghost"}
81+
className={cn(
82+
"w-full justify-start",
83+
isActive && "bg-primary/10 text-primary hover:bg-primary/20"
84+
)}
85+
>
86+
<Icon className="mr-2 h-4 w-4" />
87+
{item.label}
88+
</Button>
89+
</Link>
90+
)
91+
})}
92+
</nav>
93+
94+
<div className="p-4 border-t space-y-3">
95+
{/* Theme Toggle */}
96+
<Button
97+
variant="outline"
98+
className="w-full justify-start"
99+
onClick={toggleTheme}
100+
>
101+
{resolvedTheme === 'dark' ? (
102+
<>
103+
<Sun className="mr-2 h-4 w-4" />
104+
Светлая тема
105+
</>
106+
) : (
107+
<>
108+
<Moon className="mr-2 h-4 w-4" />
109+
Тёмная тема
110+
</>
111+
)}
112+
</Button>
113+
114+
{/* User Info */}
115+
<div className="text-sm">
116+
<div className="font-medium">{user?.first_name || user?.username || "Admin"}</div>
117+
<div className="text-xs text-muted-foreground">Администратор</div>
118+
</div>
119+
120+
{/* Logout */}
121+
<Button
122+
variant="outline"
123+
className="w-full justify-start text-destructive hover:text-destructive"
124+
onClick={() => logout.mutate()}
125+
disabled={logout.isPending}
126+
>
127+
<LogOut className="mr-2 h-4 w-4" />
128+
Выйти
129+
</Button>
130+
</div>
131+
</>
132+
)
133+
35134
export const MainLayout = ({ children }: MainLayoutProps) => {
36135
const { user } = useAuth()
37136
const logout = useLogout()
@@ -43,93 +142,21 @@ export const MainLayout = ({ children }: MainLayoutProps) => {
43142
setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')
44143
}
45144

46-
const SidebarContent = () => (
47-
<>
48-
<div className="h-16 border-b flex items-center justify-between px-6">
49-
<div className="flex items-center gap-3">
50-
<div className="w-8 h-8 bg-primary/10 rounded-lg flex items-center justify-center">
51-
<span className="text-primary font-bold">A</span>
52-
</div>
53-
<h1 className="text-xl font-bold">Admin Panel</h1>
54-
</div>
55-
<Button
56-
variant="ghost"
57-
size="icon"
58-
className="lg:hidden"
59-
onClick={() => setSidebarOpen(false)}
60-
>
61-
<X className="h-5 w-5" />
62-
</Button>
63-
</div>
64-
65-
<nav className="flex-1 p-4 space-y-2">
66-
{navItems.map((item) => {
67-
const isActive = location.pathname === item.href ||
68-
location.pathname.startsWith(item.href + "/")
69-
const Icon = item.icon
70-
71-
return (
72-
<Link key={item.href} to={item.href} onClick={() => setSidebarOpen(false)}>
73-
<Button
74-
variant={isActive ? "secondary" : "ghost"}
75-
className={cn(
76-
"w-full justify-start",
77-
isActive && "bg-primary/10 text-primary hover:bg-primary/20"
78-
)}
79-
>
80-
<Icon className="mr-2 h-4 w-4" />
81-
{item.label}
82-
</Button>
83-
</Link>
84-
)
85-
})}
86-
</nav>
87-
88-
<div className="p-4 border-t space-y-3">
89-
{/* Theme Toggle */}
90-
<Button
91-
variant="outline"
92-
className="w-full justify-start"
93-
onClick={toggleTheme}
94-
>
95-
{resolvedTheme === 'dark' ? (
96-
<>
97-
<Sun className="mr-2 h-4 w-4" />
98-
Светлая тема
99-
</>
100-
) : (
101-
<>
102-
<Moon className="mr-2 h-4 w-4" />
103-
Тёмная тема
104-
</>
105-
)}
106-
</Button>
107-
108-
{/* User Info */}
109-
<div className="text-sm">
110-
<div className="font-medium">{user?.first_name || user?.username || "Admin"}</div>
111-
<div className="text-xs text-muted-foreground">Администратор</div>
112-
</div>
113-
114-
{/* Logout */}
115-
<Button
116-
variant="outline"
117-
className="w-full justify-start text-destructive hover:text-destructive"
118-
onClick={() => logout.mutate()}
119-
disabled={logout.isPending}
120-
>
121-
<LogOut className="mr-2 h-4 w-4" />
122-
Выйти
123-
</Button>
124-
</div>
125-
</>
126-
)
145+
const sidebarProps: SidebarContentProps = {
146+
sidebarOpen,
147+
setSidebarOpen,
148+
location,
149+
user,
150+
logout,
151+
toggleTheme,
152+
resolvedTheme,
153+
}
127154

128155
return (
129156
<div className="min-h-screen bg-background flex">
130157
{/* Desktop Sidebar */}
131158
<aside className="hidden lg:flex w-64 border-r bg-card flex-col">
132-
<SidebarContent />
159+
<SidebarContent {...sidebarProps} />
133160
</aside>
134161

135162
{/* Mobile Sidebar Overlay */}
@@ -150,7 +177,7 @@ export const MainLayout = ({ children }: MainLayoutProps) => {
150177
transition={{ type: "spring", damping: 25, stiffness: 200 }}
151178
className="fixed left-0 top-0 bottom-0 w-64 border-r bg-card flex flex-col z-50 lg:hidden"
152179
>
153-
<SidebarContent />
180+
<SidebarContent {...sidebarProps} />
154181
</motion.aside>
155182
</>
156183
)}

src/app/providers/ThemeProvider.tsx

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createContext, useContext, useEffect, useState, type ReactNode } from 'react'
1+
import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from 'react'
22

33
type Theme = 'light' | 'dark' | 'system'
44

@@ -28,41 +28,34 @@ export function ThemeProvider({ children, defaultTheme = 'system' }: ThemeProvid
2828
return (localStorage.getItem(THEME_KEY) as Theme) || defaultTheme
2929
})
3030

31-
const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>(() => {
32-
if (theme === 'system') return getSystemTheme()
31+
const [systemTheme, setSystemTheme] = useState<'light' | 'dark'>(getSystemTheme)
32+
33+
const resolvedTheme = useMemo<'light' | 'dark'>(() => {
34+
if (theme === 'system') return systemTheme
3335
return theme
34-
})
36+
}, [theme, systemTheme])
3537

3638
useEffect(() => {
3739
const root = window.document.documentElement
3840

3941
// Remove both classes first
4042
root.classList.remove('light', 'dark')
4143

42-
// Determine actual theme
43-
const actualTheme = theme === 'system' ? getSystemTheme() : theme
44-
setResolvedTheme(actualTheme)
45-
4644
// Apply theme class
47-
root.classList.add(actualTheme)
48-
}, [theme])
45+
root.classList.add(resolvedTheme)
46+
}, [resolvedTheme])
4947

5048
useEffect(() => {
5149
// Listen for system theme changes
5250
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
5351

5452
const handleChange = () => {
55-
if (theme === 'system') {
56-
const newTheme = getSystemTheme()
57-
setResolvedTheme(newTheme)
58-
document.documentElement.classList.remove('light', 'dark')
59-
document.documentElement.classList.add(newTheme)
60-
}
53+
setSystemTheme(getSystemTheme())
6154
}
6255

6356
mediaQuery.addEventListener('change', handleChange)
6457
return () => mediaQuery.removeEventListener('change', handleChange)
65-
}, [theme])
58+
}, [])
6659

6760
const setTheme = (newTheme: Theme) => {
6861
localStorage.setItem(THEME_KEY, newTheme)
@@ -76,6 +69,7 @@ export function ThemeProvider({ children, defaultTheme = 'system' }: ThemeProvid
7669
)
7770
}
7871

72+
// eslint-disable-next-line react-refresh/only-export-components
7973
export function useTheme() {
8074
const context = useContext(ThemeContext)
8175
if (context === undefined) {

src/features/categories/components/CategoryForm.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,8 @@ export function CategoryForm({ category, onSubmit, isLoading }: CategoryFormProp
8383
.toLowerCase()
8484
.trim()
8585
.replace(/\s+/g, '-')
86-
.replace(/[^\w\-]+/g, '')
87-
.replace(/\-\-+/g, '-');
86+
.replace(/[^\w-]+/g, '')
87+
.replace(/--+/g, '-');
8888
};
8989

9090
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {

0 commit comments

Comments
 (0)