- Add comprehensive TDD documentation to CLAUDE.md with coverage requirements and examples - Extract reusable UI components to frontend/src/components/ui/ (Modal, FormInput, Button, Alert, etc.) - Add shared constants (schedulePresets) and utility hooks (useCrudMutation, useFormValidation) - Update frontend/CLAUDE.md with component documentation and usage examples - Refactor CreateTaskModal to use shared components and constants - Fix test assertions to be more robust and accurate across all test files 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
313 lines
9.5 KiB
Markdown
313 lines
9.5 KiB
Markdown
# SmoothSchedule Frontend Development Guide
|
|
|
|
## Project Overview
|
|
|
|
This is the React frontend for SmoothSchedule, a multi-tenant scheduling platform.
|
|
|
|
**See also:** `/home/poduck/Desktop/smoothschedule2/smoothschedule/CLAUDE.md` for backend documentation.
|
|
|
|
## Key Paths
|
|
|
|
```
|
|
/home/poduck/Desktop/smoothschedule2/
|
|
├── frontend/ # This React frontend
|
|
│ ├── src/
|
|
│ │ ├── api/client.ts # Axios API client
|
|
│ │ ├── components/ # Feature components
|
|
│ │ │ └── ui/ # Reusable UI components (see below)
|
|
│ │ ├── constants/ # Shared constants
|
|
│ │ │ └── schedulePresets.ts # Schedule/cron presets
|
|
│ │ ├── hooks/ # React Query hooks (useResources, useAuth, etc.)
|
|
│ │ ├── pages/ # Page components
|
|
│ │ ├── types.ts # TypeScript interfaces
|
|
│ │ ├── i18n/locales/en.json # English translations
|
|
│ │ └── utils/cookies.ts # Cookie utilities
|
|
│ ├── .env.development # Frontend env vars
|
|
│ └── vite.config.ts # Vite configuration
|
|
│
|
|
└── smoothschedule/ # Django backend (runs in Docker!)
|
|
├── docker-compose.local.yml # Docker config
|
|
├── .envs/.local/ # Backend env vars
|
|
├── config/settings/ # Django settings
|
|
└── smoothschedule/
|
|
├── schedule/ # Core scheduling app
|
|
└── users/ # User management
|
|
```
|
|
|
|
## Reusable UI Components
|
|
|
|
All reusable UI components are in `src/components/ui/`. Import from the barrel file:
|
|
|
|
```typescript
|
|
import { Modal, FormInput, Button, Alert } from '../components/ui';
|
|
```
|
|
|
|
### Available Components
|
|
|
|
| Component | Description |
|
|
|-----------|-------------|
|
|
| **Modal** | Reusable modal dialog with header, body, footer |
|
|
| **ModalFooter** | Standardized modal footer with buttons |
|
|
| **FormInput** | Text input with label, error, hint support |
|
|
| **FormSelect** | Select dropdown with label, error support |
|
|
| **FormTextarea** | Textarea with label, error support |
|
|
| **FormCurrencyInput** | ATM-style currency input (cents) |
|
|
| **CurrencyInput** | Raw currency input component |
|
|
| **Button** | Button with variants, loading state, icons |
|
|
| **SubmitButton** | Pre-configured submit button |
|
|
| **Alert** | Alert banner (error, success, warning, info) |
|
|
| **ErrorMessage** | Error alert shorthand |
|
|
| **SuccessMessage** | Success alert shorthand |
|
|
| **TabGroup** | Tab navigation (default, pills, underline) |
|
|
| **StepIndicator** | Multi-step wizard indicator |
|
|
| **LoadingSpinner** | Loading spinner with variants |
|
|
| **PageLoading** | Full page loading state |
|
|
| **Card** | Card container with header/body/footer |
|
|
| **EmptyState** | Empty state placeholder |
|
|
| **Badge** | Status badges |
|
|
|
|
### Usage Examples
|
|
|
|
```typescript
|
|
// Modal with form
|
|
<Modal isOpen={isOpen} onClose={onClose} title="Edit Resource" size="lg">
|
|
<FormInput
|
|
label="Name"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
error={errors.name}
|
|
required
|
|
/>
|
|
<FormSelect
|
|
label="Type"
|
|
value={type}
|
|
onChange={(e) => setType(e.target.value)}
|
|
options={[
|
|
{ value: 'STAFF', label: 'Staff' },
|
|
{ value: 'ROOM', label: 'Room' },
|
|
]}
|
|
/>
|
|
</Modal>
|
|
|
|
// Alert messages
|
|
{error && <ErrorMessage message={error} />}
|
|
{success && <SuccessMessage message="Saved successfully!" />}
|
|
|
|
// Tabs
|
|
<TabGroup
|
|
tabs={[
|
|
{ id: 'details', label: 'Details' },
|
|
{ id: 'schedule', label: 'Schedule' },
|
|
]}
|
|
activeTab={activeTab}
|
|
onChange={setActiveTab}
|
|
/>
|
|
```
|
|
|
|
## Utility Hooks
|
|
|
|
### useCrudMutation
|
|
|
|
Factory hook for CRUD mutations with React Query:
|
|
|
|
```typescript
|
|
import { useCrudMutation, createCrudHooks } from '../hooks/useCrudMutation';
|
|
|
|
// Simple usage
|
|
const createResource = useCrudMutation<Resource, CreateResourceData>({
|
|
endpoint: '/resources',
|
|
method: 'POST',
|
|
invalidateKeys: [['resources']],
|
|
});
|
|
|
|
// Create all CRUD hooks at once
|
|
const { useCreate, useUpdate, useDelete } = createCrudHooks<Resource>('/resources', 'resources');
|
|
```
|
|
|
|
### useFormValidation
|
|
|
|
Schema-based form validation:
|
|
|
|
```typescript
|
|
import { useFormValidation, required, email, minLength } from '../hooks/useFormValidation';
|
|
|
|
const schema = {
|
|
email: [required('Email is required'), email('Invalid email')],
|
|
password: [required(), minLength(8, 'Min 8 characters')],
|
|
};
|
|
|
|
const { errors, validateForm, isValid } = useFormValidation(schema);
|
|
|
|
const handleSubmit = () => {
|
|
if (validateForm(formData)) {
|
|
// Submit
|
|
}
|
|
};
|
|
```
|
|
|
|
## Constants
|
|
|
|
### Schedule Presets
|
|
|
|
```typescript
|
|
import { SCHEDULE_PRESETS, TRIGGER_OPTIONS, OFFSET_PRESETS } from '../constants/schedulePresets';
|
|
```
|
|
|
|
## Local Development Domain Setup
|
|
|
|
### Why lvh.me instead of localhost?
|
|
|
|
This project uses **lvh.me** for local development instead of `localhost` due to cookie domain restrictions in RFC 6265.
|
|
|
|
**The Problem with localhost:**
|
|
- Browsers reject cookies with `domain=.localhost` for security reasons
|
|
- `localhost` is treated as a special-use domain where domain cookies don't work
|
|
- Cannot share cookies across subdomains like `platform.localhost` and `business.localhost`
|
|
|
|
**The Solution - lvh.me:**
|
|
- `lvh.me` is a public DNS service that resolves all `*.lvh.me` domains to `127.0.0.1`
|
|
- Browsers allow setting cookies with `domain=.lvh.me`
|
|
- Cookies are accessible across all subdomains (platform.lvh.me, business1.lvh.me, etc.)
|
|
- No /etc/hosts configuration needed - it just works!
|
|
|
|
### Development URLs
|
|
|
|
Use these URLs for local development:
|
|
|
|
- **Base domain:** `http://lvh.me:5173`
|
|
- **Platform dashboard:** `http://platform.lvh.me:5173`
|
|
- **Business subdomains:** `http://{subdomain}.lvh.me:5173`
|
|
|
|
### Multi-Tenant Architecture
|
|
|
|
The application uses subdomain-based multi-tenancy:
|
|
|
|
1. **Platform Users** (superuser, platform_manager, platform_support)
|
|
- Access the app at `http://platform.lvh.me:5173`
|
|
- See platform dashboard and administrative features
|
|
|
|
2. **Business Users** (owner, manager, staff, resource)
|
|
- Access the app at `http://{business_subdomain}.lvh.me:5173`
|
|
- See business-specific dashboard and features
|
|
|
|
### Cookie-Based Authentication
|
|
|
|
Tokens are stored in cookies with `domain=.lvh.me` to enable cross-subdomain access:
|
|
|
|
```typescript
|
|
// Set cookie accessible across all *.lvh.me subdomains
|
|
setCookie('access_token', token, 7); // domain=.lvh.me
|
|
|
|
// Cookie is accessible on:
|
|
// - platform.lvh.me:5173
|
|
// - business1.lvh.me:5173
|
|
// - business2.lvh.me:5173
|
|
// etc.
|
|
```
|
|
|
|
**Key Files:**
|
|
- `/src/utils/cookies.ts` - Cookie utilities with cross-subdomain support
|
|
- `/src/hooks/useAuth.ts` - Authentication hooks using cookies
|
|
- `/src/api/client.ts` - API client with cookie-based auth
|
|
|
|
### Login Flow
|
|
|
|
1. User navigates to `http://platform.lvh.me:5173`
|
|
2. If not authenticated, shows login page
|
|
3. User enters credentials and submits
|
|
4. Backend validates and returns JWT tokens + user data
|
|
5. Tokens stored in cookies with `domain=.lvh.me`
|
|
6. User data stored in React Query cache
|
|
7. App checks user role:
|
|
- Platform users: Stay on platform.lvh.me
|
|
- Business users: Redirect to {business_subdomain}.lvh.me
|
|
8. Cookies accessible on target subdomain - user sees dashboard
|
|
|
|
### Testing with Playwright
|
|
|
|
Tests use lvh.me for proper subdomain testing:
|
|
|
|
```typescript
|
|
// Start on platform subdomain
|
|
await page.goto('http://platform.lvh.me:5173');
|
|
|
|
// Login sets cookies with domain=.lvh.me
|
|
await page.getByPlaceholder(/username/i).fill('poduck');
|
|
await page.getByPlaceholder(/password/i).fill('starry12');
|
|
await page.getByRole('button', { name: /sign in/i }).click();
|
|
|
|
// Cookies accessible, dashboard loads
|
|
await expect(page.getByRole('heading', { name: /platform dashboard/i })).toBeVisible();
|
|
```
|
|
|
|
### Running the Development Server
|
|
|
|
```bash
|
|
# Install dependencies
|
|
npm install
|
|
|
|
# Start dev server
|
|
npm run dev
|
|
|
|
# Access at http://platform.lvh.me:5173
|
|
```
|
|
|
|
### Running Tests
|
|
|
|
```bash
|
|
# Run all tests
|
|
npm test
|
|
|
|
# Run E2E tests
|
|
npx playwright test
|
|
|
|
# Run specific test file
|
|
npx playwright test login-flow.spec.ts
|
|
|
|
# Run with UI
|
|
npx playwright test --ui
|
|
```
|
|
|
|
## Production Deployment
|
|
|
|
In production, replace `lvh.me` with your actual domain:
|
|
|
|
1. Update `src/utils/cookies.ts`:
|
|
```typescript
|
|
const domain = window.location.hostname.includes('yourdomain.com')
|
|
? '.yourdomain.com'
|
|
: window.location.hostname;
|
|
```
|
|
|
|
2. Configure DNS:
|
|
- `platform.yourdomain.com` → Your server IP
|
|
- `*.yourdomain.com` → Your server IP (wildcard for business subdomains)
|
|
|
|
3. SSL certificates:
|
|
- Get wildcard certificate for `*.yourdomain.com`
|
|
- Or use Let's Encrypt with wildcard support
|
|
|
|
## Troubleshooting
|
|
|
|
### Cookies not working?
|
|
- Make sure you're using `lvh.me`, not `localhost`
|
|
- Check browser DevTools → Application → Cookies
|
|
- Verify `domain=.lvh.me` is set on cookies
|
|
- Clear cookies and try again
|
|
|
|
### Redirect issues?
|
|
- Check `/src/pages/LoginPage.tsx` redirect logic
|
|
- Verify user role and business_subdomain in response
|
|
- Check browser console for navigation errors
|
|
|
|
### Can't access lvh.me?
|
|
- Verify internet connection (lvh.me requires DNS lookup)
|
|
- Try `ping lvh.me` - should resolve to `127.0.0.1`
|
|
- Alternative: Use `127.0.0.1.nip.io` (similar service)
|
|
|
|
## References
|
|
|
|
- [RFC 6265 - HTTP State Management Mechanism](https://datatracker.ietf.org/doc/html/rfc6265)
|
|
- [lvh.me DNS service](http://lvh.me)
|
|
- [Cookie Domain Attribute Rules](https://stackoverflow.com/questions/1062963/how-do-browser-cookie-domains-work)
|