440 lines
13 KiB
Markdown
440 lines
13 KiB
Markdown
# CLAUDE.md
|
|
|
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
|
|
## Project Overview
|
|
|
|
This is a **SvelteKit 5** frontend application for Nexus, built with:
|
|
|
|
- **SvelteKit** as the meta-framework
|
|
- **Houdini** for GraphQL client operations
|
|
- **Tailwind CSS v4** for styling
|
|
- **TypeScript** with strict mode enabled
|
|
- **Node adapter** for production deployment
|
|
|
|
### Relationship to nexus-5-frontend-1
|
|
|
|
This project (nexus-5-frontend-2) is a **streamlined team-focused application** that abstracts functionality from nexus-5-frontend-1 (the admin interface).
|
|
|
|
**Key Differences**:
|
|
|
|
- **nexus-5-frontend-1**: Admin-focused dashboard with comprehensive controls
|
|
- **nexus-5-frontend-2**: Team-focused app with simplified workflows for field/service operations
|
|
|
|
**Using nexus-5-frontend-1 as Reference**:
|
|
|
|
- ✅ **DO** reference nexus-5-frontend-1 for:
|
|
- Understanding business logic and data flow
|
|
- GraphQL query structures and data models
|
|
- UI component patterns and layouts
|
|
- Understanding feature requirements and scope
|
|
|
|
- ❌ **DO NOT** replicate from nexus-5-frontend-1:
|
|
- Dashboard patterns (this app has no dashboard)
|
|
- Admin-specific features
|
|
- Complex data loading patterns (this app uses simpler SvelteKit patterns)
|
|
- Overly complex UI components
|
|
|
|
**When to Reference**:
|
|
|
|
1. When understanding what data a feature needs (check the GraphQL queries)
|
|
2. When understanding business logic (how tasks, sessions, scopes work)
|
|
3. When you need UI inspiration for similar features
|
|
4. When clarifying requirements for features that exist in both apps
|
|
|
|
**Important**: Always ask the user if you're unclear whether a pattern from nexus-5-frontend-1 should be replicated or simplified for this team-focused app.
|
|
|
|
## Development Commands
|
|
|
|
```bash
|
|
# Start development server (runs on https://local.example.com)
|
|
npm run dev
|
|
|
|
# Build for production
|
|
npm run build
|
|
|
|
# Preview production build
|
|
npm run preview
|
|
|
|
# Type checking
|
|
npm run check
|
|
|
|
# Type checking in watch mode
|
|
npm run check:watch
|
|
|
|
# Format code
|
|
npm run format
|
|
|
|
# Lint code (runs prettier check + eslint)
|
|
npm run lint
|
|
```
|
|
|
|
## Docker Deployment
|
|
|
|
The application is containerized with Docker for production deployment.
|
|
|
|
### Docker Files
|
|
|
|
- **`Dockerfile`**: Multi-stage build (Node 22 Alpine)
|
|
- Stage 1: Install dependencies and build the SvelteKit app
|
|
- Stage 2: Production image with only built output and production dependencies
|
|
- **`docker-compose.yml`**: Simple deployment configuration
|
|
- **`.dockerignore`**: Excludes node_modules, .houdini, build artifacts, etc.
|
|
|
|
### Running with Docker
|
|
|
|
```bash
|
|
# Build and run with docker-compose
|
|
docker-compose up --build
|
|
|
|
# Run in detached mode
|
|
docker-compose up -d --build
|
|
|
|
# Stop the container
|
|
docker-compose down
|
|
```
|
|
|
|
### Configuration
|
|
|
|
The container exposes port 3000 internally, mapped to port 7000 on the host by default. Environment variables:
|
|
|
|
- `NODE_ENV=production`
|
|
- `PORT=3000`
|
|
- `HOST=0.0.0.0`
|
|
|
|
To change the external port, modify `docker-compose.yml`:
|
|
|
|
```yaml
|
|
ports:
|
|
- 'YOUR_PORT:3000'
|
|
```
|
|
|
|
## Architecture
|
|
|
|
### GraphQL Integration (Houdini)
|
|
|
|
This project uses **Houdini** for type-safe GraphQL operations:
|
|
|
|
- **Client Configuration**: `src/client.ts` - Houdini client connects to GraphQL endpoint at `http://192.168.100.174:5500/graphql/`
|
|
- **Schema**: `schema.graphql` at project root
|
|
- **Generated Code**: `.houdini/` directory (gitignored, auto-generated)
|
|
- **GraphQL Documents**: Write queries/mutations in `.gql` files or inline in `.svelte` components
|
|
- **Config**: `houdini.config.js` - Includes authentication headers for schema watching:
|
|
- `X-USER-ID`
|
|
- `X-USER-PROFILE-TYPE`
|
|
- `X-OATHKEEPER-SECRET`
|
|
- `X-DJANGO-PROFILE-ID`
|
|
|
|
Authentication headers are read from environment variables in `.env` file.
|
|
|
|
### Path Aliases
|
|
|
|
- `$houdini` → `.houdini/` - Access Houdini generated types and runtime
|
|
- Standard SvelteKit aliases (`$lib`, `$app`, etc.) are available
|
|
|
|
### Development Server
|
|
|
|
The dev server is configured to run on `local.example.com` with HTTPS enabled via `vite-plugin-mkcert`.
|
|
|
|
### TypeScript Configuration
|
|
|
|
- **Strict mode** enabled
|
|
- **Root dirs** include `.houdini/types` for Houdini type integration
|
|
- Module resolution set to `bundler`
|
|
|
|
### Vite Plugins
|
|
|
|
Configured in `vite.config.ts`:
|
|
|
|
1. `houdini` - GraphQL client integration
|
|
2. `tailwindcss` - Tailwind CSS v4
|
|
3. `sveltekit` - SvelteKit integration
|
|
4. `devtoolsJson` - Development tools
|
|
5. `mkcert` - Local HTTPS certificates
|
|
|
|
### Environment Variables
|
|
|
|
The `.env` file contains:
|
|
|
|
- API endpoints and keys for Calendar and Email services
|
|
- Authentication credentials for GraphQL schema introspection
|
|
- All Houdini auth headers (USER_ID, USER_PROFILE_TYPE, OATHKEEPER_SECRET, DJANGO_PROFILE_ID)
|
|
|
|
**Important**: These credentials are for development only.
|
|
|
|
### ESLint Configuration
|
|
|
|
Uses the new flat config format (`eslint.config.js`) with:
|
|
|
|
- TypeScript ESLint recommended rules
|
|
- Svelte plugin with recommended rules
|
|
- Prettier integration
|
|
- `no-undef` disabled for TypeScript files (as recommended by typescript-eslint)
|
|
|
|
## Working with GraphQL
|
|
|
|
### Schema Updates
|
|
|
|
When the GraphQL schema changes:
|
|
|
|
1. Update `schema.graphql` manually, or
|
|
2. Houdini's `watchSchema` will auto-update during development
|
|
3. Run `npm run prepare` or restart dev server to regenerate types
|
|
|
|
### Writing Queries
|
|
|
|
Queries can be written:
|
|
|
|
- In separate `.gql` files
|
|
- Inline in `.svelte` components using Houdini's Svelte integration
|
|
|
|
GraphQL documents are automatically discovered in:
|
|
|
|
- `**/*.gql` files
|
|
- `**/*.svelte` components
|
|
- `.houdini/graphql/documents.gql`
|
|
|
|
## Project Structure
|
|
|
|
```
|
|
src/
|
|
├── app.d.ts # SvelteKit app types
|
|
├── app.css # Global styles (Tailwind imports)
|
|
├── app.html # HTML template
|
|
├── client.ts # Houdini client setup
|
|
├── lib/
|
|
│ ├── assets/ # Static assets (favicon, etc.)
|
|
│ ├── components/ # Reusable Svelte components
|
|
│ │ ├── entity/ # Entity-related components (StatusBadge, TeamMemberList, etc.)
|
|
│ │ ├── layout/ # Layout components (Container, Navigation, etc.)
|
|
│ │ └── ui/ # Generic UI components
|
|
│ ├── graphql/ # GraphQL documents
|
|
│ │ ├── queries/ # Query .gql files organized by entity
|
|
│ │ └── mutations/ # Mutation .gql files
|
|
│ └── utils/ # Utility functions (date formatting, relay helpers, etc.)
|
|
└── routes/ # SvelteKit file-based routing
|
|
├── +layout.svelte # Root layout with navigation
|
|
├── +page.svelte # Home page
|
|
├── services/ # Services list and detail pages
|
|
├── projects/ # Projects list and detail pages
|
|
├── reports/ # Reports list and detail pages
|
|
├── messages/ # Messaging pages
|
|
└── sessions/ # Session pages (services/projects)
|
|
```
|
|
|
|
### Key Directories
|
|
|
|
- **`src/lib/components/entity/`**: Shared components for displaying entity data (services, projects). Includes `StatusBadge`, `TeamMemberList`, `AddressCard`, `IdSidebar`, scope cards, etc.
|
|
- **`src/lib/graphql/`**: GraphQL documents organized by entity type. Queries and mutations are in separate `.gql` files.
|
|
- **`src/lib/utils/`**: Helper functions like `formatDate()`, `fromGlobalId()` (Relay ID conversion), etc.
|
|
|
|
## Important Notes
|
|
|
|
- This project uses **Svelte 5** - use modern Svelte 5 syntax (runes, snippets, etc.)
|
|
- **Tailwind CSS v4** is configured - use the latest v4 features and syntax
|
|
- The project uses **adapter-node** for production builds
|
|
- HTTPS is enabled in development - the server runs on `local.example.com`
|
|
|
|
## Coding Standards
|
|
|
|
### Svelte 5 Specific Rules
|
|
|
|
#### SVG `fill` Attribute
|
|
|
|
**IMPORTANT**: Never use the `fill` attribute directly on SVG elements in Svelte 5. Always use the `style` attribute instead.
|
|
|
|
❌ **Incorrect**:
|
|
|
|
```svelte
|
|
<svg fill="none" viewBox="0 0 24 24">
|
|
<svg fill="currentColor" viewBox="0 0 24 24">
|
|
```
|
|
|
|
✅ **Correct**:
|
|
|
|
```svelte
|
|
<svg style="fill: none" viewBox="0 0 24 24">
|
|
<svg style="fill: currentColor" viewBox="0 0 24 24">
|
|
```
|
|
|
|
**Reason**: The `fill` attribute is considered obsolete in Svelte 5 and will trigger warnings. Use inline styles instead.
|
|
|
|
#### HTML Semantics - Button Content
|
|
|
|
**IMPORTANT**: Buttons can only contain phrasing content (inline elements). Never place block-level elements like `<div>` inside `<button>` elements.
|
|
|
|
❌ **Incorrect**:
|
|
|
|
```svelte
|
|
<button>
|
|
<div class="flex items-center gap-3">
|
|
<svg>...</svg>
|
|
<span>Text</span>
|
|
</div>
|
|
</button>
|
|
```
|
|
|
|
✅ **Correct**:
|
|
|
|
```svelte
|
|
<button>
|
|
<span class="flex items-center gap-3">
|
|
<svg>...</svg>
|
|
<span>Text</span>
|
|
</span>
|
|
</button>
|
|
```
|
|
|
|
**Valid phrasing content elements** (safe to use in buttons):
|
|
|
|
- `<span>`, `<strong>`, `<em>`, `<b>`, `<i>`, `<small>`, `<mark>`
|
|
- `<svg>`, `<img>`, `<abbr>`, `<code>`, `<kbd>`, `<sup>`, `<sub>`
|
|
|
|
**Invalid block elements** (never use in buttons):
|
|
|
|
- `<div>`, `<p>`, `<h1-h6>`, `<ul>`, `<ol>`, `<section>`, `<article>`
|
|
|
|
**Reason**: HTML5 semantic rules require button elements to only contain phrasing content. Block elements will trigger validation warnings and may cause accessibility issues.
|
|
|
|
### TypeScript Standards
|
|
|
|
#### Use Houdini Generated Types
|
|
|
|
**IMPORTANT**: Never use `any` or `unknown` when Houdini provides generated types.
|
|
|
|
❌ **Incorrect**:
|
|
|
|
```typescript
|
|
let scopeData = $state<{ scope?: any } | null>(null);
|
|
tasks.filter((t: any) => ...)
|
|
```
|
|
|
|
✅ **Correct**:
|
|
|
|
```typescript
|
|
import type { GetScope$result } from '$houdini';
|
|
let scopeData = $state<GetScope$result | null>(null);
|
|
tasks.filter((t) => ...)
|
|
```
|
|
|
|
**Available Houdini types**:
|
|
|
|
- Import result types: `GetQueryName$result`
|
|
- Import input types: `GetQueryName$input`
|
|
- Types are automatically generated in `.houdini/artifacts/`
|
|
|
|
**Reason**: Houdini generates complete TypeScript types from GraphQL schema. Using these types provides full type safety and autocomplete.
|
|
|
|
## UI Patterns
|
|
|
|
### Responsive Design
|
|
|
|
The app targets mobile devices (360px minimum width) as well as desktop. Common patterns:
|
|
|
|
**Mobile Cards / Desktop Table Pattern:**
|
|
|
|
For data-heavy pages like reports, use separate layouts:
|
|
|
|
```svelte
|
|
<!-- Mobile cards: visible below md breakpoint -->
|
|
<div class="space-y-4 md:hidden">
|
|
{#each items as item}
|
|
<a href="/item/{item.id}" class="block rounded-lg border p-4">
|
|
<!-- Card content -->
|
|
</a>
|
|
{/each}
|
|
</div>
|
|
|
|
<!-- Desktop table: hidden below md breakpoint -->
|
|
<div class="hidden md:block">
|
|
<table class="w-full">
|
|
<!-- Table content -->
|
|
</table>
|
|
</div>
|
|
```
|
|
|
|
**Clickable Table Rows:**
|
|
|
|
Make entire rows clickable by wrapping each cell's content in anchor tags:
|
|
|
|
```svelte
|
|
<tr class="group cursor-pointer transition-colors hover:bg-[rgb(var(--bg-tertiary))]">
|
|
<td class="px-4 py-3">
|
|
<a href="/item/{id}" class="block text-primary-500 group-hover:underline">
|
|
{name}
|
|
</a>
|
|
</td>
|
|
<td class="px-4 py-3">
|
|
<a href="/item/{id}" class="block">
|
|
{otherValue}
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
```
|
|
|
|
## Data Patterns
|
|
|
|
### Pagination Pattern (Services & Projects)
|
|
|
|
The services (`/services`) and projects (`/projects`) pages use a hybrid server/client pagination pattern:
|
|
|
|
**How it works:**
|
|
|
|
1. **Server Load** (`+page.ts`):
|
|
- Fetches first 20 items per tab (scheduled, in_progress, completed)
|
|
- Applies month/year filter from URL params (`?month=X&year=Y`)
|
|
- Passes `teamProfileId`, `dateFilter`, and `pageSize` to client for subsequent fetches
|
|
|
|
2. **Client State** (`+page.svelte`):
|
|
- Uses `$state` to accumulate items (initialized from server data via `$effect`)
|
|
- Tracks cursor and `hasMore` state per tab
|
|
- "Load More" button triggers client-side fetch using Houdini store classes
|
|
|
|
3. **Tab-specific behavior:**
|
|
- **Scheduled**: `SCHEDULED` + `CANCELLED` statuses, ascending date order
|
|
- **In Progress**: `IN_PROGRESS` status only, no pagination (typically few items)
|
|
- **Completed**: `COMPLETED` status only, descending date order
|
|
|
|
**Client-side load more pattern:**
|
|
|
|
```typescript
|
|
import { GetServicesByTeamMemberStore } from '$houdini';
|
|
import { ServiceChoices, DateOrdering } from '$houdini';
|
|
|
|
async function loadMoreScheduled() {
|
|
if (!scheduledCursor || loadingMoreScheduled) return;
|
|
loadingMoreScheduled = true;
|
|
|
|
try {
|
|
const store = new GetServicesByTeamMemberStore();
|
|
const result = await store.fetch({
|
|
variables: {
|
|
teamProfileId: data.teamProfileId,
|
|
first: data.pageSize,
|
|
after: scheduledCursor,
|
|
filters: {
|
|
date: data.dateFilter,
|
|
status: { inList: [ServiceChoices.SCHEDULED, ServiceChoices.CANCELLED] }
|
|
},
|
|
ordering: DateOrdering.ASC
|
|
}
|
|
});
|
|
|
|
if (result.data?.getServicesByTeamMember) {
|
|
const connection = result.data.getServicesByTeamMember;
|
|
scheduledItems = [...scheduledItems, ...connection.edges.map((e) => e.node)];
|
|
scheduledCursor = connection.pageInfo.endCursor;
|
|
scheduledHasMore = connection.pageInfo.hasNextPage;
|
|
}
|
|
} finally {
|
|
loadingMoreScheduled = false;
|
|
}
|
|
}
|
|
```
|
|
|
|
**Key points:**
|
|
|
|
- Month/year filter determines the data set; pagination loads more within that set
|
|
- Tab counts show "20+" when more pages exist
|
|
- No URL navigation on "Load More" - results append in place
|