diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index c8abc77..045228e 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -335,6 +335,87 @@ const taskService = new TaskService(apiClient); - Services handle all error states - check `result.success` before accessing data - Services return consistent response format: `{ success, status, data?, error? }` +## Using Swagger MCP for Backend API + +This project has access to the **Swagger MCP server** for exploring the backend API definition. The Swagger definition is automatically downloaded and converted from YAML to JSON during the Copilot setup steps. + +### Pre-configured Files + +The setup creates: +- `swagger.json` - The converted Swagger/OpenAPI definition +- `.swagger-mcp` - Configuration file with the path to swagger.json + +### Available Swagger MCP Tools + +#### 1. listEndpoints + +Lists all available API endpoints with their HTTP methods and descriptions: + +``` +swagger-listEndpoints(swaggerFilePath: "/path/to/swagger.json") +``` + +#### 2. listEndpointModels + +Shows the request/response models for a specific endpoint: + +``` +swagger-listEndpointModels( + swaggerFilePath: "/path/to/swagger.json", + path: "/groups/", + method: "GET" +) +``` + +#### 3. generateModelCode + +Generates TypeScript interfaces from Swagger model definitions: + +``` +swagger-generateModelCode( + swaggerFilePath: "/path/to/swagger.json", + modelName: "Group" +) +``` + +#### 4. generateEndpointToolCode + +Generates TypeScript code for calling an API endpoint: + +``` +swagger-generateEndpointToolCode( + swaggerFilePath: "/path/to/swagger.json", + path: "/groups/", + method: "GET" +) +``` + +### When to Use Swagger MCP + +- **Exploring new API endpoints**: Use `listEndpoints` to discover available APIs +- **Understanding request/response formats**: Use `listEndpointModels` and `generateModelCode` +- **Creating new services**: Reference the generated TypeScript interfaces for DTOs +- **Verifying API contracts**: Ensure your service implementations match the backend API + +### Example Workflow + +1. List all endpoints to find the one you need: + ``` + swagger-listEndpoints(swaggerFilePath: "swagger.json") + ``` + +2. Get the models for that endpoint: + ``` + swagger-listEndpointModels(swaggerFilePath: "swagger.json", path: "/groups/", method: "GET") + ``` + +3. Generate TypeScript interface for the model: + ``` + swagger-generateModelCode(swaggerFilePath: "swagger.json", modelName: "Group") + ``` + +4. Use the generated interface to create your DTO in `/src/lib/dto/` + ## Internationalization (i18n) This project uses **Paraglide** for internationalization with support for **English (en)** and **Polish (pl)**. diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index ebeb561..4b424c3 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -43,6 +43,20 @@ jobs: npm run build echo "Swagger MCP server built successfully" + - name: Download and convert Swagger definition + run: | + # Download YAML swagger definition from backend docs + curl -s "https://mini-maxit.github.io/backend/master/swagger.yaml" -o /tmp/swagger.yaml + + # Convert YAML to JSON using Python (available on ubuntu-latest) + python3 -c "import yaml, json; print(json.dumps(yaml.safe_load(open('/tmp/swagger.yaml')), indent=2))" > swagger.json + + # Create .swagger-mcp config file pointing to the JSON file + echo "SWAGGER_FILEPATH=$(pwd)/swagger.json" > .swagger-mcp + + echo "Swagger definition downloaded and converted successfully" + echo "Config file created at .swagger-mcp" + - name: Prepare project (sync, typecheck, lint) # Using || true to allow Copilot to start even with minor typecheck/lint issues run: | diff --git a/.gitignore b/.gitignore index 7c6e593..05b8a8f 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,10 @@ vite.config.ts.timestamp-* # Paraglide src/lib/paraglide + +# Logs +logs + +# Swagger MCP +swagger.json +.swagger-mcp diff --git a/messages/en.json b/messages/en.json index e68f2c4..184c15d 100644 --- a/messages/en.json +++ b/messages/en.json @@ -490,5 +490,22 @@ "admin_users_pagination_total": "{total} total users", "admin_users_pagination_prev": "Previous", "admin_users_pagination_next": "Next", - "admin_users_pagination_showing_range": "Showing {from}-{to} of {total} users" + "admin_users_pagination_showing_range": "Showing {from}-{to} of {total} users", + "admin_groups_title": "Group Management", + "admin_groups_search_filter": "Search & Filter", + "admin_groups_search_placeholder": "Search by group name...", + "admin_groups_all_groups": "All Groups", + "admin_groups_showing_count": "Showing {count} of {total} groups", + "admin_groups_load_error": "Failed to load groups", + "admin_groups_no_groups_title": "No groups found", + "admin_groups_no_groups_description": "There are no groups in the system yet.", + "admin_groups_no_matching_title": "No matching groups", + "admin_groups_no_matching_description": "No groups match your search criteria. Try adjusting your filters.", + "admin_groups_column_id": "ID", + "admin_groups_column_name": "Name", + "admin_groups_column_created_by": "Created By", + "admin_groups_column_created_at": "Created", + "admin_groups_column_updated_at": "Updated", + "admin_groups_created_by_prefix": "User #", + "sidebar_admin_groups": "Groups" } diff --git a/messages/pl.json b/messages/pl.json index 00e55df..fd027a8 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -490,5 +490,22 @@ "admin_users_pagination_total": "{total} użytkowników łącznie", "admin_users_pagination_prev": "Poprzednia", "admin_users_pagination_next": "Następna", - "admin_users_pagination_showing_range": "Wyświetlanie {from}-{to} z {total} użytkowników" + "admin_users_pagination_showing_range": "Wyświetlanie {from}-{to} z {total} użytkowników", + "admin_groups_title": "Zarządzanie Grupami", + "admin_groups_search_filter": "Szukaj i Filtruj", + "admin_groups_search_placeholder": "Szukaj po nazwie grupy...", + "admin_groups_all_groups": "Wszystkie Grupy", + "admin_groups_showing_count": "Wyświetlanie {count} z {total} grup", + "admin_groups_load_error": "Nie udało się załadować grup", + "admin_groups_no_groups_title": "Nie znaleziono grup", + "admin_groups_no_groups_description": "W systemie nie ma jeszcze żadnych grup.", + "admin_groups_no_matching_title": "Brak pasujących grup", + "admin_groups_no_matching_description": "Żadne grupy nie pasują do kryteriów wyszukiwania. Spróbuj dostosować filtry.", + "admin_groups_column_id": "ID", + "admin_groups_column_name": "Nazwa", + "admin_groups_column_created_by": "Utworzony przez", + "admin_groups_column_created_at": "Utworzono", + "admin_groups_column_updated_at": "Zaktualizowano", + "admin_groups_created_by_prefix": "Użytkownik #", + "sidebar_admin_groups": "Grupy" } diff --git a/src/lib/components/dashboard/DashboardSidebar.svelte b/src/lib/components/dashboard/DashboardSidebar.svelte index 92f9a14..44c56bc 100644 --- a/src/lib/components/dashboard/DashboardSidebar.svelte +++ b/src/lib/components/dashboard/DashboardSidebar.svelte @@ -24,6 +24,7 @@ import Globe from '@lucide/svelte/icons/globe'; import UserCircle from '@lucide/svelte/icons/user-circle'; import Users from '@lucide/svelte/icons/users'; + import FolderOpen from '@lucide/svelte/icons/folder-open'; import Languages from '@lucide/svelte/icons/languages'; import LayoutDashboard from '@lucide/svelte/icons/layout-dashboard'; import Activity from '@lucide/svelte/icons/activity'; @@ -90,6 +91,11 @@ title: () => m.sidebar_admin_users(), href: localizeHref(AppRoutes.AdminUsers), icon: Users + }, + { + title: () => m.sidebar_admin_groups(), + href: localizeHref(AppRoutes.AdminGroups), + icon: FolderOpen } ]; diff --git a/src/lib/components/dashboard/admin/groups/GroupsList.svelte b/src/lib/components/dashboard/admin/groups/GroupsList.svelte new file mode 100644 index 0000000..aa77151 --- /dev/null +++ b/src/lib/components/dashboard/admin/groups/GroupsList.svelte @@ -0,0 +1,100 @@ + + +
+ +
+
+ {m.admin_groups_showing_count({ + count: groups.length, + total: total + })} +
+
+ + + + + + + {m.admin_groups_column_id()} + + + {m.admin_groups_column_name()} + + + + + + + + + {#each groups as group (group.id)} + + {group.id} + +
+
+ +
+
+ {group.name} +
+
+
+ + + +
+ {/each} +
+
+
diff --git a/src/lib/components/dashboard/admin/groups/index.ts b/src/lib/components/dashboard/admin/groups/index.ts new file mode 100644 index 0000000..47bd09b --- /dev/null +++ b/src/lib/components/dashboard/admin/groups/index.ts @@ -0,0 +1 @@ +export { default as GroupsList } from './GroupsList.svelte'; diff --git a/src/lib/components/dashboard/utils.ts b/src/lib/components/dashboard/utils.ts index ba85a90..dc11c93 100644 --- a/src/lib/components/dashboard/utils.ts +++ b/src/lib/components/dashboard/utils.ts @@ -17,7 +17,8 @@ export function getDashboardTitleTranslationFromPathname(pathname: string): stri [AppRoutes.Admin]: () => m.sidebar_admin(), [AppRoutes.TeacherContests]: () => m.sidebar_admin_contests(), [AppRoutes.TeacherTasks]: () => m.sidebar_admin_tasks(), - [AppRoutes.AdminUsers]: () => m.admin_users_title() + [AppRoutes.AdminUsers]: () => m.admin_users_title(), + [AppRoutes.AdminGroups]: () => m.admin_groups_title() }; // Check for dynamic routes (e.g., /dashboard/tasks/[taskId]) diff --git a/src/lib/dto/group.ts b/src/lib/dto/group.ts new file mode 100644 index 0000000..55c2416 --- /dev/null +++ b/src/lib/dto/group.ts @@ -0,0 +1,15 @@ +export interface Group { + id: number; + name: string; + createdBy: number; + createdAt: string; + updatedAt: string; +} + +export interface CreateGroupDto { + name: string; +} + +export interface EditGroupDto { + name?: string; +} diff --git a/src/lib/routes.ts b/src/lib/routes.ts index 5d3c2eb..86f9585 100644 --- a/src/lib/routes.ts +++ b/src/lib/routes.ts @@ -27,6 +27,7 @@ export enum AppRoutes { Admin = `${AppRoutes.Dashboard}/admin`, AdminUsers = `${AppRoutes.Admin}/users`, + AdminGroups = `${AppRoutes.Admin}/groups`, Error = '/error' } diff --git a/src/lib/services/GroupService.ts b/src/lib/services/GroupService.ts new file mode 100644 index 0000000..edcb8e5 --- /dev/null +++ b/src/lib/services/GroupService.ts @@ -0,0 +1,53 @@ +import { ApiError, type ApiService } from './ApiService'; +import type { ApiResponse } from '../dto/response'; +import type { Group } from '../dto/group'; + +export class GroupService { + constructor(private apiService: ApiService) {} + + async listGroups(): Promise<{ + success: boolean; + status: number; + data?: Group[]; + error?: string; + }> { + try { + const response: ApiResponse = await this.apiService.get({ + url: '/groups-management/groups' + }); + return { success: true, data: response.data, status: 200 }; + } catch (error) { + if (error instanceof ApiError) { + return { + success: false, + error: error.getApiMessage(), + status: error.getStatus() + }; + } + throw error; + } + } + + async getGroup(groupId: number): Promise<{ + success: boolean; + status: number; + data?: Group; + error?: string; + }> { + try { + const response: ApiResponse = await this.apiService.get({ + url: `/groups-management/groups/${groupId}` + }); + return { success: true, data: response.data, status: 200 }; + } catch (error) { + if (error instanceof ApiError) { + return { + success: false, + error: error.getApiMessage(), + status: error.getStatus() + }; + } + throw error; + } + } +} diff --git a/src/lib/services/index.ts b/src/lib/services/index.ts index be1a8c5..4298c0e 100644 --- a/src/lib/services/index.ts +++ b/src/lib/services/index.ts @@ -5,6 +5,7 @@ export { ContestsManagementService, createContestsManagementService } from './ContestsManagementService'; +export { GroupService } from './GroupService'; export { SubmissionService } from './SubmissionService'; export { TaskService, createTaskService } from './TaskService'; export { TasksManagementService } from './TasksManagementService'; diff --git a/src/routes/dashboard/admin/groups/+page.svelte b/src/routes/dashboard/admin/groups/+page.svelte new file mode 100644 index 0000000..a49f7a5 --- /dev/null +++ b/src/routes/dashboard/admin/groups/+page.svelte @@ -0,0 +1,87 @@ + + +
+
+

{m.admin_groups_title()}

+
+ +
+

{m.admin_groups_search_filter()}

+ +
+
+ + +
+
+
+ +
+
+

{m.admin_groups_all_groups()}

+ {#if groupsQuery.current} +

+ {m.admin_groups_showing_count({ + count: filteredGroups.length, + total: groupsQuery.current.items.length + })} +

+ {/if} +
+ + {#if groupsQuery.error} + groupsQuery.refresh()} + /> + {:else if groupsQuery.loading} + + {:else if groupsQuery.current && groupsQuery.current.items.length === 0} + + {:else if filteredGroups.length === 0} + + {:else} + + {/if} +
+
diff --git a/src/routes/dashboard/admin/groups/groups.remote.ts b/src/routes/dashboard/admin/groups/groups.remote.ts new file mode 100644 index 0000000..91ef079 --- /dev/null +++ b/src/routes/dashboard/admin/groups/groups.remote.ts @@ -0,0 +1,20 @@ +import { query, getRequestEvent } from '$app/server'; +import { createApiClient } from '$lib/services/ApiService'; +import { GroupService } from '$lib/services/GroupService'; +import { error } from '@sveltejs/kit'; + +export const getGroups = query(async () => { + const event = getRequestEvent(); + const apiClient = createApiClient(event.cookies); + const groupService = new GroupService(apiClient); + + const result = await groupService.listGroups(); + + if (!result.success || !result.data) { + error(result.status, { message: result.error || 'Failed to fetch groups.' }); + } + + return { + items: result.data + }; +});