diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..52601e4 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,241 @@ +## Comprehensive Project Structure Overview + +I've explored the Modelence application codebase. Here's a detailed breakdown of what's available: + +### 1. PROJECT STRUCTURE + +``` +/user-app/ +├── src/ +│ ├── client/ # React frontend +│ │ ├── assets/ # Images/logos (favicon.svg, modelence.svg) +│ │ ├── components/ +│ │ │ ├── ui/ # Reusable UI components (shadcn-style) +│ │ │ │ ├── Button.tsx +│ │ │ │ ├── Input.tsx +│ │ │ │ ├── Label.tsx +│ │ │ │ └── Card.tsx +│ │ │ ├── LoadingSpinner.tsx # Custom loading component +│ │ │ └── Page.tsx # Page wrapper with header +│ │ ├── pages/ # Route pages +│ │ │ ├── HomePage.tsx +│ │ │ ├── LoginPage.tsx +│ │ │ ├── SignupPage.tsx +│ │ │ ├── ExamplePage.tsx +│ │ │ ├── PrivateExamplePage.tsx +│ │ │ ├── LogoutPage.tsx +│ │ │ ├── TermsPage.tsx +│ │ │ └── NotFoundPage.tsx +│ │ ├── lib/ +│ │ │ └── utils.ts # Utility functions (cn helper) +│ │ ├── router.tsx # React Router configuration +│ │ ├── index.tsx # App entry point +│ │ ├── types.d.ts +│ │ └── index.css +│ │ +│ └── server/ # Node.js backend +│ ├── app.ts # Server entry point +│ └── example/ +│ ├── index.ts # Module definition with queries/mutations +│ ├── db.ts # Database schemas +│ └── cron.ts # Scheduled jobs +│ +├── Configuration Files +│ ├── tsconfig.json # TypeScript config with @/* path alias +│ ├── tailwind.config.js # Tailwind CSS setup +│ ├── vite.config.ts # Vite bundler config +│ ├── postcss.config.js +│ └── modelence.config.ts # Modelence framework config +│ +└── package.json # Dependencies & scripts +``` + +### 2. AVAILABLE UI COMPONENTS (SHADCN-STYLE) + +All components are custom implementations located in `/user-app/src/client/components/ui/`: + +#### Button Component (`/user-app/src/client/components/ui/Button.tsx`) +- **Variants**: default, destructive, outline, secondary, ghost, link +- **Sizes**: default, sm, lg, icon +- **Features**: Forward ref, fully styled with Tailwind, hover/active states +- **Props**: `ButtonProps extends React.ButtonHTMLAttributes` + +#### Input Component (`/user-app/src/client/components/ui/Input.tsx`) +- **Features**: Forward ref, styled with Tailwind +- **Supports**: All standard HTML input attributes +- **Styling**: Border, focus ring, dark mode, placeholder colors + +#### Label Component (`/user-app/src/client/components/ui/Label.tsx`) +- **Features**: Semantic label element with peer-disabled states +- **Props**: `LabelProps extends React.LabelHTMLAttributes` + +#### Card Component (`/user-app/src/client/components/ui/Card.tsx`) +- **Subcomponents**: + - `Card` - Main container + - `CardHeader` - Header section with padding + - `CardTitle` - Title text styling + - `CardDescription` - Description text styling + - `CardContent` - Content wrapper + - `CardFooter` - Footer section + +All components use the `cn()` utility function for class merging. + +### 3. UTILITY FUNCTIONS + +**File**: `/user-app/src/client/lib/utils.ts` + +```typescript +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} +``` +- Uses `clsx` for conditional classes +- Uses `tailwind-merge` to prevent class conflicts +- Perfect for merging component classes with custom overrides + +### 4. EXISTING FORM PATTERNS + +The app already has two working form examples you can reference: + +#### LoginForm (`/user-app/src/client/pages/LoginPage.tsx`) +- Email and password fields +- `FormData` API for form submission +- Card-based layout with headers and footers +- Validation and error handling +- Links to signup + +#### SignupForm (`/user-app/src/client/pages/SignupPage.tsx`) +- Email, password, confirm password +- Checkbox for terms acceptance +- Success state handling +- Client-side password validation +- Toast error notifications +- `useCallback` hook for form submission +- State management for success state + +### 5. APP STRUCTURE & ARCHITECTURE + +#### Client Setup (`/user-app/src/client/index.tsx`) +```typescript +- React Query (TanStack) integration +- React Router DOM +- React Hot Toast for notifications +- Suspense boundaries with loading state +- Global error handler +``` + +#### Router Configuration (`/user-app/src/client/router.tsx`) +- **Public Routes**: Home, Example, Terms, Logout, 404 +- **Guest Routes**: Login, Signup (redirects to home if authenticated) +- **Private Routes**: PrivateExamplePage (redirects to login if not authenticated) +- **Route Protection**: + - `GuestRoute` component for auth-only pages + - `PrivateRoute` component for protected pages + - Redirect with `_redirect` query param to return after login + +#### Page Wrapper (`/user-app/src/client/components/Page.tsx`) +- Header with logo, user info, logout button +- Responsive layout with max-width +- Body section with optional loading state +- Built-in navigation + +### 6. MODULE SYSTEM (Backend) + +**File**: `/user-app/src/server/example/index.ts` + +Example shows Module pattern with: + +```typescript +new Module('example', { + configSchema: { /* configuration */ }, + stores: [ /* database stores */ ], + queries: { + getItem: async (args, { user }) => { /* query logic */ }, + getItems: async (args, { user }) => { /* query logic */ } + }, + mutations: { + createItem: async (args, { user }) => { /* mutation logic */ }, + updateItem: async (args, { user }) => { /* mutation logic */ } + }, + cronJobs: { + dailyTest: dailyTestCron + } +}) +``` + +#### Database Pattern (`/user-app/src/server/example/db.ts`) +```typescript +export const dbExampleItems = new Store('exampleItems', { + schema: { + title: schema.string(), + createdAt: schema.date(), + userId: schema.userId(), + }, + indexes: [] +}); +``` + +### 7. KEY DEPENDENCIES + +From `package.json`: +```json +{ + "@modelence/react-query": "^1.0.2", // Modelence + React Query integration + "@tanstack/react-query": "^5.90.12", // Server state management + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.22.0", // Client routing + "react-hot-toast": "^2.4.1", // Toast notifications + "zod": "^4.1.13", // Schema validation + "tailwindcss": "^3.4.1", // Styling + "clsx": "^2.1.1", // Class utilities + "tailwind-merge": "^3.4.0" // Class merging +} +``` + +### 8. BUILD & DEVELOPMENT + +**Scripts** (from package.json): +```bash +npm run dev # Development server +npm run build # Production build +npm start # Start production server +npm test # Run tests (not configured) +``` + +**Vite Configuration**: +- Root: `src/client` +- Path alias: `@/` → `./src/` +- Dev server: `0.0.0.0:5173` (allows external access) +- React plugin enabled + +### 9. STYLING SETUP + +- **Tailwind CSS**: Configured with `src/client/**/*.{js,jsx,ts,tsx}` content paths +- **PostCSS**: Enabled with autoprefixer +- **Dark Mode**: Supported in all components (dark: prefixed classes) +- **Color Scheme**: Gray, black, white primary colors; blue, red accents + +### 10. AVAILABLE PATTERNS FOR TODO LIST FORM + +You can reuse: + +1. **Form Structure**: FormData API like in LoginPage/SignupPage +2. **Validation**: Zod on backend, client-side checks in form +3. **UI Components**: Button, Input, Label, Card for form container +4. **Page Layout**: Use Page wrapper component +5. **Toast Notifications**: `react-hot-toast` for feedback +6. **State Management**: React Query for server state +7. **Hooks**: `useCallback`, `useState`, `useMutation`, `useQuery` +8. **Styling**: Use `cn()` utility to combine classes + +### Summary + +This is a full-stack Modelence framework application with: +- Clean component structure ready for a todo list feature +- All necessary UI building blocks already available +- Form handling patterns established +- Database and backend module patterns ready to follow +- Authentication system in place +- TypeScript support throughout +- No external shadcn/ui dependency needed - custom components are already implemented diff --git a/src/client/components/Page.tsx b/src/client/components/Page.tsx index cb13d7f..443d6b7 100644 --- a/src/client/components/Page.tsx +++ b/src/client/components/Page.tsx @@ -7,10 +7,12 @@ import { Link } from 'react-router-dom'; import { useSession } from 'modelence/client'; import LoadingSpinner from '@/client/components/LoadingSpinner'; import { Button } from '@/client/components/ui/Button'; +import { cn } from '@/client/lib/utils'; interface PageProps { children?: React.ReactNode; isLoading?: boolean; + className?: string; } function Header() { @@ -44,10 +46,10 @@ function PageWrapper({ children }: { children: React.ReactNode }) { return
{children}
; } -function PageBody({ children, isLoading = false }: PageProps) { +function PageBody({ children, className, isLoading = false }: PageProps) { return ( -
-
+
+
{isLoading ? (
@@ -60,11 +62,11 @@ function PageBody({ children, isLoading = false }: PageProps) { ); } -export default function Page({ children, isLoading = false }: PageProps) { +export default function Page({ children, className, isLoading = false }: PageProps) { return (
- {children} + {children} ); } diff --git a/src/client/pages/ExamplePage.tsx b/src/client/pages/ExamplePage.tsx index 3d490b1..71bcbf3 100644 --- a/src/client/pages/ExamplePage.tsx +++ b/src/client/pages/ExamplePage.tsx @@ -1,7 +1,7 @@ import { useCallback } from 'react'; import { useParams } from 'react-router-dom'; -import { useQuery, useQueryClient } from '@tanstack/react-query'; -import { modelenceQuery, createQueryKey } from '@modelence/react-query'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { modelenceQuery, modelenceMutation, createQueryKey } from '@modelence/react-query'; type ExampleItem = { title: string; @@ -17,6 +17,10 @@ export default function ExamplePage() { enabled: !!itemId, }); + const { mutate: createItem, isPending: isCreatingItem } = useMutation({ + ...modelenceMutation('example.createItem'), + }); + const invalidateItem = useCallback(() => { queryClient.invalidateQueries({ queryKey: createQueryKey('example.getItem', { itemId }) }); }, [queryClient, itemId]); @@ -33,6 +37,7 @@ export default function ExamplePage() { )} +
); } diff --git a/src/client/pages/HomePage.tsx b/src/client/pages/HomePage.tsx index cfbc2dd..f581ace 100644 --- a/src/client/pages/HomePage.tsx +++ b/src/client/pages/HomePage.tsx @@ -1,34 +1,44 @@ import logo from '@/client/assets/modelence.svg'; +import Page from '@/client/components/Page'; export default function HomePage() { return ( -
-
-
- Modelence Logo -
-

Hello, World!

-

Welcome to your new Modelence app

- -
-

- This is your home page placeholder - {' '} - - src/client/pages/HomePage.tsx - -

-
+ +
+ +
+
+ ); +} + +// TODO: Replace with actual content +function PlaceholderView() { + return ( +
+
+ Modelence Logo +
+

Hello, World!

+

Welcome to your new Modelence app

+ +
+

+ This is your home page placeholder - {' '} + + src/client/pages/HomePage.tsx + +

+
- +
); diff --git a/src/client/pages/NotFoundPage.tsx b/src/client/pages/NotFoundPage.tsx index c4f7840..d36e9bf 100644 --- a/src/client/pages/NotFoundPage.tsx +++ b/src/client/pages/NotFoundPage.tsx @@ -7,12 +7,12 @@ export default function NotFoundPage() { return (
- + 404 - +

Page not found

diff --git a/src/server/example/cron.ts b/src/server/example/cron.ts new file mode 100644 index 0000000..4461a17 --- /dev/null +++ b/src/server/example/cron.ts @@ -0,0 +1,9 @@ +import { time } from 'modelence'; + +export const dailyTestCron = { + description: 'Daily cron job example', + interval: time.days(1), + handler: async () => { + // This is just an example. Any code written here will run daily. + }, +}; diff --git a/src/server/example/db.ts b/src/server/example/db.ts index 140cf56..5d03747 100644 --- a/src/server/example/db.ts +++ b/src/server/example/db.ts @@ -4,6 +4,7 @@ export const dbExampleItems = new Store('exampleItems', { schema: { title: schema.string(), createdAt: schema.date(), + userId: schema.userId(), }, indexes: [] }); diff --git a/src/server/example/index.ts b/src/server/example/index.ts index f25efb9..8ee0666 100644 --- a/src/server/example/index.ts +++ b/src/server/example/index.ts @@ -1,18 +1,88 @@ import z from 'zod'; -import { Module, ObjectId } from 'modelence/server'; +import { AuthError } from 'modelence'; +import { Module, ObjectId, UserInfo, getConfig } from 'modelence/server'; import { dbExampleItems } from './db'; +import { dailyTestCron } from './cron'; export default new Module('example', { + configSchema: { + itemsPerPage: { + type: 'number', + default: 5, + isPublic: false, + }, + }, + stores: [dbExampleItems], queries: { - getItem: async (args: unknown) => { + getItem: async (args: unknown, { user }: { user: UserInfo | null }) => { + if (!user) { + throw new AuthError('Not authenticated'); + } + const { itemId } = z.object({ itemId: z.string() }).parse(args); const exampleItem = await dbExampleItems.requireOne({ _id: new ObjectId(itemId) }); + + if (exampleItem.userId.toString() !== user.id) { + throw new AuthError('Not authorized'); + } + return { title: exampleItem.title, createdAt: exampleItem.createdAt, }; }, + + getItems: async (_args: unknown, { user }: { user: UserInfo | null }) => { + if (!user) { + throw new AuthError('Not authenticated'); + } + + const itemsPerPage = getConfig('example.itemsPerPage') as number; + const exampleItems = await dbExampleItems.fetch({ + userId: new ObjectId(user.id), + }, { limit: itemsPerPage }) + return exampleItems.map((item) => ({ + _id: item._id.toString(), + title: item.title, + createdAt: item.createdAt, + })); + } }, + + mutations: { + createItem: async (args: unknown, { user }: { user: UserInfo | null }) => { + if (!user) { + throw new AuthError('Not authenticated'); + } + + const { title } = z.object({ title: z.string() }).parse(args); + + await dbExampleItems.insertOne({ title, createdAt: new Date(), userId: new ObjectId(user.id) }); + }, + + updateItem: async (args: unknown, { user }: { user: UserInfo | null }) => { + if (!user) { + throw new AuthError('Not authenticated'); + } + + const { itemId, title } = z.object({ itemId: z.string(), title: z.string() }).parse(args); + + const exampleItem = await dbExampleItems.requireOne({ _id: new ObjectId(itemId) }); + if (exampleItem.userId.toString() !== user.id) { + throw new AuthError('Not authorized'); + } + + const { modifiedCount } = await dbExampleItems.updateOne({ _id: new ObjectId(itemId) }, { $set: { title } }); + + if (modifiedCount === 0) { + throw new Error('Item not found'); + } + }, + }, + + cronJobs: { + dailyTest: dailyTestCron + } });