Skip to content
Draft
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
81 changes: 81 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)**.
Expand Down
14 changes: 14 additions & 0 deletions .github/workflows/copilot-setup-steps.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,10 @@ vite.config.ts.timestamp-*

# Paraglide
src/lib/paraglide

# Logs
logs

# Swagger MCP
swagger.json
.swagger-mcp
19 changes: 18 additions & 1 deletion messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
19 changes: 18 additions & 1 deletion messages/pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
6 changes: 6 additions & 0 deletions src/lib/components/dashboard/DashboardSidebar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
}
];

Expand Down
100 changes: 100 additions & 0 deletions src/lib/components/dashboard/admin/groups/GroupsList.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<script lang="ts">
import type { Group } from '$lib/dto/group';
import * as Table from '$lib/components/ui/table';
import FolderOpen from '@lucide/svelte/icons/folder-open';
import UserIcon from '@lucide/svelte/icons/user';
import Calendar from '@lucide/svelte/icons/calendar';
import { formatDistanceToNow, format, isSameDay } from 'date-fns';
import * as m from '$lib/paraglide/messages';

interface GroupsListProps {
groups: Group[];
total: number;
}

let { groups, total }: GroupsListProps = $props();

function formatCreatedAt(createdAt: string): string {
const date = new Date(createdAt);
const now = new Date();
if (isSameDay(date, now)) {
return formatDistanceToNow(date, { addSuffix: true });
}
return format(date, 'dd/MMM/yyyy');
}
</script>

<div class="space-y-3">
<!-- Top bar: count -->
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div class="text-xs text-muted-foreground">
{m.admin_groups_showing_count({
count: groups.length,
total: total
})}
</div>
</div>

<!-- Data table -->
<Table.Root class="mt-1">
<Table.Header>
<Table.Row>
<Table.Head class="w-14">
{m.admin_groups_column_id()}
</Table.Head>
<Table.Head>
{m.admin_groups_column_name()}
</Table.Head>
<Table.Head class="hidden sm:table-cell">
{m.admin_groups_column_created_by()}
</Table.Head>
<Table.Head class="hidden lg:table-cell">
{m.admin_groups_column_created_at()}
</Table.Head>
<Table.Head class="hidden xl:table-cell">
{m.admin_groups_column_updated_at()}
</Table.Head>
</Table.Row>
</Table.Header>

<Table.Body>
{#each groups as group (group.id)}
<Table.Row>
<Table.Cell class="font-mono text-xs">{group.id}</Table.Cell>
<Table.Cell>
<div class="flex items-center gap-2">
<div class="flex h-8 w-8 items-center justify-center rounded-full bg-primary/10">
<FolderOpen class="h-4 w-4 text-primary" />
</div>
<div class="flex min-w-0 flex-col">
<span class="truncate font-medium text-foreground">{group.name}</span>
</div>
</div>
</Table.Cell>
<Table.Cell class="hidden sm:table-cell">
<div class="flex items-center gap-2 text-muted-foreground">
<UserIcon class="h-4 w-4" />
<span>{m.admin_groups_created_by_prefix()}{group.createdBy}</span>
</div>
</Table.Cell>
<Table.Cell class="hidden lg:table-cell">
<div class="flex items-center gap-2 text-muted-foreground">
<Calendar class="h-4 w-4" />
<span class="whitespace-nowrap">
{formatCreatedAt(group.createdAt)}
</span>
</div>
</Table.Cell>
<Table.Cell class="hidden xl:table-cell">
<div class="flex items-center gap-2 text-muted-foreground">
<Calendar class="h-4 w-4" />
<span class="whitespace-nowrap">
{formatCreatedAt(group.updatedAt)}
</span>
</div>
</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
</div>
1 change: 1 addition & 0 deletions src/lib/components/dashboard/admin/groups/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as GroupsList } from './GroupsList.svelte';
3 changes: 2 additions & 1 deletion src/lib/components/dashboard/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down
15 changes: 15 additions & 0 deletions src/lib/dto/group.ts
Original file line number Diff line number Diff line change
@@ -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;
}
1 change: 1 addition & 0 deletions src/lib/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export enum AppRoutes {

Admin = `${AppRoutes.Dashboard}/admin`,
AdminUsers = `${AppRoutes.Admin}/users`,
AdminGroups = `${AppRoutes.Admin}/groups`,

Error = '/error'
}
Expand Down
53 changes: 53 additions & 0 deletions src/lib/services/GroupService.ts
Original file line number Diff line number Diff line change
@@ -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<Group[]> = 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<Group> = 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;
}
}
}
1 change: 1 addition & 0 deletions src/lib/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Loading
Loading