Skip to content
Merged
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
126 changes: 126 additions & 0 deletions echo/docs/modular_component_pattern.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# Modular Component Pattern

## Overview

The monolithic `frontend/src/lib/query.ts` file has been **dissected and removed**. We now follow a modular component pattern where each domain has its own directory with components, hooks, and utilities.

## 🚨 Important Changes

- **`query.ts` no longer exists** - all functionality moved to modular directories
- **New import patterns** - hooks imported from component directories
- **Domain-based organization** - related functionality grouped together

## Directory Structure

Each component domain follows this pattern:

```
frontend/src/components/{domain}/
|
├── ComponentName.tsx # React components
├── hooks/
│ └── index.ts # Domain-specific React Query hooks
└── utils/
└── index.ts # Domain-specific utilities
```

## Where to Find Functionality

### Component Domains
- **Participant**: `@/components/participant/hooks`
- **Project**: `@/components/project/hooks`
- **Chat**: `@/components/chat/hooks`
- **Auth**: `@/components/auth/hooks`
and so on...

## Import Patterns

### ❌ Old Way (No longer works)
```typescript
import { useProjectById } from '@/lib/query';
```

### ✅ New Way
```typescript
import { useProjectById } from '@/components/project/hooks';
```

## Creating New Components

### 1. Create Directory Structure
```bash
frontend/src/components/{domain}/
├── hooks/
│ └── index.ts
└── utils/
└── index.ts
```

### 2. Export Hooks
```typescript
// frontend/src/components/{domain}/hooks/index.ts
export const useDomainQuery = () => {
// Implementation
};

export const useDomainMutation = () => {
// Implementation
};
```

### 3. Import in Components
```typescript
import { useDomainQuery, useDomainMutation } from '@/components/{domain}/hooks';
```

## Guidelines

### 1. **Organization**
- Keep related functionality together
- Use descriptive names that indicate the domain
- Export all hooks from `hooks/index.ts`

### 2. **File Naming**
- Components: `PascalCase.tsx`
- Hooks/Utils/Types: `index.ts` (within respective directories)

### 3. **Code Structure**
- Group mutations and queries logically
- Add JSDoc comments for complex functions
- Follow existing patterns in the codebase

## Troubleshooting

### "Cannot find module '@/lib/query'"
- The file no longer exists
- Find the hook in the appropriate component directory
- Use the new import pattern

### "Hook not found"
- Search the codebase for the hook name
- Check component directories for the relevant domain
- Look in shared library files

### "Import path error"
- Ensure you're using the correct path format
- Check that the hook is exported from the index.ts file

## Benefits

- **Better Organization**: Related functionality grouped together
- **Easier Navigation**: Find specific features quickly
- **Reduced Conflicts**: Less merge conflicts between developers
- **Clear Ownership**: Each domain has its own space
- **Improved Testing**: Test individual domains in isolation
- **Better Scalability**: Easy to add new domains

## Migration Checklist

When working with existing code or creating new features:

- [ ] Check if functionality belongs in a specific component domain
- [ ] Use the appropriate import path for hooks
- [ ] Follow the established directory structure
- [ ] Keep related functionality grouped together
- [ ] Test that imports work correctly
- [ ] Verify that the component follows the modular pattern
2 changes: 1 addition & 1 deletion echo/frontend/src/components/announcement/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import {
import * as Sentry from "@sentry/react";
import { Query, readItems, createItems, aggregate } from "@directus/sdk";
import { directus } from "@/lib/directus";
import { useCurrentUser } from "@/lib/query";
import { toast } from "@/components/common/Toaster";
import { t } from "@lingui/core/macro";
import { useCurrentUser } from "@/components/auth/hooks";

export const useLatestAnnouncement = () => {
const { data: currentUser } = useCurrentUser();
Expand Down
2 changes: 1 addition & 1 deletion echo/frontend/src/components/aspect/AspectCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Box, Button, LoadingOverlay, Paper, Stack, Text } from "@mantine/core";
import { IconArrowsDiagonal } from "@tabler/icons-react";
import { useParams } from "react-router-dom";
import { I18nLink } from "@/components/common/i18nLink";
import { useProjectById } from "@/lib/query";
import { useProjectById } from "@/components/project/hooks";

export const AspectCard = ({
data,
Expand Down
182 changes: 182 additions & 0 deletions echo/frontend/src/components/auth/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import { toast } from "@/components/common/Toaster";
import { useI18nNavigate } from "@/hooks/useI18nNavigate";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { directus } from "@/lib/directus";
import {
passwordRequest,
passwordReset,
readUser,
registerUser,
registerUserVerify,
} from "@directus/sdk";
import { ADMIN_BASE_URL } from "@/config";
import { throwWithMessage } from "../utils/errorUtils";

export const useCurrentUser = () =>
useQuery({
queryKey: ["users", "me"],
queryFn: () => {
try {
return directus.request(readUser("me"));
} catch (error) {
return null;
}
},
});

export const useResetPasswordMutation = () => {
const navigate = useI18nNavigate();
return useMutation({
mutationFn: async ({
token,
password,
}: {
token: string;
password: string;
}) => {
try {
const response = await directus.request(passwordReset(token, password));
return response;
} catch (e) {
throwWithMessage(e);
}
},
onSuccess: () => {
toast.success(
"Password reset successfully. Please login with new password.",
);
navigate("/login");
},
onError: (e) => {
try {
toast.error(e.message);
} catch (e) {
toast.error("Error resetting password. Please contact support.");
}
},
});
};

export const useRequestPasswordResetMutation = () => {
const navigate = useI18nNavigate();
return useMutation({
mutationFn: async (email: string) => {
try {
const response = await directus.request(
passwordRequest(email, `${ADMIN_BASE_URL}/password-reset`),
);
return response;
} catch (e) {
throwWithMessage(e);
}
},
onSuccess: () => {
toast.success("Password reset email sent successfully");
navigate("/check-your-email");
},
onError: (e) => {
toast.error(e.message);
},
});
};

export const useVerifyMutation = (doRedirect: boolean = true) => {
const navigate = useI18nNavigate();

return useMutation({
mutationFn: async (data: { token: string }) => {
try {
const response = await directus.request(registerUserVerify(data.token));
return response;
} catch (e) {
throwWithMessage(e);
}
},
onSuccess: () => {
toast.success("Email verified successfully.");
if (doRedirect) {
setTimeout(() => {
// window.location.href = `/login?new=true`;
navigate(`/login?new=true`);
}, 4500);
}
},
onError: (e) => {
toast.error(e.message);
},
});
};

export const useRegisterMutation = () => {
const navigate = useI18nNavigate();
return useMutation({
mutationFn: async (payload: Parameters<typeof registerUser>) => {
try {
const response = await directus.request(registerUser(...payload));
return response;
} catch (e) {
try {
throwWithMessage(e);
} catch (inner) {
if (inner instanceof Error) {
if (inner.message === "You don't have permission to access this.") {
throw new Error(
"Oops! It seems your email is not eligible for registration at this time. Please consider joining our waitlist for future updates!",
);
}
}
}
}
},
onSuccess: () => {
toast.success("Please check your email to verify your account.");
navigate("/check-your-email");
},
onError: (e) => {
toast.error(e.message);
},
});
};

// todo: add redirection logic here
export const useLoginMutation = () => {
return useMutation({
mutationFn: (payload: Parameters<typeof directus.login>) => {
return directus.login(...payload);
},
onSuccess: () => {
toast.success("Login successful");
},
});
};

export const useLogoutMutation = () => {
const queryClient = useQueryClient();
const navigate = useI18nNavigate();

return useMutation({
mutationFn: async ({
next: _,
}: {
next?: string;
reason?: string;
doRedirect: boolean;
}) => {
try {
await directus.logout();
} catch (e) {
throwWithMessage(e);
}
},
onMutate: async ({ next, reason, doRedirect }) => {
queryClient.resetQueries();
if (doRedirect) {
navigate(
"/login" +
(next ? `?next=${encodeURIComponent(next)}` : "") +
(reason ? `&reason=${reason}` : ""),
);
}
},
});
};
22 changes: 22 additions & 0 deletions echo/frontend/src/components/auth/utils/errorUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// always throws a error with a message
export function throwWithMessage(e: unknown): never {
if (
e &&
typeof e === "object" &&
"errors" in e &&
Array.isArray((e as any).errors)
) {
// Handle Directus error format
const message = (e as any).errors[0].message;
console.log(message);
throw new Error(message);
} else if (e instanceof Error) {
// Handle generic errors
console.log(e.message);
throw new Error(e.message);
} else {
// Handle unknown errors
console.log("An unknown error occurred");
throw new Error("Something went wrong");
}
}
6 changes: 1 addition & 5 deletions echo/frontend/src/components/chat/ChatAccordion.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import {
useDeleteChatMutation,
useProjectChats,
useUpdateChatMutation,
} from "@/lib/query";
import { useDeleteChatMutation, useProjectChats, useUpdateChatMutation } from "./hooks";
import {
Accordion,
ActionIcon,
Expand Down
2 changes: 1 addition & 1 deletion echo/frontend/src/components/chat/ChatContextProgress.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { t } from "@lingui/core/macro";
import { useProjectChatContext } from "@/lib/query";
import { capitalize } from "@/lib/utils";
import { Box, Progress, Skeleton, Tooltip } from "@mantine/core";
import { ENABLE_CHAT_AUTO_SELECT } from "@/config";
import { useProjectChatContext } from "./hooks";

export const ChatContextProgress = ({ chatId }: { chatId: string }) => {
const chatContextQuery = useProjectChatContext(chatId);
Expand Down
Loading