This is an Astro-based event management platform built with TypeScript, React, and a clean architecture pattern. The project follows Domain-Driven Design (DDD) principles with a modular structure.
Astro handles routing from src/pages, with shared layout primitives in src/layouts and global styles in src/styles. Domain logic is grouped under src/modules (e.g. events, meetups, organizations) while cross-cutting utilities live in src/modules/shared. Database schemas, content migrations, and seeds live in db/, and static assets are split between public/ for runtime delivery and docs/ for marketing collateral.
- Domain Layer: Contains business logic, entities, value objects, and domain services
- Application Layer: Contains use cases, commands, and application services
- Infrastructure Layer: Contains database repositories and external service implementations
- Presentation Layer: Contains UI components, forms, and server actions
Each feature module follows this structure:
src/modules/{module-name}/
??? domain/ # Business logic, entities, value objects
??? application/ # Use cases, commands, queries
??? infrastructure/ # Database repositories, external services
??? presentation/ # UI components, forms, server actions
The shared module contains:
src/modules/shared/domain/- Common domain utilities (DateTime, Primitives, ValueObject)src/modules/shared/presentation/ui/- Reusable UI components (shadcn/ui based)src/modules/shared/application/- Common use case patterns
Install dependencies with pnpm install --frozen-lockfile to stay aligned with CI. Use pnpm dev for hot-reload development, pnpm build for production bundles, and pnpm preview to inspect the compiled output. Run pnpm astro check whenever you touch Astro collections or TypeScript types to catch regressions early.
- Git Hooks: Use simple-git-hooks for pre-commit formatting
- Code Formatting: Use Prettier with Tailwind plugin
- Linting: Use commitlint for conventional commits
- Type Checking: Run
pnpm astro checkregularly
- Use strict TypeScript configuration
- Follow the established path aliases:
@/ui/*?./src/modules/shared/presentation/ui/*@/shared/*?./src/modules/shared/*@/{module}/*?./src/modules/{module}/*@/*?./src/*
Prettier enforces two-space indentation, single quotes, trailing commas, and no semicolons; format large sets of changes with pnpm exec prettier --write .. Tailwind utility order is normalized by prettier-plugin-tailwindcss, so keep class lists descriptive rather than rearranging manually.
- No Line-by-Line Comments: Write self-documenting code that explains intent through clear naming and structure
- Semantic Code: Use descriptive variable, function, and class names that explain purpose without comments
- Single Responsibility: Each function/class should have one reason to change
- Meaningful Names: Use pronounceable, searchable names that reveal intent
- Small Functions: Keep functions under 20 lines with descriptive names
- No Magic Numbers: Extract constants with meaningful names
- Explicit Intent: Code should read like well-written prose
Example of Clean Code:
// ❌ Avoid this
const calculate = (a: number, b: number): number => {
// Calculate the total price including tax
const tax = 0.21 // 21% tax rate
return a * b * (1 + tax)
}
// ✅ Prefer this
const calculateTotalPriceWithTax = (unitPrice: number, quantity: number): number => {
const STANDARD_TAX_RATE = 0.21
return unitPrice * quantity * (1 + STANDARD_TAX_RATE)
}File Naming Conventions:
- Components: PascalCase (e.g.,
EventEditForm.tsx) - Hooks: camelCase starting with 'use' (e.g.,
useUploadFile.ts) - Utilities: camelCase (e.g.,
datetime.ts) - Types: PascalCase (e.g.,
EventData.ts) - Constants: UPPER_SNAKE_CASE (e.g.,
DATE_FORMATS.ts) - Route-level
.astrofiles: kebab-case to match Astro expectations - React components: PascalCase
- Hooks and utilities: camelCase
- Entities: Use private constructors with static factory methods
- Value Objects: Extend the base
ValueObject<T>class - Primitives: Use the
Primitives<T>type for serialization/deserialization - Validation: Implement domain validators for business rules
- IDs: Use dedicated ID classes (e.g.,
EventId,OrganizationId)
Example Entity Pattern:
export class Event implements EventProps {
private constructor(props: EventProps) {
/* ... */
}
static fromPrimitives(primitives: Primitives<Event>): Event {
/* ... */
}
toPrimitives(): Primitives<Event> {
/* ... */
}
static create(data: EventData, organizationId: string): Event {
/* ... */
}
}- Use Cases: Extend the base
UseCase<Param, Result>class - Commands: Extend the
Command<Param, Result>class for write operations - Queries: Use regular functions for read operations
- Dependency Injection: Use constructor injection for dependencies
Example Use Case Pattern:
export class CreateEventCommand extends Command<Param, void> {
constructor(private readonly eventsRepository: EventsRepository) {
super()
}
async execute(param: Param): Promise<void> {
/* ... */
}
}
### Infrastructure Layer Patterns
- **Mappers**: Use mapper classes to convert between database objects and domain entities.
- **DTOs (Data Transfer Objects)**: Define interfaces for raw database objects to ensure type safety in mappers.
- **Organization**:
- Place mappers in `infrastructure/mappers/`.
- Place DTOs in `infrastructure/dtos/`.
- Always extract DTO interfaces to their own files.
- **Naming Conventions**:
- DTOs: `AstroDb{Entity}Dto` (e.g., `AstroDbBugDto`)
- Mappers: `AstroDb{Entity}Mapper` (e.g., `AstroDbBugMapper`)
Example Mapper Pattern:
```typescript
import { Bug } from '../../domain/bug'
import type { AstroDbBugDto } from '../dtos/astro-db-bug.dto'
export class AstroDbBugMapper {
static toDomain(raw: AstroDbBugDto, userName?: string): Bug {
return new Bug({
/* ... mapping logic ... */
})
}
static toPersistence(bug: Bug) {
return {
/* ... mapping logic ... */
}
}
}
## Dependency Injection Patterns
The project uses **diod** library for dependency injection without decorators. Dependencies are manually defined in containers and resolved through constructor injection.
### Container Setup
- **No Decorators**: Avoid using `@Service` or similar decorators
- **Manual Registration**: Define all dependencies manually in container files
- **Constructor Injection**: Dependencies are injected through constructors
- **Module Containers**: Each module has its own container definition
- **ContainerBuilder**: Use ContainerBuilder for creating containers
- **CamelCase Exports**: Container names must be in CamelCase (e.g., `provincesContainer`)
Example Container Pattern:
```typescript
import { ContainerBuilder } from 'diod'
export const eventsContainer = new ContainerBuilder()
.register(
EventsRepository,
AstroDBEventsRepository,
{ scope: 'singleton' }
)
.register(
CreateEventCommand,
CreateEventCommand,
{ scope: 'transient' }
)
.build()
Resolve dependencies from the appropriate container when needed:
const createEventCommand = eventsContainer.get(CreateEventCommand)
await createEventCommand.execute(eventData)- React Components: Use functional components with TypeScript
- Forms: Use react-hook-form with Zod validation
- Server Actions: Use Astro's server actions for form submissions
- Client Components: Mark with
'use client'directive when needed
- shadcn/ui: Use the established component library
- Styling: Use Tailwind CSS with the
cn()utility for class merging - Variants: Use class-variance-authority for component variants
- Icons: Use Lucide React icons consistently
Example Component Pattern:
const buttonVariants = cva(
"base-classes",
{
variants: { /* ... */ },
defaultVariants: { /* ... */ }
}
)
function Button({ className, variant, size, ...props }: Props) {
return <button className={cn(buttonVariants({ variant, size, className }))} {...props} />
}- Schema: Define tables in
db/config.tsusing Astro DB - Migrations: Use the content migration system
- IMPORTANT: Never run database migrations automatically. After modifying
db/config.ts, only runpnpm exec astro db pushwhen explicitly instructed by the user - Repositories: Define interfaces as abstract classes in domain layer, implement in infrastructure layer
- Primitives: Convert between domain objects and database primitives
- When altering database tables or seeds, run the relevant script in
db/and document the dataset or migration steps in your PR
- Validation: Use Zod schemas for form validation
- Error Handling: Display errors using FormMessage components
- File Uploads: Use the
useUploadFilehook for image uploads - Date Handling: Use the
Datetimeutility class for date operations
- Local State: Use React hooks (useState, useEffect)
- Form State: Use react-hook-form
- Server State: Use Astro server actions
- Global State: Use React Context when needed
- Domain Errors: Create specific error classes (e.g.,
InvalidEventError) - Form Errors: Use react-hook-form error handling
- Toast Notifications: Use Sonner for user feedback
- Validation: Use Zod for runtime validation
An automated suite is not in place yet; add coverage alongside the feature you touch (e.g. src/modules/events/__tests__/event-form.spec.ts) and keep fixtures small. At minimum, exercise critical flows (creating events, joining meetups, auth) against pnpm dev before requesting a review.
- Unit Tests: Test domain logic and use cases
- Integration Tests: Test server actions and API endpoints
- Component Tests: Test React components with user interactions
- E2E Tests: Test complete user flows
- Code Splitting: Use dynamic imports for large components
- Image Optimization: Use Sharp for image processing
- Bundle Size: Monitor and optimize bundle size
- Lazy Loading: Implement lazy loading for heavy components
- Input Validation: Always validate user input
- Authentication: Use Lucia for session management
- Authorization: Check permissions before operations
- CSRF Protection: Use Astro's built-in CSRF protection
This repo uses @commitlint/config-conventional, so follow type(scope): subject (feat(events): add recurring schedules) and keep subjects under 72 characters. Enable the local hooks with pnpm exec simple-git-hooks; the pre-commit hook runs pretty-quick to enforce formatting prior to linting. PRs should describe the problem, the approach, any manual testing, and include screenshots for UI work; call out new .env keys or config changes so reviewers can reproduce.
- Create domain entity with private constructor
- Add value objects for IDs and complex types
- Implement fromPrimitives/toPrimitives methods
- Add domain validators
- Create repository interface
- Implement infrastructure repository
- Create use cases for operations
- Set up dependency injection container for the module
- Add presentation components
- Define Zod schema for validation
- Create form component with react-hook-form
- Add server action for form submission
- Implement error handling and loading states
- Add proper accessibility attributes
- Use shadcn/ui as base when possible
- Add proper TypeScript types
- Use class-variance-authority for variants
- Add proper accessibility support
- Export from appropriate index files
Do not create barrel files (index.ts files that re-export modules). Import directly from the source files to maintain clear dependencies and avoid circular imports. This applies to:
- Domain entities and value objects
- Application use cases and commands
- Infrastructure repositories
- Presentation components
- Shared utilities
Example of correct imports:
// ❌ Don't do this
import { Event, EventId } from '@/events/domain'
// ✅ Do this instead
import { Event } from '@/events/domain/event'
import { EventId } from '@/events/domain/event-id'- No
window.location.reload(): Do not use full page reloads for refreshing data. Instead, use Astro's View Transitionsnavigate()function with{ history: 'replace' }option to refresh data without a full page reload.
Correct pattern for refreshing data:
import { navigate } from 'astro:transitions/client'
const refreshPage = () => {
const currentUrl = new URL(window.location.href)
navigate(currentUrl.pathname + currentUrl.search, { history: 'replace' })
}- Astro: 5.14.8 (SSG/SSR framework)
- React: 18.2.0 (UI library)
- TypeScript: 5.3.3 (Type system)
- Tailwind CSS: 4.1.3 (Styling)
- shadcn/ui: UI component library
- Zod: 3.24.2 (Validation)
- react-hook-form: 7.55.0 (Form management)
- Lucia: 3.2.2 (Authentication)
- Astro DB: 0.18.0 (Database)
- diod: Dependency injection container
BASE_URL: Public base URLASTRO_DB_REMOTE_URL: Database connectionASTRO_DB_APP_TOKEN: Database tokenOAUTH_GITHUB_CLIENT_ID/SECRET: GitHub OAuthGOOGLE_CLIENT_ID/SECRET: Google OAuthTWITTER_CLIENT_ID/SECRET: Twitter OAuthPINATA_JWT: IPFS storagePINATA_GATEWAY_URL: IPFS gatewayGOOGLE_MAPS_PLACES_API_KEYPUBLIC_GOOGLE_MAPS_EMBED_API_KEYRESEND_API_KEY
- Development:
pnpm dev - Build:
pnpm build - Preview:
pnpm preview - Deployment: Netlify with server-side rendering
- Database: Astro DB with remote connection
Remember to follow these patterns consistently throughout the codebase to maintain code quality and architectural integrity.