Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
settings.json
86 changes: 86 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Claude Code Guidelines

## Branch naming

Pattern: `<type>/<issue-number>-<short-slug>`

Types observed in this repo:
- `client/` — web-client only changes (UI, components, pages, theme, client deps)
- `feature/` — new functionality that touches backend or crosses client/server layers
- `chore/` — tooling, deps, config, migrations
- `infra/` — Docker, CI, proxy, infrastructure
- `docs/` — documentation only

Examples:
- `client/44-members-page`
- `client/43-app-shell`
- `chore/53-web-client-ci`
- `feature/27-hook-client-to-server-end-points` — client+server change, so feature/

## Commit messages

Conventional commit prefix, lowercase, imperative, no period. Short and specific.

Prefixes:
- `feat:` — new feature or behaviour
- `fix:` — bug fix
- `chore:` — tooling, deps, config, no production code change
- `refactor:` — restructuring without behaviour change
- `style:` — formatting, CSS, theme, no logic change
- `docs:` — documentation only
- `test:` — adding or updating tests
- `ci:` — CI/CD pipeline changes

Good:
- `feat: add sidebar layout component`
- `fix: axios interceptor on 401`
- `chore: migrate auth token storage to zustand`
- `style: apply bebas neue to page titles`

Bad:
- `Add sidebar layout component` — missing prefix
- `Fixed the thing` — vague, past tense
- `WIP`

## PR titles

Pattern: `<Type> #<issue>: <short description>`

Type is title-case, matching the branch type:
- `Client #44: Members page`
- `Client #43: App shell with sidebar layout`
- `Chore #53: Web client CI pipeline`
- `Infra #31: Implemented path routing for services`
- `Feature #27: Hook client to server endpoints` — crosses layers, so Feature

Always include the issue number.

## PR body

Use this structure every time:

```
## Why
One or two sentences. What problem does this solve, or what goal does it serve?
Not what the code does — why it exists.

## What changed
- Bullet per meaningful change, grouped by area if needed
- Keep each line scannable (one idea, one line)

## Notes
Non-obvious decisions, tradeoffs, known limitations, or follow-up issues.
Omit this section if there is nothing worth flagging.

## Testing
How was this verified? (e.g. "pnpm build passes", "manually tested dark mode toggle", "all routes navigate correctly")

Closes #<issue>
```

Rules:
- Always close the linked issue with `Closes #<issue>`
- **Why** is mandatory — never skip it
- **Notes** is optional — only include if there is something a reviewer would otherwise have to figure out themselves
- No walls of prose; no vague "various improvements" bullets
- If setup steps are needed (migrations, one-time installs), add them under **Notes**
8 changes: 6 additions & 2 deletions web-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,12 @@
"test:watch": "vitest"
},
"dependencies": {
"@fontsource/bebas-neue": "^5.2.7",
"@fontsource/poppins": "^5.2.7",
"@tailwindcss/vite": "^4.3.0",
"axios": "^1.16.1",
"daisyui": "^5.5.20",
"lucide-react": "^1.17.0",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"react-router-dom": "^7.15.1",
Expand All @@ -39,10 +43,10 @@
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.6.0",
"jsdom": "^25.0.1",
"openapi-typescript": "^7.8.0",
"typescript": "~6.0.2",
"typescript-eslint": "^8.59.2",
"vite": "^8.0.12",
"vitest": "^2.1.9",
"openapi-typescript": "^7.8.0"
"vitest": "^2.1.9"
}
}
36 changes: 36 additions & 0 deletions web-client/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 18 additions & 14 deletions web-client/src/app/router/AppRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { getLettersHello } from '@/features/letters/api'
import { getMembersHello } from '@/features/members/api'
import { getOrganizationHello } from '@/features/organization/api'
import { getPaymentsHello } from '@/features/payments/api'
import { ThemeToggle } from '@/app/theme/ThemeToggle'

type ServicePlaceholderPageProps = {
title: string
Expand Down Expand Up @@ -43,37 +44,40 @@ function ServicePlaceholderPage({ title, loadMessage }: ServicePlaceholderPagePr
}, [loadMessage])

return (
<section className="rounded-xl border border-slate-200 bg-white p-6 shadow-sm">
<h1 className="text-2xl font-semibold text-slate-900">{title}</h1>
<p className="mt-2 text-sm text-slate-500">
Placeholder page for initial client navigation.
</p>
<section className="card bg-base-200 shadow-md">
<div className="card-body">
<h1 className="font-display text-display-md uppercase tracking-wide text-base-content">{title}</h1>
<p className="text-body-sm text-base-content/70">
Placeholder page for initial client navigation.
</p>

<div className="mt-6 rounded-lg bg-slate-50 p-4">
{loading && <p className="text-slate-700">Loading hello endpoint response...</p>}
{message && <p className="font-mono text-slate-900">{message}</p>}
{error && <p className="text-red-700">Failed to load response: {error}</p>}
<div className="mt-4 rounded-lg bg-base-300 p-4">
{loading && <p className="text-body text-base-content">Loading hello endpoint response...</p>}
{message && <p className="font-mono text-base-content">{message}</p>}
{error && <p className="text-error">Failed to load response: {error}</p>}
</div>
</div>
</section>
)
}

const navLinkClass = ({ isActive }: { isActive: boolean }) =>
`rounded-md px-3 py-2 text-sm ${isActive ? 'bg-slate-900 text-white' : 'bg-slate-200 text-slate-700'}`
`btn btn-sm ${isActive ? 'btn-primary' : 'btn-ghost'}`

export function AppRouter() {
return (
<div className="min-h-screen bg-slate-100 text-slate-900">
<header className="border-b border-slate-200 bg-white">
<div className="min-h-screen bg-base-100 text-base-content">
<header className="border-b border-base-300">
<div className="mx-auto flex max-w-5xl flex-col gap-4 px-4 py-4 sm:flex-row sm:items-center sm:justify-between">
<p className="text-lg font-semibold">Team Devoops Client</p>
<nav className="flex flex-wrap gap-2">
<h1 className="font-display text-display-sm uppercase tracking-wide">Team Devoops</h1>
<nav className="flex flex-wrap items-center gap-2">
<NavLink to="/members" className={navLinkClass}>Members</NavLink>
<NavLink to="/events" className={navLinkClass}>Events</NavLink>
<NavLink to="/payments" className={navLinkClass}>Payments</NavLink>
<NavLink to="/letters" className={navLinkClass}>Letters</NavLink>
<NavLink to="/organization" className={navLinkClass}>Organization</NavLink>
<NavLink to="/feedback" className={navLinkClass}>Feedback</NavLink>
<ThemeToggle />
</nav>
</div>
</header>
Expand Down
8 changes: 8 additions & 0 deletions web-client/src/app/theme/ThemeContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { createContext } from 'react'

export interface ThemeContextType {
theme: 'lumio' | 'lumio-dark'
toggleTheme: () => void
}

export const ThemeContext = createContext<ThemeContextType | undefined>(undefined)
29 changes: 29 additions & 0 deletions web-client/src/app/theme/ThemeProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { type ReactNode, useEffect, useState } from 'react'
import { ThemeContext } from './ThemeContext'

type Theme = 'lumio' | 'lumio-dark'

export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<Theme>(() => {
const stored = localStorage.getItem('theme') as Theme | null
if (stored === 'lumio' || stored === 'lumio-dark') return stored

const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
return prefersDark ? 'lumio-dark' : 'lumio'
})

useEffect(() => {
document.documentElement.setAttribute('data-theme', theme)
localStorage.setItem('theme', theme)
}, [theme])

const toggleTheme = () => {
setTheme((prev) => (prev === 'lumio' ? 'lumio-dark' : 'lumio'))
}

return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
)
}
17 changes: 17 additions & 0 deletions web-client/src/app/theme/ThemeToggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Moon, Sun } from 'lucide-react'
import { useTheme } from './useTheme'

export function ThemeToggle() {
const { theme, toggleTheme } = useTheme()
const isDark = theme === 'lumio-dark'

return (
<button
onClick={toggleTheme}
className="btn btn-sm btn-ghost"
aria-label={`Switch to ${isDark ? 'light' : 'dark'} mode`}
>
{isDark ? <Sun size={16} /> : <Moon size={16} />}
</button>
)
}
10 changes: 10 additions & 0 deletions web-client/src/app/theme/useTheme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { useContext } from 'react'
import { ThemeContext } from './ThemeContext'

export function useTheme() {
const context = useContext(ThemeContext)
if (!context) {
throw new Error('useTheme must be used within ThemeProvider')
}
return context
}
2 changes: 1 addition & 1 deletion web-client/src/features/payments/client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import axios from 'axios'

export const paymentsClient = axios.create({
baseURL: '/api/v1/finances',
baseURL: '/api/v1/finance',
})
Loading
Loading