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
11 changes: 11 additions & 0 deletions messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,17 @@
"admin_tasks_remove_cancel_button": "Cancel",
"admin_tasks_remove_success": "Task deleted successfully!",
"admin_tasks_remove_error": "Failed to delete task",
"admin_groups_title": "Groups Management",
"admin_groups_all_groups": "All Groups",
"admin_groups_card_created": "Created:",
"admin_groups_card_updated": "Updated:",
"admin_groups_card_created_by": "Created By:",
"admin_groups_card_id_prefix": "#",
"admin_groups_card_user_prefix": "User #",
"admin_groups_card_view_details": "View Group",
"admin_groups_load_error_title": "Failed to load groups",
"admin_groups_no_groups_title": "No groups yet",
"admin_groups_no_groups_description": "Create your first group to get started",
"task_collaborators_add_title": "Add Collaborator",
"task_collaborators_add_description": "Add a new collaborator to this task",
"task_collaborators_add_dialog_title": "Add Collaborator",
Expand Down
11 changes: 11 additions & 0 deletions messages/pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,17 @@
"admin_tasks_remove_cancel_button": "Anuluj",
"admin_tasks_remove_success": "Zadanie zostało pomyślnie usunięte!",
"admin_tasks_remove_error": "Nie udało się usunąć zadania",
"admin_groups_title": "Zarządzanie Grupami",
"admin_groups_all_groups": "Wszystkie Grupy",
"admin_groups_card_created": "Utworzono:",
"admin_groups_card_updated": "Zaktualizowano:",
"admin_groups_card_created_by": "Utworzone przez:",
"admin_groups_card_id_prefix": "#",
"admin_groups_card_user_prefix": "Użytkownik #",
"admin_groups_card_view_details": "Zobacz Grupę",
"admin_groups_load_error_title": "Nie udało się załadować grup",
"admin_groups_no_groups_title": "Brak grup",
"admin_groups_no_groups_description": "Utwórz swoją pierwszą grupę, aby zacząć",
"task_collaborators_add_title": "Dodaj Współpracownika",
"task_collaborators_add_description": "Dodaj nowego współpracownika do tego zadania",
"task_collaborators_add_dialog_title": "Dodaj Współpracownika",
Expand Down
16 changes: 16 additions & 0 deletions src/lib/components/dashboard/admin/groups/GroupsList.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<script lang="ts">
import AdminGroupCard from '$lib/components/dashboard/groups/AdminGroupCard.svelte';
import type { Group } from '$lib/dto/group';

interface GroupsListProps {
groups: Group[];
}

let { groups }: GroupsListProps = $props();
</script>

<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{#each groups as group (group.id)}
<AdminGroupCard {group} />
{/each}
</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';
77 changes: 77 additions & 0 deletions src/lib/components/dashboard/groups/AdminGroupCard.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<script lang="ts">
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import Users from '@lucide/svelte/icons/users';
import Calendar from '@lucide/svelte/icons/calendar';
import User from '@lucide/svelte/icons/user';
import Clock from '@lucide/svelte/icons/clock';
import type { Group } from '$lib/dto/group';
import * as m from '$lib/paraglide/messages';
import { formatDate } from '$lib/utils';

interface AdminGroupCardProps {
group: Group;
}

let { group }: AdminGroupCardProps = $props();
</script>

<Card.Root
class="group relative flex h-full flex-col overflow-hidden border-border shadow-md transition-all duration-300 hover:-translate-y-1 hover:shadow-lg"
>
<!-- Gradient Background Overlay -->
<div
class="absolute inset-0 bg-linear-to-br from-primary/5 via-secondary/5 to-primary/10 opacity-30 transition-opacity duration-300 group-hover:opacity-50"
></div>

<Card.Header class="relative">
<div class="flex items-start justify-between gap-2">
<span
class="inline-flex items-center rounded-full bg-primary/10 px-2.5 py-0.5 text-xs font-bold text-primary"
>
{m.admin_groups_card_id_prefix()}{group.id}
</span>
</div>
<Card.Title
class="mt-3 flex items-start gap-2 text-lg transition-colors group-hover:text-primary"
>
<Users class="mt-0.5 h-5 w-5 flex-shrink-0" />
<span class="break-words">{group.name}</span>
</Card.Title>
</Card.Header>

<Card.Content class="relative mt-auto space-y-4">
<!-- Group Metadata -->
<div class="space-y-2">
<div class="flex items-center gap-2 text-sm">
<Calendar class="h-4 w-4 text-muted-foreground" />
<span class="text-muted-foreground">{m.admin_groups_card_created()}</span>
<span class="font-medium text-foreground">{formatDate(group.createdAt)}</span>
</div>

<div class="flex items-center gap-2 text-sm">
<Clock class="h-4 w-4 text-muted-foreground" />
<span class="text-muted-foreground">{m.admin_groups_card_updated()}</span>
<span class="font-medium text-foreground">{formatDate(group.updatedAt)}</span>
</div>

<div class="flex items-center gap-2 text-sm">
<User class="h-4 w-4 text-muted-foreground" />
<span class="text-muted-foreground">{m.admin_groups_card_created_by()}</span>
<span class="font-medium text-foreground"
>{m.admin_groups_card_user_prefix()}{group.createdBy}</span
>
</div>
</div>

<!-- Action Buttons -->
<div class="flex flex-col gap-2">
<Button
variant="outline"
class="w-full transition-all duration-300 hover:-translate-y-0.5 hover:shadow-md"
>
{m.admin_groups_card_view_details()}
</Button>
</div>
</Card.Content>
</Card.Root>
7 changes: 7 additions & 0 deletions src/lib/dto/group.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface Group {
id: number;
name: string;
createdAt: string;
updatedAt: string;
createdBy: number;
}
30 changes: 30 additions & 0 deletions src/lib/services/GroupsManagementService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ApiError, type ApiService } from './ApiService';
import type { ApiResponse } from '../dto/response';
import type { Group } from '../dto/group';

export class GroupsManagementService {
constructor(private apiClient: ApiService) {}

async getGroups(): Promise<{
success: boolean;
status: number;
data?: Group[];
error?: string;
}> {
try {
const response = await this.apiClient.get<ApiResponse<Group[]>>({
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;
}
}
}
1 change: 1 addition & 0 deletions src/lib/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export {
ContestsManagementService,
createContestsManagementService
} from './ContestsManagementService';
export { GroupsManagementService } from './GroupsManagementService';
export { SubmissionService } from './SubmissionService';
export { TaskService, createTaskService } from './TaskService';
export { TasksManagementService } from './TasksManagementService';
Expand Down
38 changes: 38 additions & 0 deletions src/routes/dashboard/teacher/groups/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<script lang="ts">
import { getGroups } from './groups.remote';
import { GroupsList } from '$lib/components/dashboard/admin/groups';
import { LoadingSpinner, ErrorCard, EmptyState } from '$lib/components/common';
import Users from '@lucide/svelte/icons/users';
import * as m from '$lib/paraglide/messages';

const groupsQuery = getGroups();
</script>

<div class="space-y-6">
<div class="flex items-center justify-between">
<h1 class="text-3xl font-bold text-foreground">{m.admin_groups_title()}</h1>
</div>

<!-- Groups List Section -->
<div class="space-y-4">
<h2 class="text-2xl font-bold text-foreground">{m.admin_groups_all_groups()}</h2>

{#if groupsQuery.error}
<ErrorCard
title={m.admin_groups_load_error_title()}
error={groupsQuery.error}
onRetry={() => groupsQuery.refresh()}
/>
{:else if groupsQuery.loading}
<LoadingSpinner />
{:else if groupsQuery.current && groupsQuery.current.length === 0}
<EmptyState
title={m.admin_groups_no_groups_title()}
description={m.admin_groups_no_groups_description()}
icon={Users}
/>
{:else if groupsQuery.current}
<GroupsList groups={groupsQuery.current} />
{/if}
</div>
</div>
17 changes: 17 additions & 0 deletions src/routes/dashboard/teacher/groups/groups.remote.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { query, getRequestEvent } from '$app/server';
import { createApiClient } from '$lib/services/ApiService';
import { GroupsManagementService } from '$lib/services/GroupsManagementService';
import { error } from '@sveltejs/kit';

export const getGroups = query(async () => {
const event = getRequestEvent();
const apiClient = createApiClient(event.cookies);
const groupsManagementService = new GroupsManagementService(apiClient);

const result = await groupsManagementService.getGroups();
if (!result.success || !result.data) {
error(result.status, { message: result.error || 'Failed to fetch groups.' });
}

return result.data;
});