Classroom Exam Bilgileri

This commit is contained in:
Sedat Öztürk 2025-10-15 22:52:01 +03:00
parent a3c524579a
commit f64f13557e
45 changed files with 6631 additions and 7 deletions

View file

@ -7879,6 +7879,12 @@
"tr": "Ders Dönemleri", "tr": "Ders Dönemleri",
"en": "Lesson Periods" "en": "Lesson Periods"
}, },
{
"resourceName": "Platform",
"key": "App.Classroom.Tag",
"tr": "Etiketler",
"en": "Tags"
},
{ {
"resourceName": "Platform", "resourceName": "Platform",
"key": "App.Definitions.RegistrationType", "key": "App.Definitions.RegistrationType",
@ -14591,6 +14597,16 @@
"RequiredPermissionName": "App.Definitions.LessonPeriod", "RequiredPermissionName": "App.Definitions.LessonPeriod",
"IsDisabled": false "IsDisabled": false
}, },
{
"ParentCode": "App.Coordinator.Definitions",
"Code": "App.Classroom.Tag",
"DisplayName": "App.Classroom.Tag",
"Order": 10,
"Url": "/admin/list/list-tag",
"Icon": "FcTags",
"RequiredPermissionName": "App.Classroom.Tag",
"IsDisabled": false
},
{ {
"ParentCode": "App.Coordinator", "ParentCode": "App.Coordinator",
"Code": "App.Classroom", "Code": "App.Classroom",
@ -21922,6 +21938,70 @@
"MultiTenancySide": 2, "MultiTenancySide": 2,
"MenuGroup": "Kurs" "MenuGroup": "Kurs"
}, },
{
"GroupName": "App.Coordinator",
"Name": "App.Classroom.Tag",
"ParentName": null,
"DisplayName": "App.Classroom.Tag",
"IsEnabled": true,
"MultiTenancySide": 3,
"MenuGroup": "Kurs"
},
{
"GroupName": "App.Coordinator",
"Name": "App.Classroom.Tag.Create",
"ParentName": "App.Classroom.Tag",
"DisplayName": "Create",
"IsEnabled": true,
"MultiTenancySide": 3,
"MenuGroup": "Kurs"
},
{
"GroupName": "App.Coordinator",
"Name": "App.Classroom.Tag.Update",
"ParentName": "App.Classroom.Tag",
"DisplayName": "Update",
"IsEnabled": true,
"MultiTenancySide": 3,
"MenuGroup": "Kurs"
},
{
"GroupName": "App.Coordinator",
"Name": "App.Classroom.Tag.Delete",
"ParentName": "App.Classroom.Tag",
"DisplayName": "Delete",
"IsEnabled": true,
"MultiTenancySide": 3,
"MenuGroup": "Kurs"
},
{
"GroupName": "App.Coordinator",
"Name": "App.Classroom.Tag.Export",
"ParentName": "App.Classroom.Tag",
"DisplayName": "Export",
"IsEnabled": true,
"MultiTenancySide": 3,
"MenuGroup": "Kurs"
},
{
"GroupName": "App.Coordinator",
"Name": "App.Classroom.Tag.Import",
"ParentName": "App.Classroom.Tag",
"DisplayName": "Import",
"IsEnabled": true,
"MultiTenancySide": 3,
"MenuGroup": "Kurs"
},
{
"GroupName": "App.Coordinator",
"Name": "App.Classroom.Tag.Activity",
"ParentName": "App.Classroom.Tag",
"DisplayName": "Activity",
"IsEnabled": true,
"MultiTenancySide": 3,
"MenuGroup": "Kurs"
},
{ {
"GroupName": "App.SupplyChain", "GroupName": "App.SupplyChain",
"Name": "App.SupplyChain.MaterialTypes", "Name": "App.SupplyChain.MaterialTypes",

View file

@ -29929,6 +29929,275 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
} }
#endregion #endregion
//Classroom
#region Tag
if (!await _listFormRepository.AnyAsync(a => a.ListFormCode == ListFormCodes.Lists.Tag))
{
var listFormTag = await _listFormRepository.InsertAsync(
new ListForm()
{
ListFormType = ListFormTypeEnum.List,
IsSubForm = false,
LayoutJson = JsonSerializer.Serialize(new LayoutDto()
{
Grid = true,
Card = true,
Pivot = true,
Chart = true,
DefaultLayout = "grid",
CardLayoutColumn = 3
}),
CultureName = LanguageCodes.En,
ListFormCode = ListFormCodes.Lists.Tag,
Name = AppCodes.Classroom.Tag,
Title = AppCodes.Classroom.Tag,
DataSourceCode = SeedConsts.DataSources.DefaultCode,
IsTenant = true,
IsBranch = false,
IsOrganizationUnit = false,
Description = AppCodes.Classroom.Tag,
SelectCommandType = SelectCommandTypeEnum.Table,
SelectCommand = SelectCommandByTableName("Tag", Prefix.DbTableCoordinator),
KeyFieldName = "Id",
KeyFieldDbSourceType = DbType.Guid,
DefaultFilter = "\"IsDeleted\" = 'false'",
SortMode = GridOptions.SortModeSingle,
FilterRowJson = JsonSerializer.Serialize(new GridFilterRowDto { Visible = true }),
HeaderFilterJson = JsonSerializer.Serialize(new { Visible = true }),
SearchPanelJson = JsonSerializer.Serialize(new { Visible = true }),
GroupPanelJson = JsonSerializer.Serialize(new { Visible = true }),
SelectionJson = JsonSerializer.Serialize(new SelectionDto
{
Mode = GridOptions.SelectionModeSingle,
AllowSelectAll = false
}),
ColumnOptionJson = JsonSerializer.Serialize(new
{
ColumnFixingEnabled = true,
ColumnAutoWidth = true,
ColumnChooserEnabled = true,
AllowColumnResizing = true,
AllowColumnReordering = true,
ColumnResizingMode = "widget",
}),
PermissionJson = JsonSerializer.Serialize(new PermissionCrudDto
{
C = AppCodes.Classroom.Tag + ".Create",
R = AppCodes.Classroom.Tag,
U = AppCodes.Classroom.Tag + ".Update",
D = AppCodes.Classroom.Tag + ".Delete",
E = AppCodes.Classroom.Tag + ".Export",
I = AppCodes.Classroom.Tag + ".Import",
A = AppCodes.Classroom.Tag + ".Activity",
}),
DeleteCommand = $"UPDATE \"{SelectCommandByTableName("Tag", Prefix.DbTableCoordinator)}\" SET \"DeleterId\"=@DeleterId, \"DeletionTime\"=CURRENT_TIMESTAMP, \"IsDeleted\"='true' WHERE \"Id\"=@Id",
DeleteFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] {
new() {
FieldName = "DeleterId",
FieldDbType = DbType.Guid,
Value = "@USERID",
CustomValueType = FieldCustomValueTypeEnum.CustomKey },
new() {
FieldName = "Id",
FieldDbType = DbType.Guid,
Value = "@ID",
CustomValueType = FieldCustomValueTypeEnum.CustomKey }
}),
PagerOptionJson = JsonSerializer.Serialize(new GridPagerOptionDto
{
Visible = true,
AllowedPageSizes = "10,20,50,100",
ShowPageSizeSelector = true,
ShowNavigationButtons = true,
ShowInfo = false,
InfoText = "Page {0} of {1} ({2} items)",
DisplayMode = GridColumnOptions.PagerDisplayModeAdaptive,
ScrollingMode = GridColumnOptions.ScrollingModeStandard,
LoadPanelEnabled = "auto",
LoadPanelText = "Loading..."
}),
EditingOptionJson = JsonSerializer.Serialize(new GridEditingDto
{
Popup = new GridEditingPopupDto()
{
Title = "Tag Form",
Width = 500,
Height = 250
},
AllowDeleting = true,
AllowAdding = true,
AllowUpdating = true,
SendOnlyChangedFormValuesUpdate = false,
}),
EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>() {
new() { Order=1,ColCount=1,ColSpan=2,ItemType="group", Items=
[
new EditingFormItemDto { Order = 1, DataField = "Name", ColSpan = 2, IsRequired = true, EditorType2=EditorTypes.dxTextBox },
new EditingFormItemDto { Order = 2, DataField = "Description", ColSpan = 2, EditorType2=EditorTypes.dxTextBox },
new EditingFormItemDto { Order = 3, DataField = "Color", ColSpan = 2, IsRequired = true, EditorType2=EditorTypes.dxColorBox },
]
}
}),
InsertFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] {
new() {
FieldName = "CreationTime",
FieldDbType = DbType.DateTime,
Value = "@NOW",
CustomValueType = FieldCustomValueTypeEnum.CustomKey },
new() {
FieldName = "CreatorId",
FieldDbType = DbType.Guid,
Value = "@USERID",
CustomValueType = FieldCustomValueTypeEnum.CustomKey },
new() {
FieldName = "IsDeleted",
FieldDbType = DbType.Boolean,
Value = "false",
CustomValueType = FieldCustomValueTypeEnum.Value }
}),
}
);
#region Tag Fields
await _listFormFieldRepository.InsertManyAsync(new ListFormField[] {
new() {
ListFormCode = listFormTag.ListFormCode,
RoleId = null,
UserId = null,
CultureName = LanguageCodes.En,
SourceDbType = DbType.Guid,
FieldName = "Id",
Width = 100,
ListOrderNo = 1,
Visible = false,
IsActive = true,
IsDeleted = false,
SortIndex = 0,
ValidationRuleJson = JsonSerializer.Serialize(new ValidationRuleDto[] {
new ValidationRuleDto() { Type = Enum.GetName(UiColumnValidationRuleTypeEnum.required) }
}),
ColumnCustomizationJson = JsonSerializer.Serialize(new ColumnCustomizationDto
{
AllowReordering = true,
}),
PermissionJson = JsonSerializer.Serialize(new ListFormFieldPermissionDto
{
C = AppCodes.Classroom.Tag + ".Create",
R = AppCodes.Classroom.Tag,
U = AppCodes.Classroom.Tag + ".Update",
E = true,
I = true,
Deny = false
}),
PivotSettingsJson = JsonSerializer.Serialize(new ListFormFieldPivotSettingsDto
{
IsPivot = true
})
},
new() {
ListFormCode = listFormTag.ListFormCode,
RoleId = null,
UserId = null,
CultureName = LanguageCodes.En,
SourceDbType = DbType.String,
FieldName = "Name",
Width = 250,
ListOrderNo = 2,
Visible = true,
IsActive = true,
IsDeleted = false,
SortIndex = 1,
SortDirection = GridColumnOptions.SortOrderAsc,
AllowSearch = true,
ValidationRuleJson = JsonSerializer.Serialize(new ValidationRuleDto[] {
new ValidationRuleDto() { Type = Enum.GetName(UiColumnValidationRuleTypeEnum.required) }
}),
ColumnCustomizationJson = JsonSerializer.Serialize(new ColumnCustomizationDto
{
AllowReordering = true,
}),
PermissionJson = JsonSerializer.Serialize(new ListFormFieldPermissionDto
{
C = AppCodes.Classroom.Tag + ".Create",
R = AppCodes.Classroom.Tag,
U = AppCodes.Classroom.Tag + ".Update",
E = true,
I = true,
Deny = false
}),
PivotSettingsJson = JsonSerializer.Serialize(new ListFormFieldPivotSettingsDto
{
IsPivot = true
})
},
new() {
ListFormCode = listFormTag.ListFormCode,
RoleId = null,
UserId = null,
CultureName = LanguageCodes.En,
SourceDbType = DbType.String,
FieldName = "Description",
Width = 400,
ListOrderNo = 3,
Visible = true,
IsActive = true,
IsDeleted = false,
AllowSearch = true,
ColumnCustomizationJson = JsonSerializer.Serialize(new ColumnCustomizationDto
{
AllowReordering = true,
}),
PermissionJson = JsonSerializer.Serialize(new ListFormFieldPermissionDto
{
C = AppCodes.Classroom.Tag + ".Create",
R = AppCodes.Classroom.Tag,
U = AppCodes.Classroom.Tag + ".Update",
E = true,
I = true,
Deny = false
}),
PivotSettingsJson = JsonSerializer.Serialize(new ListFormFieldPivotSettingsDto
{
IsPivot = true
})
},
new() {
ListFormCode = listFormTag.ListFormCode,
RoleId = null,
UserId = null,
CultureName = LanguageCodes.En,
SourceDbType = DbType.String,
FieldName = "Color",
Width = 100,
ListOrderNo = 4,
Visible = true,
IsActive = true,
IsDeleted = false,
AllowSearch = true,
ColumnCustomizationJson = JsonSerializer.Serialize(new ColumnCustomizationDto
{
AllowReordering = true,
}),
PermissionJson = JsonSerializer.Serialize(new ListFormFieldPermissionDto
{
C = AppCodes.Classroom.Tag + ".Create",
R = AppCodes.Classroom.Tag,
U = AppCodes.Classroom.Tag + ".Update",
E = true,
I = true,
Deny = false
}),
PivotSettingsJson = JsonSerializer.Serialize(new ListFormFieldPivotSettingsDto
{
IsPivot = true
})
}
});
#endregion
}
#endregion
#endregion #endregion
} }
} }

View file

@ -595,6 +595,7 @@ public static class PlatformConsts
public const string ClassType = "list-classtype"; public const string ClassType = "list-classtype";
public const string Class = "list-class"; public const string Class = "list-class";
public const string Level = "list-level"; public const string Level = "list-level";
public const string Tag = "list-tag";
} }
} }

View file

@ -440,6 +440,12 @@ public static class SeedConsts
public const string Level = Default + ".Level"; public const string Level = Default + ".Level";
} }
public static class Classroom
{
public const string Default = Prefix.App + ".Classroom";
public const string Tag = Default + ".Tag";
}
public static class Accounting public static class Accounting
{ {
public const string Default = Prefix.App + ".Accounting"; public const string Default = Prefix.App + ".Accounting";

View file

@ -0,0 +1,17 @@
using System;
using Volo.Abp.Domain.Entities.Auditing;
using Volo.Abp.MultiTenancy;
namespace Kurs.Platform.Entities;
public class Tag : FullAuditedEntity<Guid>, IMultiTenant
{
public Guid? TenantId;
public string Name { get; set; } // Etiket adı (ör: Grammar)
public string Description { get; set; } // Açıklama
public string Color { get; set; } // Renk kodu (ör: #3B82F6)
public int UsageCount { get; set; } // Kullanım sayısı
Guid? IMultiTenant.TenantId => TenantId;
}

View file

@ -139,6 +139,9 @@ public class PlatformDbContext :
public DbSet<ClassroomParticipant> Participants { get; set; } public DbSet<ClassroomParticipant> Participants { get; set; }
public DbSet<ClassroomAttandance> AttendanceRecords { get; set; } public DbSet<ClassroomAttandance> AttendanceRecords { get; set; }
public DbSet<ClassroomChat> ChatMessages { get; set; } public DbSet<ClassroomChat> ChatMessages { get; set; }
public DbSet<Tag> Tags { get; set; }
#endregion #endregion
#region Entities from the modules #region Entities from the modules
@ -1561,5 +1564,17 @@ public class PlatformDbContext :
b.Property(x => x.Subject).IsRequired().HasMaxLength(256); b.Property(x => x.Subject).IsRequired().HasMaxLength(256);
b.Property(x => x.Content).IsRequired().HasMaxLength(2000); b.Property(x => x.Content).IsRequired().HasMaxLength(2000);
}); });
// Classroom
builder.Entity<Tag>(b =>
{
b.ToTable(Prefix.DbTableCoordinator + nameof(Tag), Prefix.DbSchema);
b.ConfigureByConvention();
b.Property(x => x.Name).IsRequired().HasMaxLength(100);
b.Property(x => x.Description).HasMaxLength(500);
b.Property(x => x.Color).HasMaxLength(7);
b.Property(x => x.UsageCount).HasDefaultValue(0);
});
} }
} }

View file

@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
namespace Kurs.Platform.Migrations namespace Kurs.Platform.Migrations
{ {
[DbContext(typeof(PlatformDbContext))] [DbContext(typeof(PlatformDbContext))]
[Migration("20251013214306_Initial")] [Migration("20251015192202_Initial")]
partial class Initial partial class Initial
{ {
/// <inheritdoc /> /// <inheritdoc />
@ -6112,6 +6112,69 @@ namespace Kurs.Platform.Migrations
b.ToTable("DSource", (string)null); b.ToTable("DSource", (string)null);
}); });
modelBuilder.Entity("Kurs.Platform.Entities.Tag", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<string>("Color")
.HasMaxLength(7)
.HasColumnType("nvarchar(7)");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");
b.Property<int>("UsageCount")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasDefaultValue(0);
b.HasKey("Id");
b.ToTable("CTag", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Entities.Uom", b => modelBuilder.Entity("Kurs.Platform.Entities.Uom", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")

View file

@ -434,6 +434,29 @@ namespace Kurs.Platform.Migrations
table.PrimaryKey("PK_AbpUsers", x => x.Id); table.PrimaryKey("PK_AbpUsers", x => x.Id);
}); });
migrationBuilder.CreateTable(
name: "CTag",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
Name = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
Description = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
Color = table.Column<string>(type: "nvarchar(7)", maxLength: 7, nullable: true),
UsageCount = table.Column<int>(type: "int", nullable: false, defaultValue: 0),
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifierId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
DeleterId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_CTag", x => x.Id);
});
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "DBank", name: "DBank",
columns: table => new columns: table => new
@ -4142,6 +4165,9 @@ namespace Kurs.Platform.Migrations
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "AbpUserTokens"); name: "AbpUserTokens");
migrationBuilder.DropTable(
name: "CTag");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "DBankAccount"); name: "DBankAccount");

View file

@ -6109,6 +6109,69 @@ namespace Kurs.Platform.Migrations
b.ToTable("DSource", (string)null); b.ToTable("DSource", (string)null);
}); });
modelBuilder.Entity("Kurs.Platform.Entities.Tag", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<string>("Color")
.HasMaxLength(7)
.HasColumnType("nvarchar(7)");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");
b.Property<int>("UsageCount")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasDefaultValue(0);
b.HasKey("Id");
b.ToTable("CTag", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Entities.Uom", b => modelBuilder.Entity("Kurs.Platform.Entities.Uom", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")

View file

@ -1754,7 +1754,6 @@
"Sunday": true "Sunday": true
} }
], ],
"Classrooms": [ "Classrooms": [
{ {
"name": "Matematik 101 - Diferansiyel Denklemler", "name": "Matematik 101 - Diferansiyel Denklemler",
@ -1807,5 +1806,27 @@
"participantCount": 0, "participantCount": 0,
"settingsJson": "{\"AllowHandRaise\":true,\"AllowStudentChat\":true,\"AllowPrivateMessages\":true,\"AllowStudentScreenShare\":true,\"DefaultMicrophoneState\":\"muted\",\"DefaultCameraState\":\"off\",\"DefaultLayout\":\"grid\",\"AutoMuteNewParticipants\":true}" "settingsJson": "{\"AllowHandRaise\":true,\"AllowStudentChat\":true,\"AllowPrivateMessages\":true,\"AllowStudentScreenShare\":true,\"DefaultMicrophoneState\":\"muted\",\"DefaultCameraState\":\"off\",\"DefaultLayout\":\"grid\",\"AutoMuteNewParticipants\":true}"
} }
],
"Tags": [
{
"Name": "Grammar",
"Description": "Grammar related questions",
"Color": "#3B82F6"
},
{
"Name": "Vocabulary",
"Description": "Vocabulary building exercises",
"Color": "#10B981"
},
{
"Name": "Reading",
"Description": "Reading comprehension and analysis",
"Color": "#F59E0B"
},
{
"Name": "Writing",
"Description": "Writing skills development",
"Color": "#EF4444"
}
] ]
} }

View file

@ -52,6 +52,7 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency
private readonly IRepository<ClassCancellationReason, Guid> _classCancellationReasonRepository; private readonly IRepository<ClassCancellationReason, Guid> _classCancellationReasonRepository;
private readonly IRepository<WorkHour, Guid> _workHourRepository; private readonly IRepository<WorkHour, Guid> _workHourRepository;
private readonly IRepository<Classroom, Guid> _classroomRepository; private readonly IRepository<Classroom, Guid> _classroomRepository;
private readonly IRepository<Tag, Guid> _tagRepository;
public TenantDataSeeder( public TenantDataSeeder(
IRepository<GlobalSearch, int> globalSearch, IRepository<GlobalSearch, int> globalSearch,
@ -90,7 +91,8 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency
IRepository<Source, Guid> sourceRepository, IRepository<Source, Guid> sourceRepository,
IRepository<Interesting, Guid> interestingRepository, IRepository<Interesting, Guid> interestingRepository,
IRepository<Program, Guid> programRepository, IRepository<Program, Guid> programRepository,
IRepository<ForumCategory, Guid> forumCategoryRepository IRepository<ForumCategory, Guid> forumCategoryRepository,
IRepository<Tag, Guid> tagRepository
) )
{ {
_globalSearch = globalSearch; _globalSearch = globalSearch;
@ -130,6 +132,7 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency
_interestingRepository = interestingRepository; _interestingRepository = interestingRepository;
_programRepository = programRepository; _programRepository = programRepository;
_forumCategoryRepository = forumCategoryRepository; _forumCategoryRepository = forumCategoryRepository;
_tagRepository = tagRepository;
} }
private static IConfigurationRoot BuildConfiguration() private static IConfigurationRoot BuildConfiguration()
@ -725,5 +728,20 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency
}); });
} }
} }
foreach (var item in items.Tags)
{
var exists = await _tagRepository.AnyAsync(x => x.Name == item.Name);
if (!exists)
{
await _tagRepository.InsertAsync(new Tag
{
Name = item.Name,
Description = item.Description,
Color = item.Color
});
}
}
} }
} }

View file

@ -26,6 +26,7 @@ public class TenantSeederDto
public List<BlogPostSeedDto> BlogPosts { get; set; } public List<BlogPostSeedDto> BlogPosts { get; set; }
public List<ContactSeedDto> Contacts { get; set; } public List<ContactSeedDto> Contacts { get; set; }
public List<ClassroomSeedDto> Classrooms { get; set; } public List<ClassroomSeedDto> Classrooms { get; set; }
public List<TagSeedDto> Tags { get; set; }
//Tanımlamalar //Tanımlamalar
public List<SectorSeedDto> Sectors { get; set; } public List<SectorSeedDto> Sectors { get; set; }
@ -344,3 +345,10 @@ public class ProgramSeedDto
public string Name { get; set; } public string Name { get; set; }
public string Status { get; set; } public string Status { get; set; }
} }
public class TagSeedDto
{
public string Name { get; set; }
public string Description { get; set; }
public string Color { get; set; }
}

View file

@ -0,0 +1,79 @@
export const adminNavigationItems = [
{
id: "dashboard",
title: "Dashboard",
icon: "LayoutDashboard",
path: "/admin/dashboard",
description: "Genel bakış ve istatistikler",
},
{
id: "tags",
title: "Etiket Yönetimi",
icon: "Tags",
path: "/admin/tags",
description: "Soru ve içerik etiketleri",
},
{
id: "question-pools",
title: "Soru Havuzları",
icon: "Database",
path: "/admin/question-pools",
description: "Soru havuzları ve kategoriler",
},
{
id: "exams",
title: "Sınav Yönetimi",
icon: "FileText",
path: "/admin/exams",
description: "Sınavları yönetin",
},
{
id: "assignments",
title: "Ödev Yönetimi",
icon: "BookOpen",
path: "/admin/assignments",
description: "Ödevleri yönetin",
},
{
id: "tests",
title: "Test Yönetimi",
icon: "ClipboardList",
path: "/admin/tests",
description: "PDF testler ve cevap anahtarları",
},
];
export const questionTypes = [
{ value: "multiple-choice", label: "Çoktan Seçmeli", icon: "CheckCircle" },
{ value: "fill-blank", label: "Boşluk Doldurma", icon: "Edit3" },
{ value: "multiple-answer", label: "Çok Yanıtlı", icon: "CheckSquare" },
{ value: "matching", label: "Eşleştirme", icon: "Link" },
{ value: "ordering", label: "Sıralama", icon: "ArrowUpDown" },
{ value: "open-ended", label: "Açık Uçlu", icon: "MessageSquare" },
{ value: "true-false", label: "Doğru-Yanlış", icon: "ToggleLeft" },
{ value: "calculation", label: "Hesaplama", icon: "Calculator" },
];
export const difficultyLevels = [
{ value: "easy", label: "Kolay", color: "green" },
{ value: "medium", label: "Orta", color: "yellow" },
{ value: "hard", label: "Zor", color: "red" },
];
export const examTypes = [
{ value: "exam", label: "Sınav", icon: "FileText" },
{ value: "assignment", label: "Ödev", icon: "BookOpen" },
{ value: "test", label: "Test", icon: "ClipboardList" },
];
export const getBreadcrumb = (currentPath: string): string[] => {
const pathMap: Record<string, string[]> = {
"/admin/dashboard": ["Admin", "Dashboard"],
"/admin/tags": ["Admin", "Etiket Yönetimi"],
"/admin/question-pools": ["Admin", "Soru Havuzları"],
"/admin/exams": ["Admin", "Sınav Yönetimi"],
"/admin/assignments": ["Admin", "Ödev Yönetimi"],
"/admin/tests": ["Admin", "Test Yönetimi"],
};
return pathMap[currentPath] || ["Admin"];
};

39
ui/src/mocks/mockExams.ts Normal file
View file

@ -0,0 +1,39 @@
import { Exam } from "@/types/coordinator";
import { generateMockPools } from "./mockPools";
export const generateMockExam = (): Exam[] => [
{
id: "exam-1",
title: "Grammar Assessment",
description: "Comprehensive grammar evaluation",
type: "exam",
questions: generateMockPools()[0].questions,
timeLimit: 30,
totalPoints: 25,
passingScore: 60,
allowReview: true,
randomizeQuestions: false,
showResults: true,
maxAttempts: 1,
isActive: true,
creationTime: new Date(),
lastModificationTime: new Date(),
},
{
id: "assignment-1",
title: "Assignment 1",
description: "First assignment on grammar topics",
type: "assignment",
questions: generateMockPools()[0].questions,
timeLimit: 30,
totalPoints: 25,
passingScore: 60,
allowReview: true,
randomizeQuestions: false,
showResults: true,
maxAttempts: 1,
isActive: true,
creationTime: new Date(),
lastModificationTime: new Date(),
},
];

46
ui/src/mocks/mockPools.ts Normal file
View file

@ -0,0 +1,46 @@
import { QuestionPool } from "@/types/coordinator";
// Dynamic data - no hardcoded content
export const generateMockPools = (): QuestionPool[] => [
{
id: "pool-1",
name: "Grammar Fundamentals",
description: "Essential grammar concepts and structures",
questions: [
{
id: "q1",
type: "multiple-choice",
title: "Verb Tenses",
content: "Select the appropriate tense form for the given context.",
options: [
{ id: "opt1", text: "Option A", isCorrect: false },
{ id: "opt2", text: "Option B", isCorrect: true },
{ id: "opt3", text: "Option C", isCorrect: false },
{ id: "opt4", text: "Option D", isCorrect: false },
],
correctAnswer: "opt2",
points: 10,
difficulty: "easy",
tags: ["grammar", "verbs"],
creationTime: new Date(),
lastModificationTime: new Date(),
},
{
id: "q2",
type: "fill-blank",
title: "Articles",
content:
'Fill in the blank: "I saw _____ elephant at the zoo yesterday."',
correctAnswer: "an",
points: 15,
difficulty: "medium",
tags: ["grammar", "articles"],
creationTime: new Date(),
lastModificationTime: new Date(),
},
],
tags: ["grammar", "fundamentals"],
createdBy: "admin",
creationTime: new Date(),
},
];

36
ui/src/mocks/mockTags.ts Normal file
View file

@ -0,0 +1,36 @@
import { TagItem } from "@/types/coordinator";
export const generateMockTags = (): TagItem[] => [
{
id: "tag-1",
name: "Grammar",
description: "Grammar related questions",
color: "#3B82F6",
usageCount: 25,
creationTime: new Date(),
},
{
id: "tag-2",
name: "Vocabulary",
description: "Vocabulary building exercises",
color: "#10B981",
usageCount: 18,
creationTime: new Date(),
},
{
id: "tag-3",
name: "Reading",
description: "Reading comprehension tasks",
color: "#F59E0B",
usageCount: 12,
creationTime: new Date(),
},
{
id: "tag-4",
name: "Writing",
description: "Writing skill assessments",
color: "#EF4444",
usageCount: 8,
creationTime: new Date(),
},
];

48
ui/src/mocks/mockTests.ts Normal file
View file

@ -0,0 +1,48 @@
import { Exam } from "@/types/coordinator";
export const generateMockPDFTest = (): Exam => ({
id: "test-1",
title: "Reading Assessment",
description: "Document-based reading evaluation",
type: "test",
testDocument: {
url: "/sample-documents/reading-test.pdf",
type: "pdf",
name: "reading-assessment.pdf",
},
answerKeyTemplate: [
{
id: "ak1",
questionNumber: 1,
type: "multiple-choice",
options: ["A", "B", "C", "D"],
points: 10,
correctAnswer: "A",
},
{
id: "ak2",
questionNumber: 2,
type: "true-false",
points: 5,
correctAnswer: "true",
},
{
id: "ak3",
questionNumber: 3,
type: "fill-blank",
points: 15,
correctAnswer: "answer",
},
],
questions: [],
timeLimit: 45,
totalPoints: 30,
passingScore: 70,
allowReview: true,
randomizeQuestions: false,
showResults: true,
maxAttempts: 1,
isActive: true,
creationTime: new Date(),
lastModificationTime: new Date(),
});

130
ui/src/types/coordinator.ts Normal file
View file

@ -0,0 +1,130 @@
export type QuestionType =
| "multiple-choice"
| "fill-blank"
| "multiple-answer"
| "matching"
| "ordering"
| "open-ended"
| "true-false"
| "calculation";
export type ExamType = "exam" | "assignment" | "test";
export type TestType = 'pdf' | 'image';
export type MediaType = 'image' | 'video';
export type QuestionDifficulty = 'easy' | 'medium' | 'hard';
export type ExamSessionStatus = 'in-progress' | 'completed' | 'submitted';
export interface QuestionPool {
id: string;
name: string;
description: string;
questions: Question[];
tags: string[];
createdBy: string;
creationTime: Date;
}
export interface Question {
id: string;
type: QuestionType;
title: string;
content: string;
mediaUrl?: string;
mediaType?: MediaType;
options?: QuestionOption[];
correctAnswer?: string | string[];
points: number;
timeLimit?: number;
explanation?: string;
tags: string[];
difficulty: QuestionDifficulty;
creationTime: Date;
lastModificationTime: Date;
}
export interface QuestionOption {
id: string;
text: string;
isCorrect: boolean;
order?: number;
}
export interface Exam {
id: string;
title: string;
description: string;
type: ExamType;
testDocument?: {
url: string;
type: TestType;
name: string;
};
answerKeyTemplate?: AnswerKeyItem[];
questions: Question[];
timeLimit: number;
totalPoints: number;
passingScore: number;
allowReview: boolean;
randomizeQuestions: boolean;
showResults: boolean;
maxAttempts: number;
startTime?: Date;
endTime?: Date;
isActive: boolean;
creationTime: Date;
lastModificationTime: Date;
}
export interface AnswerKeyItem {
id: string;
questionNumber: number;
type: QuestionType;
options?: string[];
points: number;
correctAnswer?: string | string[];
}
export interface StudentAnswer {
questionId: string;
answer: string | string[];
timeSpent: number;
isCorrect?: boolean;
points?: number;
}
export interface ExamSession {
id: string;
examId: string;
studentId: string;
startTime: Date;
endTime?: Date;
answers: StudentAnswer[];
totalScore?: number;
status: ExamSessionStatus;
timeRemaining: number;
}
export interface TagItem {
id: string;
name: string;
description: string;
color: string;
usageCount: number;
creationTime: Date;
}
export interface NavigationItem {
id: string;
title: string;
icon: string;
path: string;
description?: string;
badge?: number;
}
export interface AdminRoute {
path: string;
component: string;
title: string;
breadcrumb: string[];
}

View file

@ -0,0 +1,131 @@
import { QuestionPool, Exam, TagItem, ExamSession } from "@/types/coordinator";
import { useState } from "react";
export function useCoordinator() {
const [currentPath, setCurrentPath] = useState("/admin/dashboard");
const [pools, setPools] = useState<QuestionPool[]>([]);
const [exams, setExams] = useState<Exam[]>([]);
const [tags, setTags] = useState<TagItem[]>([]);
const [currentExam, setCurrentExam] = useState<Exam | null>(null);
const handleUpdateTest = (updatedTest: Exam) => {
setExams((prev) =>
prev.map((exam) => (exam.id === updatedTest.id ? updatedTest : exam))
);
};
const handleDeleteTest = (testId: string) => {
setExams((prev) => prev.filter((exam) => exam.id !== testId));
};
const handleCreatePool = (
poolData: Omit<QuestionPool, "id" | "creationTime">
) => {
const newPool: QuestionPool = {
...poolData,
id: `pool-${Date.now()}`,
creationTime: new Date(),
};
setPools((prev) => [...prev, newPool]);
};
const handleUpdatePool = (updatedPool: QuestionPool) => {
setPools((prev) =>
prev.map((pool) => (pool.id === updatedPool.id ? updatedPool : pool))
);
};
const handleDeletePool = (poolId: string) => {
setPools((prev) => prev.filter((pool) => pool.id !== poolId));
};
const handleCreateExam = (
examData: Omit<Exam, "id" | "creationTime" | "lastModificationTime">
) => {
const newExam: Exam = {
...examData,
id: `exam-${Date.now()}`,
creationTime: new Date(),
lastModificationTime: new Date(),
};
setExams((prev) => [...prev, newExam]);
setCurrentPath("/admin/exams");
};
const handleSaveExam = (exam: Exam) => {
setExams((prev) => prev.map((e) => (e.id === exam.id ? exam : e)));
setCurrentPath("/admin/exams");
};
const handleCreateTest = (
testData: Omit<Exam, "id" | "creationTime" | "lastModificationTime">
) => {
const newTest: Exam = {
...testData,
id: `test-${Date.now()}`,
creationTime: new Date(),
lastModificationTime: new Date(),
};
setExams((prev) => [...prev, newTest]);
setCurrentPath("/admin/tests");
};
const handleCreateTag = (
tagData: Omit<TagItem, "id" | "usageCount" | "creationTime">
) => {
const newTag: TagItem = {
...tagData,
id: `tag-${Date.now()}`,
usageCount: 0,
creationTime: new Date(),
};
setTags((prev) => [...prev, newTag]);
};
const handleUpdateTag = (updatedTag: TagItem) => {
setTags((prev) =>
prev.map((tag) => (tag.id === updatedTag.id ? updatedTag : tag))
);
};
const handleDeleteTag = (tagId: string) => {
setTags((prev) => prev.filter((tag) => tag.id !== tagId));
};
const handleExamComplete = (session: ExamSession) => {
console.log("Exam completed:", session);
alert("Assessment completed successfully!");
setCurrentPath("/admin/dashboard");
setCurrentExam(null);
};
const startExam = (exam: Exam) => {
setCurrentExam(exam);
};
return {
currentPath,
setCurrentPath,
pools,
setPools,
exams,
setExams,
tags,
setTags,
currentExam,
setCurrentExam,
handleUpdateTest,
handleDeleteTest,
handleCreatePool,
handleUpdatePool,
handleDeletePool,
handleCreateExam,
handleSaveExam,
handleCreateTest,
handleCreateTag,
handleUpdateTag,
handleDeleteTag,
handleExamComplete,
startExam,
};
}

View file

@ -0,0 +1,95 @@
import { useEffect, useCallback } from 'react';
interface SecurityConfig {
disableRightClick: boolean;
disableCopyPaste: boolean;
disableDevTools: boolean;
fullScreenMode: boolean;
preventTabSwitch: boolean;
}
export const useExamSecurity = (config: SecurityConfig, isActive: boolean = true) => {
const handleRightClick = useCallback((e: MouseEvent) => {
if (config.disableRightClick && isActive) {
e.preventDefault();
return false;
}
}, [config.disableRightClick, isActive]);
const handleKeyDown = useCallback((e: KeyboardEvent) => {
if (!isActive) return;
// Disable copy/paste shortcuts
if (config.disableCopyPaste) {
if (e.ctrlKey && (e.key === 'c' || e.key === 'v' || e.key === 'x' || e.key === 'a')) {
e.preventDefault();
return false;
}
}
// Disable developer tools
if (config.disableDevTools) {
if (e.key === 'F12' || (e.ctrlKey && e.shiftKey && e.key === 'I')) {
e.preventDefault();
return false;
}
}
// Disable Alt+Tab for tab switching
if (config.preventTabSwitch && e.altKey && e.key === 'Tab') {
e.preventDefault();
return false;
}
}, [config, isActive]);
const handleVisibilityChange = useCallback(() => {
if (config.preventTabSwitch && isActive && document.hidden) {
// Log tab switch attempt - in real app, this would call an API
console.warn('Tab switch detected during exam');
}
}, [config.preventTabSwitch, isActive]);
const enterFullScreen = useCallback(async () => {
if (config.fullScreenMode && isActive) {
try {
await document.documentElement.requestFullscreen();
} catch (error) {
console.error('Failed to enter fullscreen:', error);
}
}
}, [config.fullScreenMode, isActive]);
const exitFullScreen = useCallback(async () => {
if (document.fullscreenElement) {
try {
await document.exitFullscreen();
} catch (error) {
console.error('Failed to exit fullscreen:', error);
}
}
}, []);
useEffect(() => {
if (isActive) {
document.addEventListener('contextmenu', handleRightClick);
document.addEventListener('keydown', handleKeyDown);
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => {
document.removeEventListener('contextmenu', handleRightClick);
document.removeEventListener('keydown', handleKeyDown);
document.removeEventListener('visibilitychange', handleVisibilityChange);
if (config.fullScreenMode) {
exitFullScreen();
}
};
}
}, [handleRightClick, handleKeyDown, handleVisibilityChange, exitFullScreen, isActive, config.fullScreenMode]);
return {
enterFullScreen,
exitFullScreen,
isFullScreen: !!document.fullscreenElement
};
};

View file

@ -0,0 +1,94 @@
import { useState, useEffect, useCallback } from 'react';
interface UseExamTimerProps {
initialTime: number; // in seconds
onTimeUp?: () => void;
onTick?: (timeRemaining: number) => void;
autoStart?: boolean;
}
export const useExamTimer = ({
initialTime,
onTimeUp,
onTick,
autoStart = false
}: UseExamTimerProps) => {
const [timeRemaining, setTimeRemaining] = useState(initialTime);
const [isRunning, setIsRunning] = useState(autoStart);
const [isPaused, setIsPaused] = useState(false);
const start = useCallback(() => {
setIsRunning(true);
setIsPaused(false);
}, []);
const pause = useCallback(() => {
setIsPaused(true);
}, []);
const resume = useCallback(() => {
setIsPaused(false);
}, []);
const reset = useCallback(() => {
setTimeRemaining(initialTime);
setIsRunning(false);
setIsPaused(false);
}, [initialTime]);
const stop = useCallback(() => {
setIsRunning(false);
setIsPaused(false);
}, []);
useEffect(() => {
let interval: NodeJS.Timeout;
if (isRunning && !isPaused && timeRemaining > 0) {
interval = setInterval(() => {
setTimeRemaining((prev) => {
const newTime = prev - 1;
onTick?.(newTime);
if (newTime <= 0) {
setIsRunning(false);
onTimeUp?.();
return 0;
}
return newTime;
});
}, 1000);
}
return () => {
if (interval) {
clearInterval(interval);
}
};
}, [isRunning, isPaused, timeRemaining, onTimeUp, onTick]);
const formatTime = useCallback((seconds: number) => {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
if (hours > 0) {
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
return `${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}, []);
return {
timeRemaining,
formattedTime: formatTime(timeRemaining),
isRunning,
isPaused,
start,
pause,
resume,
reset,
stop,
progress: ((initialTime - timeRemaining) / initialTime) * 100
};
};

View file

@ -263,7 +263,7 @@ const ClassList: React.FC = () => {
></Helmet> ></Helmet>
<Container> <Container>
{/* Stats Cards */} {/* Stats Cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4 sm:gap-6 mb-6 sm:mb-8"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4 sm:gap-6 mb-3">
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
@ -440,7 +440,8 @@ const ClassList: React.FC = () => {
{/* Sağ kısım: buton */} {/* Sağ kısım: buton */}
{showButtons && ( {showButtons && (
<div className="flex space-x-2"> <div className="flex space-x-2">
{user.role === 'teacher' && classSession.teacherId === user.id && ( {/* {user.role === 'teacher' && classSession.teacherId === user.id && ( */}
{user.role === 'teacher' && (
<> <>
<button <button
onClick={() => handlePlanningClass(classSession)} onClick={() => handlePlanningClass(classSession)}

View file

@ -13,7 +13,7 @@ import { Container } from '@/components/shared'
import { Helmet } from 'react-helmet' import { Helmet } from 'react-helmet'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
const ClassroomPlannerPage: React.FC = () => { const PlanningPage: React.FC = () => {
const [students, setStudents] = useState<Student[]>([]) const [students, setStudents] = useState<Student[]>([])
const [seats, setSeats] = useState<Seat[]>([]) const [seats, setSeats] = useState<Seat[]>([])
const [selectedClassroom, setSelectedClassroom] = useState<Classroom | null>(null) const [selectedClassroom, setSelectedClassroom] = useState<Classroom | null>(null)
@ -295,4 +295,4 @@ const ClassroomPlannerPage: React.FC = () => {
) )
} }
export default ClassroomPlannerPage export default PlanningPage

View file

@ -0,0 +1,502 @@
import { QuestionPool, Exam, Question } from "@/types/coordinator";
import React, { useState } from "react";
import { FaPlus, FaClock, FaUsers, FaCog, FaSave } from "react-icons/fa";
interface ExamCreatorProps {
pools: QuestionPool[];
onCreateExam: (exam: Omit<Exam, "id" | "creationTime" | "lastModificationTime">) => void;
onCancel?: () => void;
editingExam?: Exam;
}
export const ExamCreator: React.FC<ExamCreatorProps> = ({
pools,
onCreateExam,
onCancel,
editingExam,
}) => {
const [formData, setFormData] = useState({
title: editingExam?.title || "",
description: editingExam?.description || "",
type: editingExam?.type || ("exam" as "exam" | "assignment" | "test"),
timeLimit: editingExam?.timeLimit || 60,
passingScore: editingExam?.passingScore || 60,
maxAttempts: editingExam?.maxAttempts || 1,
allowReview: editingExam?.allowReview ?? true,
randomizeQuestions: editingExam?.randomizeQuestions ?? false,
showResults: editingExam?.showResults ?? true,
startTime: editingExam?.startTime?.toISOString().slice(0, 16) || "",
endTime: editingExam?.endTime?.toISOString().slice(0, 16) || "",
isActive: editingExam?.isActive ?? true,
});
const [selectedQuestions, setSelectedQuestions] = useState<Question[]>(
editingExam?.questions || []
);
const [selectedPool, setSelectedPool] = useState<string>("");
const [questionFilters, setQuestionFilters] = useState({
type: "",
difficulty: "",
tag: "",
});
const handleInputChange = (field: string, value: any) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
const addQuestionsFromPool = () => {
const pool = pools.find((p) => p.id === selectedPool);
if (!pool) return;
let questionsToAdd = pool.questions;
// Apply filters
if (questionFilters.type) {
questionsToAdd = questionsToAdd.filter(
(q) => q.type === questionFilters.type
);
}
if (questionFilters.difficulty) {
questionsToAdd = questionsToAdd.filter(
(q) => q.difficulty === questionFilters.difficulty
);
}
if (questionFilters.tag) {
questionsToAdd = questionsToAdd.filter((q) =>
q.tags.includes(questionFilters.tag)
);
}
// Add questions that aren't already selected
const newQuestions = questionsToAdd.filter(
(q) => !selectedQuestions.some((sq) => sq.id === q.id)
);
setSelectedQuestions((prev) => [...prev, ...newQuestions]);
};
const removeQuestion = (questionId: string) => {
setSelectedQuestions((prev) => prev.filter((q) => q.id !== questionId));
};
const moveQuestion = (index: number, direction: "up" | "down") => {
const newQuestions = [...selectedQuestions];
const targetIndex = direction === "up" ? index - 1 : index + 1;
if (targetIndex >= 0 && targetIndex < newQuestions.length) {
[newQuestions[index], newQuestions[targetIndex]] = [
newQuestions[targetIndex],
newQuestions[index],
];
setSelectedQuestions(newQuestions);
}
};
const handleSubmit = () => {
if (!formData.title.trim() || selectedQuestions.length === 0) {
alert("Please enter exam title and select at least one question.");
return;
}
const totalPoints = selectedQuestions.reduce((sum, q) => sum + q.points, 0);
const examData = {
...formData,
questions: selectedQuestions,
totalPoints,
startTime: formData.startTime ? new Date(formData.startTime) : undefined,
endTime: formData.endTime ? new Date(formData.endTime) : undefined,
};
onCreateExam(examData);
};
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-bold text-gray-900">
{editingExam ? "Sınavı Düzenle" : "Yeni Sınav Oluştur"}
</h2>
<p className="text-sm text-gray-600 mt-0.5">
Sınav ayarlarını yapılandırın ve soruları seçin
</p>
</div>
<div className="flex items-center space-x-2">
{onCancel && (
<button
onClick={onCancel}
className="flex items-center space-x-2 bg-gray-100 hover:bg-gray-200 text-gray-700 px-4 py-1.5 text-sm rounded-lg font-medium transition-colors"
>
<span>İptal</span>
</button>
)}
<button
onClick={handleSubmit}
className="flex items-center space-x-2 bg-blue-600 hover:bg-blue-700 text-white px-4 py-1.5 text-sm rounded-lg font-medium transition-colors"
>
<FaSave className="w-3.5 h-3.5" />
<span>{editingExam ? "Güncelle" : "Oluştur"}</span>
</button>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Basic Information */}
<div className="bg-white border border-gray-200 rounded-lg p-4">
<h3 className="text-base font-semibold text-gray-900 mb-3 flex items-center">
<FaCog className="w-4 h-4 mr-2" />
Temel Bilgiler
</h3>
<div className="space-y-3">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Exam Title
</label>
<input
type="text"
value={formData.title}
onChange={(e) => handleInputChange("title", e.target.value)}
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Enter exam title"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Description
</label>
<textarea
value={formData.description}
onChange={(e) =>
handleInputChange("description", e.target.value)
}
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
rows={3}
placeholder="Enter exam description"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Type
</label>
<select
value={formData.type}
onChange={(e) => handleInputChange("type", e.target.value)}
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="exam">Exam</option>
<option value="assignment">Assignment</option>
<option value="test">Test</option>
</select>
</div>
</div>
</div>
{/* Settings */}
<div className="bg-white border border-gray-200 rounded-lg p-4">
<h3 className="text-base font-semibold text-gray-900 mb-3 flex items-center">
<FaClock className="w-4 h-4 mr-2" />
Ayarlar
</h3>
<div className="space-y-3">
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Time Limit (minutes)
</label>
<input
type="number"
value={formData.timeLimit}
onChange={(e) =>
handleInputChange("timeLimit", parseInt(e.target.value))
}
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
min="1"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Passing Score (%)
</label>
<input
type="number"
value={formData.passingScore}
onChange={(e) =>
handleInputChange("passingScore", parseInt(e.target.value))
}
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
min="0"
max="100"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Maximum Attempts
</label>
<input
type="number"
value={formData.maxAttempts}
onChange={(e) =>
handleInputChange("maxAttempts", parseInt(e.target.value))
}
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
min="1"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Start Date
</label>
<input
type="datetime-local"
value={formData.startTime}
onChange={(e) =>
handleInputChange("startTime", e.target.value)
}
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
End Date
</label>
<input
type="datetime-local"
value={formData.endTime}
onChange={(e) => handleInputChange("endTime", e.target.value)}
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
<div className="space-y-2.5">
<label className="flex items-center">
<input
type="checkbox"
checked={formData.allowReview}
onChange={(e) =>
handleInputChange("allowReview", e.target.checked)
}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="ml-2 text-sm text-gray-700">
Allow answer review
</span>
</label>
<label className="flex items-center">
<input
type="checkbox"
checked={formData.randomizeQuestions}
onChange={(e) =>
handleInputChange("randomizeQuestions", e.target.checked)
}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="ml-2 text-sm text-gray-700">
Randomize questions
</span>
</label>
<label className="flex items-center">
<input
type="checkbox"
checked={formData.showResults}
onChange={(e) =>
handleInputChange("showResults", e.target.checked)
}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="ml-2 text-sm text-gray-700">Show results</span>
</label>
<label className="flex items-center">
<input
type="checkbox"
checked={formData.isActive}
onChange={(e) =>
handleInputChange("isActive", e.target.checked)
}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="ml-2 text-sm text-gray-700">Active</span>
</label>
</div>
</div>
</div>
</div>
{/* Question Selection */}
<div className="bg-white border border-gray-200 rounded-lg p-4">
<h3 className="text-base font-semibold text-gray-900 mb-3 flex items-center">
<FaUsers className="w-4 h-4 mr-2" />
Question Selection
</h3>
{/* Pool Selection */}
<div className="mb-4">
<div className="grid grid-cols-1 md:grid-cols-4 gap-3 mb-3">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Question Pool
</label>
<select
value={selectedPool}
onChange={(e) => setSelectedPool(e.target.value)}
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">Select pool</option>
{pools.map((pool) => (
<option key={pool.id} value={pool.id}>
{pool.name} ({pool.questions.length} questions)
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Question Type
</label>
<select
value={questionFilters.type}
onChange={(e) =>
setQuestionFilters((prev) => ({
...prev,
type: e.target.value,
}))
}
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">All types</option>
<option value="multiple-choice">Multiple Choice</option>
<option value="true-false">True/False</option>
<option value="fill-blank">Fill Blank</option>
<option value="open-ended">Open Ended</option>
<option value="multiple-answer">Multiple Answer</option>
<option value="matching">Matching</option>
<option value="ordering">Ordering</option>
<option value="calculation">Calculation</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Difficulty
</label>
<select
value={questionFilters.difficulty}
onChange={(e) =>
setQuestionFilters((prev) => ({
...prev,
difficulty: e.target.value,
}))
}
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">All levels</option>
<option value="easy">Easy</option>
<option value="medium">Medium</option>
<option value="hard">Hard</option>
</select>
</div>
<div className="flex items-end">
<button
onClick={addQuestionsFromPool}
disabled={!selectedPool}
className="w-full flex items-center justify-center space-x-2 bg-green-600 hover:bg-green-700 disabled:bg-gray-300 text-white py-2 rounded-lg text-xs transition-colors"
>
<FaPlus className="w-4 h-4" />
<span>Add Questions</span>
</button>
</div>
</div>
</div>
{/* Selected Questions */}
<div>
<div className="flex items-center justify-between mb-4">
<h4 className="text-md font-medium text-gray-900">
Selected Questions ({selectedQuestions.length})
</h4>
{selectedQuestions.length > 0 && (
<span className="text-sm text-gray-600">
Total Points:{" "}
{selectedQuestions.reduce((sum, q) => sum + q.points, 0)}
</span>
)}
</div>
{selectedQuestions.length > 0 ? (
<div className="space-y-3 max-h-96 overflow-y-auto">
{selectedQuestions.map((question, index) => (
<div
key={question.id}
className="flex items-center justify-between p-4 border border-gray-200 rounded-lg"
>
<div className="flex-1">
<div className="flex items-center space-x-2 mb-1">
<span className="bg-blue-100 text-blue-800 text-xs font-medium px-2 py-1 rounded">
{question.type}
</span>
<span className="bg-green-100 text-green-800 text-xs font-medium px-2 py-1 rounded">
{question.points} pts
</span>
</div>
<p className="text-sm font-medium text-gray-900">
{question.title}
</p>
<p className="text-xs text-gray-600 truncate">
{question.content}
</p>
</div>
<div className="flex items-center space-x-1 ml-4">
<button
onClick={() => moveQuestion(index, "up")}
disabled={index === 0}
className="p-1 text-gray-400 hover:text-gray-600 disabled:opacity-30 rounded"
>
</button>
<button
onClick={() => moveQuestion(index, "down")}
disabled={index === selectedQuestions.length - 1}
className="p-1 text-gray-400 hover:text-gray-600 disabled:opacity-30 rounded"
>
</button>
<button
onClick={() => removeQuestion(question.id)}
className="p-1 text-red-400 hover:text-red-600 rounded ml-2"
>
</button>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-8 text-gray-500">
<p>No questions selected yet</p>
<p className="text-sm">Select a pool above and add questions</p>
</div>
)}
</div>
</div>
</div>
);
};

View file

@ -0,0 +1,749 @@
import { Question, QuestionType, QuestionDifficulty, QuestionOption } from "@/types/coordinator";
import React, { useState, useEffect } from "react";
import { FaSave, FaPlus, FaTrash, FaTimes } from "react-icons/fa";
interface QuestionEditorProps {
question?: Question;
onSave: (question: Omit<Question, "id" | "creationTime" | "lastModificationTime">) => void;
onCancel: () => void;
}
export const QuestionEditor: React.FC<QuestionEditorProps> = ({
question,
onSave,
onCancel,
}) => {
const [formData, setFormData] = useState({
type: "multiple-choice" as QuestionType,
title: "",
content: "",
mediaUrl: "",
mediaType: "image" as "image" | "video",
points: 10,
timeLimit: 0,
explanation: "",
tags: [] as string[],
difficulty: "medium" as QuestionDifficulty,
});
const [options, setOptions] = useState<QuestionOption[]>([]);
const [correctAnswer, setCorrectAnswer] = useState<string | string[]>("");
const [tagInput, setTagInput] = useState("");
useEffect(() => {
if (question) {
setFormData({
type: question.type,
title: question.title,
content: question.content,
mediaUrl: question.mediaUrl || "",
mediaType: question.mediaType || "image",
points: question.points,
timeLimit: question.timeLimit || 0,
explanation: question.explanation || "",
tags: question.tags,
difficulty: question.difficulty,
});
setOptions(question.options || []);
setCorrectAnswer(question.correctAnswer || "");
}
}, [question]);
const handleInputChange = (field: string, value: any) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
const addOption = () => {
const newOption: QuestionOption = {
id: `opt-${Date.now()}`,
text: "",
isCorrect: false,
order: options.length,
};
setOptions((prev) => [...prev, newOption]);
};
const updateOption = (index: number, field: string, value: any) => {
setOptions((prev) =>
prev.map((opt, i) => (i === index ? { ...opt, [field]: value } : opt))
);
};
const removeOption = (index: number) => {
setOptions((prev) => prev.filter((_, i) => i !== index));
};
const addTag = () => {
if (tagInput.trim() && !formData.tags.includes(tagInput.trim())) {
setFormData((prev) => ({
...prev,
tags: [...prev.tags, tagInput.trim()],
}));
setTagInput("");
}
};
const removeTag = (tag: string) => {
setFormData((prev) => ({
...prev,
tags: prev.tags.filter((t) => t !== tag),
}));
};
const handleSave = () => {
if (!formData.title.trim() || !formData.content.trim()) {
alert("Please fill in the title and content fields.");
return;
}
if (formData.points <= 0) {
alert("Points must be greater than 0.");
return;
}
// Validate based on question type
if (
["multiple-choice", "multiple-answer"].includes(formData.type) &&
options.length < 2
) {
alert("Please add at least 2 options.");
return;
}
if (
formData.type === "multiple-choice" &&
!options.some((opt) => opt.isCorrect)
) {
alert("Please mark the correct answer.");
return;
}
const questionData = {
...formData,
options: [
"multiple-choice",
"multiple-answer",
"matching",
"ordering",
].includes(formData.type)
? options
: undefined,
correctAnswer: getCorrectAnswer(),
};
onSave(questionData);
};
const getCorrectAnswer = (): string | string[] => {
switch (formData.type) {
case "multiple-choice":
return options.find((opt) => opt.isCorrect)?.id || "";
case "multiple-answer":
return options.filter((opt) => opt.isCorrect).map((opt) => opt.id);
case "true-false":
return correctAnswer as string;
case "fill-blank":
case "open-ended":
case "calculation":
return correctAnswer as string;
case "matching":
return options.map((opt) => opt.id);
case "ordering":
return options
.sort((a, b) => (a.order || 0) - (b.order || 0))
.map((opt) => opt.id);
default:
return correctAnswer as string;
}
};
const renderQuestionTypeSpecificFields = () => {
switch (formData.type) {
case "multiple-choice":
return (
<div className="space-y-3">
<div className="flex items-center justify-between">
<h4 className="text-sm font-medium text-gray-900">
Yanıtlar (A, B, C, D, E)
</h4>
<button
type="button"
onClick={addOption}
className="flex items-center space-x-1 text-sm text-blue-600 hover:text-blue-700"
>
<FaPlus className="w-3.5 h-3.5" />
<span>Şık Ekle</span>
</button>
</div>
{options.map((option, index) => (
<div
key={option.id}
className="flex items-center space-x-2.5 p-2.5 border border-gray-200 rounded-lg"
>
<div className="bg-blue-100 text-blue-800 text-sm font-medium px-2 py-1 rounded min-w-8 text-center">
{String.fromCharCode(65 + index)}
</div>
<input
type="radio"
name="correct-answer"
checked={option.isCorrect}
onChange={(e) => {
// For single choice, uncheck others
setOptions((prev) =>
prev.map((opt, i) => ({
...opt,
isCorrect: i === index ? e.target.checked : false,
}))
);
}}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300"
/>
<input
type="text"
value={option.text}
onChange={(e) => updateOption(index, "text", e.target.value)}
placeholder={`${String.fromCharCode(65 + index)} şıkkı`}
className="flex-1 text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
type="button"
onClick={() => removeOption(index)}
className="text-red-500 hover:text-red-700"
>
<FaTrash className="w-4 h-4" />
</button>
</div>
))}
</div>
);
case "multiple-answer":
return (
<div className="space-y-3">
<div className="flex items-center justify-between">
<h4 className="text-sm font-medium text-gray-900">
Yanıtlar (Birden fazla doğru seçilebilir)
</h4>
<button
type="button"
onClick={addOption}
className="flex items-center space-x-1 text-sm text-blue-600 hover:text-blue-700"
>
<FaPlus className="w-3.5 h-3.5" />
<span>Yanıt Ekle</span>
</button>
</div>
{options.map((option, index) => (
<div
key={option.id}
className="flex items-center space-x-2.5 p-2.5 border border-gray-200 rounded-lg"
>
<input
type="checkbox"
checked={option.isCorrect}
onChange={(e) =>
updateOption(index, "isCorrect", e.target.checked)
}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<input
type="text"
value={option.text}
onChange={(e) => updateOption(index, "text", e.target.value)}
placeholder={`Yanıt ${index + 1}`}
className="flex-1 text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<div className="text-sm text-gray-600">
{option.isCorrect ? "Doğru" : "Yanlış"}
</div>
<button
type="button"
onClick={() => removeOption(index)}
className="text-red-500 hover:text-red-700"
>
<FaTrash className="w-4 h-4" />
</button>
</div>
))}
</div>
);
case "true-false":
return (
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Doğru Cevap
</label>
<div className="flex space-x-4">
<label className="flex items-center">
<input
type="radio"
name="true-false"
value="true"
checked={correctAnswer === "true"}
onChange={(e) => setCorrectAnswer(e.target.value)}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300"
/>
<span className="ml-2">Doğru</span>
</label>
<label className="flex items-center">
<input
type="radio"
name="true-false"
value="false"
checked={correctAnswer === "false"}
onChange={(e) => setCorrectAnswer(e.target.value)}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300"
/>
<span className="ml-2">Yanlış</span>
</label>
</div>
</div>
);
case "fill-blank":
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h4 className="text-md font-medium text-gray-900">
Boşluk Cevapları (Maksimum 10 boşluk)
</h4>
</div>
<div className="space-y-3">
{Array.from({ length: 10 }, (_, index) => {
const blankAnswers = ((correctAnswer as string) || "").split(
"|"
);
return (
<div key={index} className="flex items-center space-x-3">
<div className="bg-blue-100 text-blue-800 text-sm font-medium px-3 py-1 rounded min-w-16 text-center">
Boşluk {index + 1}
</div>
<input
type="text"
value={blankAnswers[index] || ""}
onChange={(e) => {
const newAnswers = [...blankAnswers];
newAnswers[index] = e.target.value;
// Remove empty answers from the end
while (
newAnswers.length > 0 &&
!newAnswers[newAnswers.length - 1]
) {
newAnswers.pop();
}
setCorrectAnswer(newAnswers.join("|"));
}}
placeholder={`${index + 1}. boşluk için kelime/cümle`}
className="flex-1 text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
);
})}
</div>
<p className="text-xs text-gray-500 mt-2">
Soru içeriğinde _____ veya [blank] kullanarak boşlukları
işaretleyin
</p>
</div>
);
case "matching":
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h4 className="text-md font-medium text-gray-900">
Eşleştirme Çiftleri
</h4>
<button
type="button"
onClick={addOption}
className="flex items-center space-x-1 text-blue-600 hover:text-blue-700"
>
<FaPlus className="w-4 h-4" />
<span>Çift Ekle</span>
</button>
</div>
{options.map((option, index) => (
<div
key={option.id}
className="flex items-center space-x-3 p-3 border border-gray-200 rounded-lg"
>
<input
type="text"
value={option.text.split("|")[0] || ""}
onChange={(e) => {
const rightSide = option.text.split("|")[1] || "";
updateOption(
index,
"text",
`${e.target.value}|${rightSide}`
);
}}
placeholder="Sol taraf (örn: PLUS)"
className="flex-1 text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<span className="text-gray-400"></span>
<input
type="text"
value={option.text.split("|")[1] || ""}
onChange={(e) => {
const leftSide = option.text.split("|")[0] || "";
updateOption(
index,
"text",
`${leftSide}|${e.target.value}`
);
}}
placeholder="Sağ taraf (örn: ARTI)"
className="flex-1 text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
type="button"
onClick={() => removeOption(index)}
className="text-red-500 hover:text-red-700"
>
<FaTrash className="w-4 h-4" />
</button>
</div>
))}
</div>
);
case "ordering":
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h4 className="text-md font-medium text-gray-900">
Sıralanacak Öğeler
</h4>
<button
type="button"
onClick={addOption}
className="flex items-center space-x-1 text-blue-600 hover:text-blue-700"
>
<FaPlus className="w-4 h-4" />
<span>Öğe Ekle</span>
</button>
</div>
{options.map((option, index) => (
<div
key={option.id}
className="flex items-center space-x-3 p-3 border border-gray-200 rounded-lg"
>
<div className="bg-blue-100 text-blue-800 text-sm font-medium px-2 py-1 rounded min-w-16 text-center">
Sıra
</div>
<input
type="number"
value={option.order || index + 1}
onChange={(e) =>
updateOption(index, "order", parseInt(e.target.value))
}
min="1"
className="w-20 text-sm border border-gray-300 rounded-lg px-2 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<input
type="text"
value={option.text}
onChange={(e) => updateOption(index, "text", e.target.value)}
placeholder={`Öğe ${index + 1} (örn: I, TAKE, A SHOWER)`}
className="flex-1 text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
type="button"
onClick={() => removeOption(index)}
className="text-red-500 hover:text-red-700"
>
<FaTrash className="w-4 h-4" />
</button>
</div>
))}
<p className="text-xs text-gray-500 mt-2">
Öğrenciler bu öğeleri doğru sıraya göre düzenleyecek (drag & drop
veya butonlarla)
</p>
</div>
);
case "open-ended":
return (
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Örnek Cevap (Opsiyonel)
</label>
<textarea
value={correctAnswer as string}
onChange={(e) => setCorrectAnswer(e.target.value)}
placeholder="Örnek bir cevap veya anahtar noktalar yazın..."
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
rows={4}
/>
<p className="text-xs text-gray-500 mt-1">
Öğrenci bu soruya ıklama yazabilecek
</p>
</div>
);
case "calculation":
return (
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Doğru Cevap (Sadece sayısal sonuç)
</label>
<input
type="text"
value={correctAnswer as string}
onChange={(e) => setCorrectAnswer(e.target.value)}
placeholder="Örn: 3, 15.5, 42"
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<p className="text-xs text-gray-500 mt-1">
Öğrenci matematiksel hesaplama yapıp sayısal sonuç yazacak
</p>
</div>
);
default:
return null;
}
};
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg max-w-4xl w-full max-h-[90vh] overflow-y-auto">
<div className="sticky top-0 bg-white border-b border-gray-200 px-5 py-3 flex items-center justify-between">
<h2 className="text-lg font-semibold text-gray-900">
{question ? "Edit Question" : "Create New Question"}
</h2>
<button
onClick={onCancel}
className="text-gray-400 hover:text-gray-600"
>
<FaTimes className="w-5 h-5" />
</button>
</div>
<div className="p-5 space-y-4">
{/* Basic Information */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1.5">
Question Type
</label>
<select
value={formData.type}
onChange={(e) => handleInputChange("type", e.target.value)}
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="multiple-choice">Multiple Choice</option>
<option value="fill-blank">Fill in the Blank</option>
<option value="multiple-answer">Multiple Answer</option>
<option value="matching">Matching</option>
<option value="ordering">Ordering</option>
<option value="open-ended">Open Ended</option>
<option value="true-false">True/False</option>
<option value="calculation">Calculation</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Points
</label>
<input
type="number"
value={formData.points}
onChange={(e) =>
handleInputChange("points", parseInt(e.target.value))
}
min="1"
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Question Title
</label>
<input
type="text"
value={formData.title}
onChange={(e) => handleInputChange("title", e.target.value)}
placeholder="Enter question title..."
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Question Content
</label>
<textarea
value={formData.content}
onChange={(e) => handleInputChange("content", e.target.value)}
placeholder="Enter the question content..."
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
rows={4}
/>
</div>
{/* Media Upload */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1.5">
Media URL (Optional)
</label>
<input
type="url"
value={formData.mediaUrl}
onChange={(e) => handleInputChange("mediaUrl", e.target.value)}
placeholder="https://example.com/image.jpg"
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1.5">
Media Type
</label>
<select
value={formData.mediaType}
onChange={(e) => handleInputChange("mediaType", e.target.value)}
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="image">Image</option>
<option value="video">Video</option>
</select>
</div>
</div>
{/* Question Type Specific Fields */}
{renderQuestionTypeSpecificFields()}
{/* Additional Settings */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Difficulty
</label>
<select
value={formData.difficulty}
onChange={(e) =>
handleInputChange("difficulty", e.target.value)
}
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="easy">Easy</option>
<option value="medium">Medium</option>
<option value="hard">Hard</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Time Limit (minutes, 0 = no limit)
</label>
<input
type="number"
value={formData.timeLimit}
onChange={(e) =>
handleInputChange("timeLimit", parseInt(e.target.value))
}
min="0"
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Add Tag
</label>
<div className="flex space-x-2">
<input
type="text"
value={tagInput}
onChange={(e) => setTagInput(e.target.value)}
onKeyPress={(e) => e.key === "Enter" && addTag()}
placeholder="grammar, vocabulary..."
className="flex-1 text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
type="button"
onClick={addTag}
className="px-3 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
<FaPlus className="w-4 h-4" />
</button>
</div>
</div>
</div>
{/* Tags Display */}
{formData.tags.length > 0 && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1.5">
Tags
</label>
<div className="flex flex-wrap gap-1.5">
{formData.tags.map((tag) => (
<span
key={tag}
className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800"
>
{tag}
<button
type="button"
onClick={() => removeTag(tag)}
className="ml-1.5 text-blue-600 hover:text-blue-800"
>
<FaTimes className="w-2.5 h-2.5" />
</button>
</span>
))}
</div>
</div>
)}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1.5">
Explanation (Optional)
</label>
<textarea
value={formData.explanation}
onChange={(e) => handleInputChange("explanation", e.target.value)}
placeholder="Provide an explanation for the correct answer..."
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
rows={3}
/>
</div>
</div>
{/* Footer */}
<div className="sticky bottom-0 bg-gray-50 border-t border-gray-200 px-5 py-3 flex justify-end space-x-2">
<button
onClick={onCancel}
className="px-3 py-1.5 text-sm border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 font-medium transition-colors"
>
Cancel
</button>
<button
onClick={handleSave}
className="flex items-center space-x-2 px-3 py-1.5 text-sm bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-colors"
>
<FaSave className="w-3.5 h-3.5" />
<span>Save Question</span>
</button>
</div>
</div>
</div>
);
};

View file

@ -0,0 +1,475 @@
import React, { useState } from "react";
import { QuestionEditor } from "./QuestionEditor";
import { FaPlus, FaSearch, FaFilter, FaEdit, FaTrash } from "react-icons/fa";
import { generateMockPools } from "@/mocks/mockPools";
import { QuestionPool, Question } from "@/types/coordinator";
export const QuestionPoolManager: React.FC = () => {
const [pools, setPools] = useState<QuestionPool[]>(generateMockPools());
const [searchTerm, setSearchTerm] = useState("");
const [selectedTag, setSelectedTag] = useState("");
const [isCreating, setIsCreating] = useState(false);
const [editingPool, setEditingPool] = useState<QuestionPool | null>(null);
const [editingQuestion, setEditingQuestion] = useState<Question | null>(null);
const [showQuestionEditor, setShowQuestionEditor] = useState(false);
const [selectedPoolForQuestion, setSelectedPoolForQuestion] =
useState<string>("");
const [newPool, setNewPool] = useState({
name: "",
description: "",
tags: [] as string[],
});
const filteredPools = pools.filter((pool) => {
const matchesSearch =
pool.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
pool.description.toLowerCase().includes(searchTerm.toLowerCase());
const matchesTag = !selectedTag || pool.tags.includes(selectedTag);
return matchesSearch && matchesTag;
});
const allTags = Array.from(new Set(pools.flatMap((pool) => pool.tags)));
const handleCreatePool = () => {
if (!newPool.name.trim()) return;
const newPoolData: QuestionPool = {
id: `pool-${Date.now()}`,
name: newPool.name,
description: newPool.description,
tags: newPool.tags,
questions: [],
createdBy: "current-user",
creationTime: new Date(),
};
setPools((prev) => [...prev, newPoolData]);
setNewPool({ name: "", description: "", tags: [] });
setIsCreating(false);
};
const handleUpdatePool = () => {
if (!editingPool) return;
setPools((prev) =>
prev.map((pool) => (pool.id === editingPool.id ? editingPool : pool))
);
setEditingPool(null);
};
const handleCreateQuestion = (poolId: string) => {
setSelectedPoolForQuestion(poolId);
setEditingQuestion(null);
setShowQuestionEditor(true);
};
const handleEditQuestion = (question: Question, poolId: string) => {
setSelectedPoolForQuestion(poolId);
setEditingQuestion(question);
setShowQuestionEditor(true);
};
const handleSaveQuestion = (
questionData: Omit<Question, "id" | "creationTime" | "lastModificationTime">
) => {
const pool = pools.find((p) => p.id === selectedPoolForQuestion);
if (!pool) return;
const newQuestion: Question = {
...questionData,
id: editingQuestion?.id || `q-${Date.now()}`,
creationTime: editingQuestion?.creationTime || new Date(),
lastModificationTime: new Date(),
};
const updatedQuestions = editingQuestion
? pool.questions.map((q) =>
q.id === editingQuestion.id ? newQuestion : q
)
: [...pool.questions, newQuestion];
const updatedPool = { ...pool, questions: updatedQuestions };
setPools((prev) =>
prev.map((p) => (p.id === updatedPool.id ? updatedPool : p))
);
setShowQuestionEditor(false);
};
const handleDeleteQuestion = (questionId: string, poolId: string) => {
const pool = pools.find((p) => p.id === poolId);
if (!pool) return;
if (window.confirm("Are you sure you want to delete this question?")) {
const updatedQuestions = pool.questions.filter(
(q) => q.id !== questionId
);
const updatedPool = { ...pool, questions: updatedQuestions };
setPools((prev) =>
prev.map((p) => (p.id === updatedPool.id ? updatedPool : p))
);
}
};
return (
<div className="space-y-4">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-bold text-gray-900">Soru Havuzları</h2>
<p className="text-sm text-gray-600 mt-0.5">
Soru havuzlarınızı oluşturun ve yönetin
</p>
</div>
<button
onClick={() => setIsCreating(true)}
className="flex items-center space-x-2 bg-blue-600 hover:bg-blue-700 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-colors"
>
<FaPlus className="w-3.5 h-3.5" />
<span>Yeni Havuz</span>
</button>
</div>
{/* Filters */}
<div className="bg-white border border-gray-200 rounded-lg p-3">
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<div className="relative">
<FaSearch className="absolute left-3 top-2 h-4 w-4 text-gray-400" />
<input
type="text"
placeholder="Havuz ara..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-8 w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="relative">
<FaFilter className="absolute left-3 top-2 h-4 w-4 text-gray-400" />
<select
value={selectedTag}
onChange={(e) => setSelectedTag(e.target.value)}
className="pl-10 w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500 appearance-none"
>
<option value="">Tüm etiketler</option>
{allTags.map((tag) => (
<option key={tag} value={tag}>
{tag}
</option>
))}
</select>
</div>
<div className="text-sm text-gray-600 flex items-center">
Toplam: {filteredPools.length} havuz
</div>
</div>
</div>
{/* Create/Edit Pool Modal */}
{(isCreating || editingPool) && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-5 w-full max-w-md">
<h3 className="text-base font-semibold mb-3">
{isCreating ? "Yeni Soru Havuzu" : "Havuzu Düzenle"}
</h3>
<div className="space-y-3">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Havuz Adı
</label>
<input
type="text"
value={isCreating ? newPool.name : editingPool?.name || ""}
onChange={(e) => {
if (isCreating) {
setNewPool((prev) => ({ ...prev, name: e.target.value }));
} else if (editingPool) {
setEditingPool({ ...editingPool, name: e.target.value });
}
}}
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Havuz adını girin"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
ıklama
</label>
<textarea
value={
isCreating
? newPool.description
: editingPool?.description || ""
}
onChange={(e) => {
if (isCreating) {
setNewPool((prev) => ({
...prev,
description: e.target.value,
}));
} else if (editingPool) {
setEditingPool({
...editingPool,
description: e.target.value,
});
}
}}
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
rows={3}
placeholder="Açıklama girin"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Etiketler (virgülle ayırın)
</label>
<input
type="text"
value={
isCreating
? newPool.tags.join(", ")
: editingPool?.tags.join(", ") || ""
}
onChange={(e) => {
const tags = e.target.value
.split(",")
.map((tag) => tag.trim())
.filter(Boolean);
if (isCreating) {
setNewPool((prev) => ({ ...prev, tags }));
} else if (editingPool) {
setEditingPool({ ...editingPool, tags });
}
}}
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="matematik, geometri, cebir"
/>
</div>
</div>
<div className="flex space-x-2 mt-4">
<button
onClick={() => {
if (isCreating) {
setIsCreating(false);
setNewPool({ name: "", description: "", tags: [] });
} else {
setEditingPool(null);
}
}}
className="flex-1 bg-gray-100 hover:bg-gray-200 text-gray-800 px-3 py-1.5 text-sm rounded-lg font-medium transition-colors"
>
İptal
</button>
<button
onClick={isCreating ? handleCreatePool : handleUpdatePool}
className="flex-1 bg-blue-600 hover:bg-blue-700 text-white px-3 py-1.5 text-sm rounded-lg font-medium transition-colors"
>
{isCreating ? "Oluştur" : "Güncelle"}
</button>
</div>
</div>
</div>
)}
{/* Question Editor Modal */}
{showQuestionEditor && (
<QuestionEditor
question={editingQuestion || undefined}
onSave={handleSaveQuestion}
onCancel={() => setShowQuestionEditor(false)}
/>
)}
{/* Pool List */}
<div className="space-y-4">
{filteredPools.map((pool) => (
<div
key={pool.id}
className="bg-white border border-gray-200 rounded-lg"
>
{/* Pool Header */}
<div className="p-4 border-b border-gray-200">
<div className="flex items-start justify-between">
<div className="flex-1">
<h3 className="text-base font-semibold text-gray-900 mb-1">
{pool.name}
</h3>
<p className="text-xs text-gray-600 mb-1.5">
{pool.description}
</p>
<div className="flex items-center text-xs text-gray-500 mb-2">
<span>{pool.questions.length} questions</span>
<span className="mx-2"></span>
<span>{pool.creationTime.toLocaleDateString("en-US")}</span>
</div>
{pool.tags.length > 0 && (
<div className="flex flex-wrap gap-1">
{pool.tags.map((tag) => (
<span
key={tag}
className="inline-flex items-center px-1.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800"
>
{tag}
</span>
))}
</div>
)}
</div>
<div className="flex items-center space-x-2 ml-2">
<button
onClick={() => handleCreateQuestion(pool.id)}
className="flex items-center space-x-1.5 bg-blue-600 hover:bg-blue-700 text-white px-2.5 py-1.5 text-xs rounded-lg font-medium transition-colors"
>
<FaPlus className="w-3 h-3" />
<span>Add Question</span>
</button>
<button
onClick={() => setEditingPool(pool)}
className="p-1.5 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
title="Edit Pool"
>
<FaEdit className="w-3.5 h-3.5" />
</button>
<button
onClick={() => {
if (
window.confirm(
"Are you sure you want to delete this pool?"
)
) {
setPools((prev) => prev.filter((p) => p.id !== pool.id));
}
}}
className="p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors"
title="Delete Pool"
>
<FaTrash className="w-3.5 h-3.5" />
</button>
</div>
</div>
</div>
{/* Questions List */}
<div className="p-4">
{pool.questions.length > 0 ? (
<div className="space-y-2">
<h4 className="text-sm font-medium text-gray-900 mb-2">
Questions ({pool.questions.length})
</h4>
{pool.questions.map((question) => (
<div
key={question.id}
className="flex items-center justify-between p-3 border border-gray-200 rounded-lg hover:bg-gray-50"
>
<div className="flex-1">
<div className="flex items-center space-x-1.5 mb-1">
<span className="bg-blue-100 text-blue-800 text-xs font-medium px-1.5 py-0.5 rounded">
{getQuestionTypeLabel(question.type)}
</span>
<span className="bg-green-100 text-green-800 text-xs font-medium px-1.5 py-0.5 rounded">
{question.points} pts
</span>
<span
className={`text-xs font-medium px-1.5 py-0.5 rounded ${
question.difficulty === "easy"
? "bg-green-100 text-green-800"
: question.difficulty === "medium"
? "bg-yellow-100 text-yellow-800"
: "bg-red-100 text-red-800"
}`}
>
{question.difficulty}
</span>
</div>
<p className="text-xs font-medium text-gray-900 mb-0.5">
{question.title}
</p>
<p className="text-xs text-gray-600 truncate">
{question.content}
</p>
{question.tags.length > 0 && (
<div className="flex flex-wrap gap-1 mt-1.5">
{question.tags.map((tag) => (
<span
key={tag}
className="text-xs bg-gray-100 text-gray-600 px-1.5 py-0.5 rounded"
>
{tag}
</span>
))}
</div>
)}
</div>
<div className="flex items-center space-x-1 ml-4">
<button
onClick={() => handleEditQuestion(question, pool.id)}
className="p-1.5 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
title="Edit Question"
>
<FaEdit className="w-3.5 h-3.5" />
</button>
<button
onClick={() =>
handleDeleteQuestion(question.id, pool.id)
}
className="p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors"
title="Delete Question"
>
<FaTrash className="w-3.5 h-3.5" />
</button>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-6 text-gray-500">
<p className="text-sm">No questions in this pool yet</p>
<p className="text-xs">Click "Add Question" to get started</p>
</div>
)}
</div>
</div>
))}
</div>
{filteredPools.length === 0 && (
<div className="text-center py-8">
<div className="w-12 h-12 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-3">
<FaSearch className="w-6 h-6 text-gray-400" />
</div>
<h3 className="text-base font-medium text-gray-900 mb-1">
No pools found
</h3>
<p className="text-sm text-gray-600">
{searchTerm || selectedTag
? "Try adjusting your search criteria or create a new pool."
: "Create your first question pool to get started."}
</p>
</div>
)}
</div>
);
};
const getQuestionTypeLabel = (type: string): string => {
const labels: Record<string, string> = {
"multiple-choice": "Multiple Choice",
"fill-blank": "Fill Blank",
"true-false": "True/False",
"open-ended": "Open Ended",
"multiple-answer": "Multiple Answer",
matching: "Matching",
ordering: "Ordering",
calculation: "Calculation",
};
return labels[type] || type;
};

View file

@ -0,0 +1,601 @@
import { Exam, AnswerKeyItem } from "@/types/coordinator";
import React, { useState } from "react";
import {
FaUpload,
FaFileAlt,
FaImage,
FaPlus,
FaTrash,
FaSave,
} from "react-icons/fa";
interface TestCreatorProps {
onCreateTest: (
test: Omit<Exam, "id" | "creationTime" | "lastModificationTime">
) => void;
onCancel: () => void;
editingTest?: Exam;
}
export const TestCreator: React.FC<TestCreatorProps> = ({
onCreateTest,
onCancel,
editingTest,
}) => {
const [formData, setFormData] = useState({
title: editingTest?.title || "",
description: editingTest?.description || "",
timeLimit: editingTest?.timeLimit || 60,
passingScore: editingTest?.passingScore || 60,
maxAttempts: editingTest?.maxAttempts || 1,
allowReview: editingTest?.allowReview ?? true,
showResults: editingTest?.showResults ?? true,
isActive: editingTest?.isActive ?? true,
});
const [testDocument, setTestDocument] = useState<{
url: string;
type: "pdf" | "image";
name: string;
}>(editingTest?.testDocument || { url: "", type: "pdf", name: "" });
const [answerKeyTemplate, setAnswerKeyTemplate] = useState<AnswerKeyItem[]>(
editingTest?.answerKeyTemplate || []
);
const [bulkAnswerMode, setBulkAnswerMode] = useState(false);
const [bulkAnswers, setBulkAnswers] = useState("");
const handleInputChange = (field: string, value: any) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
const handleDocumentChange = (field: string, value: any) => {
setTestDocument((prev) => ({ ...prev, [field]: value }));
};
const addAnswerKeyItem = () => {
const newItem: AnswerKeyItem = {
id: `item-${Date.now()}`,
questionNumber: answerKeyTemplate.length + 1,
type: "multiple-choice",
options: ["A", "B", "C", "D"],
points: 10,
};
setAnswerKeyTemplate((prev) => [...prev, newItem]);
};
const updateAnswerKeyItem = (index: number, field: string, value: any) => {
setAnswerKeyTemplate((prev) =>
prev.map((item, i) => (i === index ? { ...item, [field]: value } : item))
);
};
const removeAnswerKeyItem = (index: number) => {
setAnswerKeyTemplate((prev) => prev.filter((_, i) => i !== index));
// Renumber remaining items
setAnswerKeyTemplate((prev) =>
prev.map((item, i) => ({
...item,
questionNumber: i + 1,
}))
);
};
const handleBulkAnswerSubmit = () => {
const answers = bulkAnswers.split("\n").filter((line) => line.trim());
const newItems: AnswerKeyItem[] = answers.map((answer, index) => ({
id: `item-${Date.now()}-${index}`,
questionNumber: index + 1,
type: "multiple-choice",
options: ["A", "B", "C", "D"],
points: 10,
correctAnswer: answer.trim().toUpperCase(),
}));
setAnswerKeyTemplate(newItems);
setBulkAnswerMode(false);
setBulkAnswers("");
};
const handleSubmit = () => {
if (!formData.title.trim()) {
alert("Lütfen test başlığını girin.");
return;
}
if (!testDocument.url.trim()) {
alert("Lütfen test dokümanını yükleyin.");
return;
}
if (answerKeyTemplate.length === 0) {
alert("Lütfen en az bir cevap anahtarı öğesi ekleyin.");
return;
}
const totalPoints = answerKeyTemplate.reduce(
(sum, item) => sum + item.points,
0
);
const testData = {
...formData,
type: "test" as const,
testDocument,
answerKeyTemplate,
questions: [], // PDF testlerde soru listesi boş
totalPoints,
randomizeQuestions: false, // PDF testlerde sabit sıralama
};
onCreateTest(testData);
};
const renderAnswerKeyItemEditor = (item: AnswerKeyItem, index: number) => {
return (
<div key={item.id} className="border border-gray-200 rounded-lg p-3">
<div className="flex items-center justify-between mb-3">
<h4 className="text-sm font-medium text-gray-900">
<span className="inline-block bg-blue-100 text-blue-800 text-xs font-semibold px-2 py-1 rounded">
Soru {item.questionNumber}
</span>
</h4>
<button
onClick={() => removeAnswerKeyItem(index)}
className="text-red-500 hover:text-red-700"
>
<FaTrash className="w-3.5 h-3.5" />
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 mb-3">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Soru Tipi
</label>
<select
value={item.type}
onChange={(e) =>
updateAnswerKeyItem(index, "type", e.target.value)
}
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="multiple-choice">Çoktan Seçmeli</option>
<option value="fill-blank">Boşluk Doldurma</option>
<option value="true-false">Doğru-Yanlış</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Soru Numarası
</label>
<input
type="number"
value={item.questionNumber}
onChange={(e) =>
updateAnswerKeyItem(
index,
"questionNumber",
parseInt(e.target.value)
)
}
min="1"
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Puan
</label>
<input
type="number"
value={item.points}
onChange={(e) =>
updateAnswerKeyItem(index, "points", parseInt(e.target.value))
}
min="1"
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
{item.type === "multiple-choice" && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Seçenekler (virgülle ayırın)
</label>
<input
type="text"
value={item.options?.join(", ") || ""}
onChange={(e) => {
const options = e.target.value
.split(",")
.map((opt) => opt.trim())
.filter(Boolean);
updateAnswerKeyItem(index, "options", options);
}}
placeholder="A, B, C, D"
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
)}
<div className="mt-4">
<label className="block text-sm font-medium text-gray-700 mb-2">
Doğru Cevap (Opsiyonel - değerlendirme için)
</label>
{item.type === "multiple-choice" ? (
<select
value={(item.correctAnswer as string) || ""}
onChange={(e) =>
updateAnswerKeyItem(index, "correctAnswer", e.target.value)
}
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">Seçin...</option>
{item.options?.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
) : item.type === "true-false" ? (
<select
value={(item.correctAnswer as string) || ""}
onChange={(e) =>
updateAnswerKeyItem(index, "correctAnswer", e.target.value)
}
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">Seçin...</option>
<option value="true">Doğru</option>
<option value="false">Yanlış</option>
</select>
) : (
<input
type="text"
value={(item.correctAnswer as string) || ""}
onChange={(e) =>
updateAnswerKeyItem(index, "correctAnswer", e.target.value)
}
placeholder="Doğru cevabı girin..."
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
)}
</div>
</div>
);
};
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-bold text-gray-900">
{editingTest ? "Testi Düzenle" : "Yeni PDF Test Oluştur"}
</h2>
<p className="text-sm text-gray-600 mt-0.5">
PDF test belgesi yükleyin ve cevap anahtarını tanımlayın
</p>
</div>
<div className="flex items-center space-x-2">
<button
onClick={onCancel}
className="flex items-center space-x-2 bg-gray-100 hover:bg-gray-200 text-gray-700 px-4 py-1.5 text-sm rounded-lg font-medium transition-colors"
>
<span>İptal</span>
</button>
<button
onClick={handleSubmit}
className="flex items-center space-x-2 bg-blue-600 hover:bg-blue-700 text-white px-4 py-1.5 text-sm rounded-lg font-medium transition-colors"
>
<FaSave className="w-3.5 h-3.5" />
<span>{editingTest ? "Güncelle" : "Oluştur"}</span>
</button>
</div>
</div>
{/* Basic Information */}
<div className="bg-white border border-gray-200 rounded-lg p-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1.5">
Test Başlığı
</label>
<input
type="text"
value={formData.title}
onChange={(e) => handleInputChange("title", e.target.value)}
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Test başlığını girin"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1.5">
Süre (dakika)
</label>
<input
type="number"
value={formData.timeLimit}
onChange={(e) =>
handleInputChange("timeLimit", parseInt(e.target.value))
}
min="1"
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1.5">
ıklama
</label>
<textarea
value={formData.description}
onChange={(e) => handleInputChange("description", e.target.value)}
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
rows={3}
placeholder="Test açıklamasını girin"
/>
</div>
</div>
{/* Document Upload */}
<div className="bg-white border border-gray-200 rounded-lg p-4">
<h3 className="text-base font-semibold text-gray-900 mb-3 flex items-center">
<FaUpload className="w-4 h-4 mr-2" />
Test Dokümanı
</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 mb-3">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Dosya Tipi
</label>
<select
value={testDocument.type}
onChange={(e) => handleDocumentChange("type", e.target.value)}
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="pdf">PDF</option>
<option value="image">Resim</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Dosya Adı
</label>
<input
type="text"
value={testDocument.name}
onChange={(e) => handleDocumentChange("name", e.target.value)}
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Dosya adını girin"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Dosya URL'si
</label>
<input
type="url"
value={testDocument.url}
onChange={(e) => handleDocumentChange("url", e.target.value)}
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="https://example.com/test.pdf"
/>
</div>
</div>
{testDocument.url && (
<div className="mt-3 p-3 bg-gray-50 rounded-lg">
<div className="flex items-center space-x-2 text-xs text-gray-600">
{testDocument.type === "pdf" ? (
<FaFileAlt className="w-3.5 h-3.5 text-red-600" />
) : (
<FaImage className="w-3.5 h-3.5 text-blue-600" />
)}
<span>Önizleme: {testDocument.name || "Dosya"}</span>
</div>
</div>
)}
</div>
{/* Answer Key Template */}
<div className="bg-white border border-gray-200 rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<h3 className="text-base font-semibold text-gray-900">
Cevap Anahtarı Şablonu
</h3>
<div className="flex space-x-2">
<button
onClick={() => setBulkAnswerMode(true)}
className="flex items-center space-x-2 bg-green-600 hover:bg-green-700 text-white px-3 py-1.5 text-sm rounded-lg font-medium transition-colors"
>
<span>Toplu Cevap</span>
</button>
<button
onClick={addAnswerKeyItem}
className="flex items-center space-x-2 bg-blue-600 hover:bg-blue-700 text-white px-3 py-1.5 text-sm rounded-lg font-medium transition-colors"
>
<FaPlus className="w-3.5 h-3.5" />
<span>Soru Ekle</span>
</button>
</div>
</div>
{/* Bulk Answer Mode */}
{bulkAnswerMode && (
<div className="mb-4 p-3 bg-blue-50 border border-blue-200 rounded-lg">
<h4 className="text-sm font-medium text-blue-900 mb-2">
Toplu Cevap Girişi
</h4>
<p className="text-xs text-blue-700 mb-2">
Her satıra bir cevap yazın (A, B, C, D). Tüm sorular çoktan
seçmeli olarak oluşturulacak.
</p>
<textarea
value={bulkAnswers}
onChange={(e) => setBulkAnswers(e.target.value)}
placeholder="A&#10;B&#10;C&#10;D&#10;A"
className="w-full text-sm h-32 border border-blue-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<div className="flex space-x-2 mt-2">
<button
onClick={handleBulkAnswerSubmit}
className="px-3 py-1.5 text-sm bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-colors"
>
Uygula
</button>
<button
onClick={() => {
setBulkAnswerMode(false);
setBulkAnswers("");
}}
className="px-3 py-1.5 text-sm border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 font-medium transition-colors"
>
İptal
</button>
</div>
</div>
)}
{answerKeyTemplate.length > 0 ? (
<div className="space-y-3">
{answerKeyTemplate.map((item, index) =>
renderAnswerKeyItemEditor(item, index)
)}
{/* Special Question Types Warning */}
{answerKeyTemplate.some(
(item) => item.type !== "multiple-choice"
) && (
<div className="mt-3 p-2.5 bg-yellow-50 border border-yellow-200 rounded-lg">
<div className="flex items-center space-x-2">
<svg
className="w-4 h-4 text-yellow-600"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
clipRule="evenodd"
/>
</svg>
<span className="text-xs font-medium text-yellow-800">
Bu testte çoktan seçmeli olmayan sorular bulunmaktadır
</span>
</div>
</div>
)}
<div className="mt-4 p-3 bg-blue-50 border border-blue-200 rounded-lg">
<div className="flex items-center justify-between text-xs">
<span className="text-blue-800 font-medium">
Toplam Soru: {answerKeyTemplate.length}
</span>
<span className="text-blue-800 font-medium">
Toplam Puan:{" "}
{answerKeyTemplate.reduce(
(sum, item) => sum + item.points,
0
)}
</span>
</div>
</div>
</div>
) : (
<div className="text-center py-6 text-gray-500">
<p className="text-sm">Henüz cevap anahtarı öğesi eklenmedi</p>
<p className="text-xs">
Yukarıdaki "Soru Ekle" butonuna tıklayarak başlayın
</p>
</div>
)}
</div>
{/* Settings */}
<div className="bg-white border border-gray-200 rounded-lg p-4">
<h3 className="text-base font-semibold text-gray-900 mb-3">Ayarlar</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1.5">
Geçme Notu (%)
</label>
<input
type="number"
value={formData.passingScore}
onChange={(e) =>
handleInputChange("passingScore", parseInt(e.target.value))
}
min="0"
max="100"
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Maksimum Deneme
</label>
<input
type="number"
value={formData.maxAttempts}
onChange={(e) =>
handleInputChange("maxAttempts", parseInt(e.target.value))
}
min="1"
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
<div className="space-y-2.5">
<label className="flex items-center">
<input
type="checkbox"
checked={formData.allowReview}
onChange={(e) =>
handleInputChange("allowReview", e.target.checked)
}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="ml-2 text-sm text-gray-700">
Cevap incelemeye izin ver
</span>
</label>
<label className="flex items-center">
<input
type="checkbox"
checked={formData.showResults}
onChange={(e) =>
handleInputChange("showResults", e.target.checked)
}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="ml-2 text-sm text-gray-700">Sonuçları göster</span>
</label>
<label className="flex items-center">
<input
type="checkbox"
checked={formData.isActive}
onChange={(e) => handleInputChange("isActive", e.target.checked)}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="ml-2 text-sm text-gray-700">Aktif</span>
</label>
</div>
</div>
</div>
);
};

View file

@ -0,0 +1,249 @@
import React, { useState } from 'react'
import { ExamCreator } from './AdminPanel/ExamCreator'
import {
FaPlus,
FaSearch,
FaFilter,
FaEdit,
FaTrash,
FaClock,
FaUsers,
FaCalendar,
FaPlay,
} from 'react-icons/fa'
import { useNavigate } from 'react-router-dom'
import { generateMockExam } from '@/mocks/mockExams'
import { generateMockPools } from '@/mocks/mockPools'
import { Exam, QuestionPool } from '@/types/coordinator'
const Assignments: React.FC = () => {
const navigate = useNavigate()
const [assignments, setAssignments] = useState<Exam[]>(generateMockExam())
const [pools] = useState<QuestionPool[]>(generateMockPools())
const [searchTerm, setSearchTerm] = useState('')
const [statusFilter, setStatusFilter] = useState('')
const [isCreating, setIsCreating] = useState(false)
const [editingAssignment, setEditingAssignment] = useState<Exam | null>(null)
const assignmentList = assignments.filter((assignment) => assignment.type === 'assignment')
const filteredAssignments = assignmentList.filter((assignment) => {
const matchesSearch =
assignment.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
assignment.description.toLowerCase().includes(searchTerm.toLowerCase())
const matchesStatus =
!statusFilter ||
(statusFilter === 'active' && assignment.isActive) ||
(statusFilter === 'inactive' && !assignment.isActive)
return matchesSearch && matchesStatus
})
const handleCreateAssignment = (
assignmentData: Omit<Exam, 'id' | 'creationTime' | 'lastModificationTime'>,
) => {
const newAssignment: Exam = {
...assignmentData,
type: 'assignment',
id: `assignment-${Date.now()}`,
creationTime: new Date(),
lastModificationTime: new Date(),
}
setAssignments((prev) => [...prev, newAssignment])
setIsCreating(false)
}
const handleUpdateAssignment = (
assignmentData: Omit<Exam, 'id' | 'creationTime' | 'lastModificationTime'>,
) => {
if (editingAssignment) {
const updatedAssignment: Exam = {
...assignmentData,
id: editingAssignment.id,
creationTime: editingAssignment.creationTime,
lastModificationTime: new Date(),
}
setAssignments((prev) =>
prev.map((a) => (a.id === updatedAssignment.id ? updatedAssignment : a)),
)
setEditingAssignment(null)
setIsCreating(false)
}
}
const handleEdit = (assignment: Exam) => {
setEditingAssignment(assignment)
setIsCreating(true)
}
const handleCancel = () => {
setIsCreating(false)
setEditingAssignment(null)
}
if (isCreating) {
return (
<ExamCreator
pools={pools}
onCreateExam={editingAssignment ? handleUpdateAssignment : handleCreateAssignment}
onCancel={handleCancel}
editingExam={editingAssignment || undefined}
/>
)
}
return (
<div className="space-y-4">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-xl font-bold text-gray-900">Ödev Yönetimi</h1>
<p className="text-sm text-gray-600 mt-0.5">Ödevleri oluşturun ve yönetin</p>
</div>
<button
onClick={() => setIsCreating(true)}
className="flex items-center space-x-2 bg-blue-600 hover:bg-blue-700 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-colors"
>
<FaPlus className="w-3.5 h-3.5" />
<span>Yeni Ödev</span>
</button>
</div>
{/* Filters */}
<div className="bg-white border border-gray-200 rounded-lg p-3">
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<div className="relative">
<FaSearch className="absolute left-3 top-2 h-4 w-4 text-gray-400" />
<input
type="text"
placeholder="Ödev ara..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-8 text-sm w-full border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="relative">
<FaFilter className="absolute left-3 top-2 h-4 w-4 text-gray-400" />
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="pl-8 text-sm w-full border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500 appearance-none"
>
<option value="">Tüm durumlar</option>
<option value="active">Aktif</option>
<option value="inactive">Pasif</option>
</select>
</div>
<div className="text-sm text-gray-600 flex items-center">
Toplam: {filteredAssignments.length} ödev
</div>
</div>
</div>
{/* Assignments List */}
<div className="space-y-3">
{filteredAssignments.length > 0 ? (
filteredAssignments.map((assignment) => (
<div
key={assignment.id}
className="bg-white border border-gray-200 rounded-lg p-4 hover:shadow-md transition-shadow"
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center space-x-2 mb-1.5">
<h3 className="text-base font-semibold text-gray-900">{assignment.title}</h3>
<span
className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${
assignment.isActive
? 'bg-green-100 text-green-800'
: 'bg-gray-100 text-gray-800'
}`}
>
{assignment.isActive ? 'Aktif' : 'Pasif'}
</span>
</div>
<p className="text-sm text-gray-600 mb-2">{assignment.description}</p>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 text-sm">
<div className="flex items-center space-x-2">
<FaClock className="w-4 h-4 text-gray-400" />
<span className="text-gray-600">{assignment.timeLimit} dakika</span>
</div>
<div className="flex items-center space-x-2">
<FaUsers className="w-4 h-4 text-gray-400" />
<span className="text-gray-600">{assignment.questions.length} soru</span>
</div>
<div className="flex items-center space-x-2">
<FaCalendar className="w-4 h-4 text-gray-400" />
<span className="text-gray-600">Toplam: {assignment.totalPoints} puan</span>
</div>
<div className="flex items-center space-x-2">
<span className="text-gray-600">Geçme: %{assignment.passingScore}</span>
</div>
</div>
{assignment.startTime && assignment.endTime && (
<div className="mt-2 p-2 bg-orange-50 rounded">
<div className="text-xs text-orange-700">
<strong>Başlangıç:</strong> {assignment.startTime.toLocaleString('tr-TR')} -
<strong> Bitiş:</strong> {assignment.endTime.toLocaleString('tr-TR')}
</div>
</div>
)}
</div>
<div className="flex items-center space-x-1.5 ml-4">
<button
onClick={() => navigate(`/assignment/${assignment.id}`)}
className="flex items-center space-x-1 px-2.5 py-1.5 bg-green-600 hover:bg-green-700 text-white text-xs rounded-lg font-medium transition-colors"
>
<FaPlay className="w-3 h-3" />
<span>Başlat</span>
</button>
<button
onClick={() => handleEdit(assignment)}
className="p-1.5 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
title="Düzenle"
>
<FaEdit className="w-3.5 h-3.5" />
</button>
<button
onClick={() => {
if (window.confirm('Bu ödevi silmek istediğinizden emin misiniz?')) {
setAssignments((prev) => prev.filter((a) => a.id !== assignment.id))
}
}}
className="p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors"
title="Sil"
>
<FaTrash className="w-3.5 h-3.5" />
</button>
</div>
</div>
</div>
))
) : (
<div className="text-center py-8">
<div className="w-12 h-12 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-3">
<FaSearch className="w-6 h-6 text-gray-400" />
</div>
<h3 className="text-base font-medium text-gray-900 mb-1">Ödev bulunamadı</h3>
<p className="text-sm text-gray-600">
{searchTerm || statusFilter
? 'Arama kriterlerinizi değiştirin veya yeni ödev oluşturun.'
: 'İlk ödevinizi oluşturun.'}
</p>
</div>
)}
</div>
</div>
)
}
export default Assignments

View file

@ -0,0 +1,83 @@
import { Question, StudentAnswer } from '@/types/coordinator';
import React from 'react';
interface ExamNavigationProps {
questions: Question[];
answers: StudentAnswer[];
currentQuestionIndex: number;
onQuestionSelect: (index: number) => void;
}
export const ExamNavigation: React.FC<ExamNavigationProps> = ({
questions,
answers,
currentQuestionIndex,
onQuestionSelect
}) => {
const getQuestionStatus = (index: number) => {
const answer = answers.find(a => a.questionId === questions[index].id);
if (!answer || !answer.answer) return 'unanswered';
if (Array.isArray(answer.answer)) {
return answer.answer.some(a => a && a.toString().trim() !== '') ? 'answered' : 'unanswered';
}
return answer.answer.toString().trim() !== '' ? 'answered' : 'unanswered';
};
const getStatusColor = (index: number) => {
const status = getQuestionStatus(index);
const isCurrent = index === currentQuestionIndex;
if (isCurrent) {
return status === 'answered'
? 'bg-blue-600 text-white border-blue-600'
: 'bg-blue-100 text-blue-800 border-blue-300';
}
return status === 'answered'
? 'bg-green-100 text-green-800 border-green-300 hover:bg-green-200'
: 'bg-white text-gray-600 border-gray-300 hover:bg-gray-50';
};
const answeredCount = questions.filter((_, index) => getQuestionStatus(index) === 'answered').length;
const unansweredCount = questions.length - answeredCount;
return (
<div className="bg-white border border-gray-200 rounded-lg p-4 sticky top-4">
<div className="mb-4">
<h3 className="text-lg font-semibold text-gray-900 mb-2">Soru Haritası</h3>
<div className="flex items-center space-x-4 text-sm">
<div className="flex items-center space-x-1">
<div className="w-3 h-3 bg-green-100 border border-green-300 rounded"></div>
<span className="text-gray-600">{answeredCount} Cevaplanmış</span>
</div>
<div className="flex items-center space-x-1">
<div className="w-3 h-3 bg-white border border-gray-300 rounded"></div>
<span className="text-gray-600">{unansweredCount} Cevaplanmamış</span>
</div>
</div>
</div>
<div className="grid grid-cols-5 gap-2">
{questions.map((_, index) => (
<button
key={index}
onClick={() => onQuestionSelect(index)}
className={`w-10 h-10 text-sm font-medium rounded-lg border-2 transition-all ${getStatusColor(index)}`}
title={`Soru ${index + 1} - ${getQuestionStatus(index) === 'answered' ? 'Cevaplanmış' : 'Cevaplanmamış'}`}
>
{index + 1}
</button>
))}
</div>
<div className="mt-4 pt-4 border-t border-gray-200">
<div className="text-sm text-gray-600">
<div>Toplam: {questions.length} soru</div>
<div>Kalan: {unansweredCount} soru</div>
</div>
</div>
</div>
);
};

View file

@ -0,0 +1,93 @@
import { useExamTimer } from '@/utils/hooks/useExamTimer';
import React from 'react';
import { FaPlay } from 'react-icons/fa';
interface ExamTimerProps {
initialTime: number;
onTimeUp: () => void;
autoStart?: boolean;
className?: string;
}
export const ExamTimer: React.FC<ExamTimerProps> = ({
initialTime,
onTimeUp,
autoStart = true,
className = ''
}) => {
const { timeRemaining, formattedTime, isRunning, isPaused, start, pause, resume, progress } = useExamTimer({
initialTime,
onTimeUp,
autoStart
});
const getTimerColor = () => {
const percentage = (timeRemaining / initialTime) * 100;
if (percentage <= 10) return 'text-red-600 bg-red-50';
if (percentage <= 25) return 'text-orange-600 bg-orange-50';
return 'text-blue-600 bg-blue-50';
};
const getProgressColor = () => {
const percentage = (timeRemaining / initialTime) * 100;
if (percentage <= 10) return 'bg-red-500';
if (percentage <= 25) return 'bg-orange-500';
return 'bg-blue-500';
};
return (
<div className={`${className}`}>
<div className={`inline-flex items-center px-4 py-2 rounded-lg border ${getTimerColor()} transition-colors`}>
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span className="font-mono font-semibold text-lg">
{formattedTime}
</span>
{!isRunning && !isPaused && timeRemaining > 0 && (
<button
onClick={start}
className="ml-2 p-1 hover:bg-gray-200 rounded"
title="Başlat"
>
<FaPlay className="w-4 h-4" />
<span>Başlat</span>
</button>
)}
{isRunning && !isPaused && (
<button
onClick={pause}
className="ml-2 p-1 hover:bg-gray-200 rounded"
title="Duraklat"
>
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
</button>
)}
{isPaused && (
<button
onClick={resume}
className="ml-2 p-1 hover:bg-gray-200 rounded"
title="Devam Et"
>
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clipRule="evenodd" />
</svg>
</button>
)}
</div>
{/* Progress bar */}
<div className="mt-2 w-full bg-gray-200 rounded-full h-1.5">
<div
className={`h-1.5 rounded-full transition-all duration-1000 ${getProgressColor()}`}
style={{ width: `${100 - progress}%` }}
/>
</div>
</div>
);
};

View file

@ -0,0 +1,396 @@
import React, { useState, useEffect } from "react";
import { ExamTimer } from "./ExamTimer";
import { SecurityWarning } from "./SecurityWarning";
import { FaFileAlt, FaImage, FaCheckCircle } from "react-icons/fa";
import { Exam, ExamSession, StudentAnswer, AnswerKeyItem } from "@/types/coordinator";
import { useExamSecurity } from "@/utils/hooks/useExamSecurity";
interface PDFTestInterfaceProps {
exam: Exam;
studentId: string;
onExamComplete: (session: ExamSession) => void;
onExamSave?: (session: ExamSession) => void;
}
export const PDFTestInterface: React.FC<PDFTestInterfaceProps> = ({
exam,
studentId,
onExamComplete,
onExamSave,
}) => {
const [session, setSession] = useState<ExamSession>({
id: `session-${Date.now()}`,
examId: exam.id,
studentId,
startTime: new Date(),
answers: [],
status: "in-progress",
timeRemaining: exam.timeLimit * 60,
});
const [answerKeyResponses, setAnswerKeyResponses] = useState<
Record<string, string | string[]>
>({});
const [securityWarning, setSecurityWarning] = useState<{
show: boolean;
message: string;
type: "warning" | "error" | "info";
}>({ show: false, message: "", type: "warning" });
// Security configuration
const securityConfig = {
disableRightClick: true,
disableCopyPaste: true,
disableDevTools: true,
fullScreenMode: true,
preventTabSwitch: true,
};
useExamSecurity(securityConfig, session.status === "in-progress");
// Auto-save mechanism
useEffect(() => {
const interval = setInterval(() => {
if (session.status === "in-progress" && onExamSave) {
onExamSave(session);
}
}, 30000);
return () => clearInterval(interval);
}, [session, onExamSave]);
const handleAnswerChange = (itemId: string, answer: string | string[]) => {
setAnswerKeyResponses((prev) => ({
...prev,
[itemId]: answer,
}));
// Update session answers
setSession((prev) => {
const existingAnswerIndex = prev.answers.findIndex(
(a) => a.questionId === itemId
);
const newAnswer: StudentAnswer = {
questionId: itemId,
answer,
timeSpent: 0,
};
const newAnswers =
existingAnswerIndex >= 0
? prev.answers.map((a, i) =>
i === existingAnswerIndex ? newAnswer : a
)
: [...prev.answers, newAnswer];
return {
...prev,
answers: newAnswers,
};
});
};
const handleTimeUp = () => {
setSecurityWarning({
show: true,
message: "Süre doldu! Test otomatik olarak teslim ediliyor...",
type: "error",
});
setTimeout(() => {
completeExam();
}, 3000);
};
const completeExam = () => {
const completedSession: ExamSession = {
...session,
endTime: new Date(),
status: "completed",
};
setSession(completedSession);
onExamComplete(completedSession);
};
const handleSubmitExam = () => {
const unanswered =
exam.answerKeyTemplate?.filter(
(item) =>
!answerKeyResponses[item.id] ||
(Array.isArray(answerKeyResponses[item.id]) &&
(answerKeyResponses[item.id] as string[]).length === 0) ||
(typeof answerKeyResponses[item.id] === "string" &&
!answerKeyResponses[item.id])
) || [];
if (unanswered.length > 0) {
const confirmSubmit = window.confirm(
`${unanswered.length} soru cevaplanmamış. Yine de testi teslim etmek istiyor musunuz?`
);
if (!confirmSubmit) return;
}
completeExam();
};
const getAnsweredCount = () => {
return (
exam.answerKeyTemplate?.filter((item) => {
const answer = answerKeyResponses[item.id];
if (Array.isArray(answer)) {
return answer.length > 0 && answer.some((a) => a.trim() !== "");
}
return answer && answer.toString().trim() !== "";
}).length || 0
);
};
const renderAnswerKeyItem = (item: AnswerKeyItem) => {
const currentAnswer = answerKeyResponses[item.id];
switch (item.type) {
case "multiple-choice":
return (
<div className="space-y-2">
{item.options?.map((option, index) => (
<label
key={index}
className="flex items-center space-x-2 cursor-pointer"
>
<input
type="radio"
name={`question-${item.id}`}
value={option}
checked={currentAnswer === option}
onChange={(e) => handleAnswerChange(item.id, e.target.value)}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300"
/>
<span className="text-sm text-gray-700">{option}</span>
</label>
))}
</div>
);
case "fill-blank":
return (
<input
type="text"
value={(currentAnswer as string) || ""}
onChange={(e) => handleAnswerChange(item.id, e.target.value)}
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Cevabınızı yazın..."
/>
);
case "true-false":
return (
<div className="flex space-x-4">
<label className="flex items-center space-x-2 cursor-pointer">
<input
type="radio"
name={`question-${item.id}`}
value="true"
checked={currentAnswer === "true"}
onChange={(e) => handleAnswerChange(item.id, e.target.value)}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300"
/>
<span className="text-sm text-gray-700">Doğru</span>
</label>
<label className="flex items-center space-x-2 cursor-pointer">
<input
type="radio"
name={`question-${item.id}`}
value="false"
checked={currentAnswer === "false"}
onChange={(e) => handleAnswerChange(item.id, e.target.value)}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300"
/>
<span className="text-sm text-gray-700">Yanlış</span>
</label>
</div>
);
default:
return null;
}
};
if (session.status === "completed") {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
<div className="max-w-md w-full bg-white rounded-lg shadow-lg p-6 text-center">
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
<FaCheckCircle className="w-8 h-8 text-green-600" />
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-2">
Test Tamamlandı!
</h2>
<p className="text-gray-600 mb-6">
Testiniz başarıyla teslim edildi. Sonuçlarınız değerlendirildikten
sonra bilgilendirileceksiniz.
</p>
<div className="text-sm text-gray-500">
Başlama: {session.startTime.toLocaleString("tr-TR")}
<br />
Bitiş: {session.endTime?.toLocaleString("tr-TR")}
</div>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-50">
<SecurityWarning
isVisible={securityWarning.show}
onDismiss={() =>
setSecurityWarning((prev) => ({ ...prev, show: false }))
}
message={securityWarning.message}
type={securityWarning.type}
/>
{/* Header */}
<header className="bg-white border-b border-gray-200 px-4 py-3 sticky top-0 z-40">
<div className="max-w-7xl mx-auto flex items-center justify-between">
<div>
<h1 className="text-xl font-semibold text-gray-900">
{exam.title}
</h1>
<p className="text-sm text-gray-600">
PDF Test - {getAnsweredCount()} /{" "}
{exam.answerKeyTemplate?.length || 0} cevaplandı
</p>
</div>
<div className="flex items-center space-x-4">
<ExamTimer
initialTime={exam.timeLimit * 60}
onTimeUp={handleTimeUp}
autoStart={true}
/>
<button
onClick={handleSubmitExam}
className="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg font-medium transition-colors"
>
Testi Teslim Et
</button>
</div>
</div>
</header>
{/* Main Content */}
<div className="max-w-7xl mx-auto p-4">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Document Viewer */}
<div className="bg-white border border-gray-200 rounded-lg p-6">
<div className="flex items-center space-x-2 mb-4">
{exam.testDocument?.type === "pdf" ? (
<FaFileAlt className="w-5 h-5 text-red-600" />
) : (
<FaImage className="w-5 h-5 text-blue-600" />
)}
<h3 className="text-lg font-semibold text-gray-900">
Test Dokümanı
</h3>
<span className="text-sm text-gray-500">
({exam.testDocument?.name})
</span>
</div>
{exam.testDocument?.type === "pdf" ? (
<div
className="border border-gray-300 rounded-lg overflow-hidden"
style={{ height: "600px" }}
>
<iframe
src={exam.testDocument.url}
className="w-full h-full"
title="Test PDF"
/>
</div>
) : (
<div className="border border-gray-300 rounded-lg overflow-hidden">
<img
src={exam.testDocument?.url}
alt="Test Image"
className="w-full h-auto max-h-96 object-contain"
/>
</div>
)}
</div>
{/* Answer Key */}
<div className="bg-white border border-gray-200 rounded-lg p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">
Cevap Anahtarı
</h3>
<div className="space-y-6 max-h-96 overflow-y-auto">
{exam.answerKeyTemplate?.map((item) => {
const isAnswered =
answerKeyResponses[item.id] &&
(Array.isArray(answerKeyResponses[item.id])
? (answerKeyResponses[item.id] as string[]).length > 0
: answerKeyResponses[item.id].toString().trim() !== "");
return (
<div
key={item.id}
className={`p-4 border-2 rounded-lg transition-all ${
isAnswered
? "border-green-200 bg-green-50"
: "border-gray-200 bg-white"
}`}
>
<div className="flex items-center justify-between mb-3">
<div className="flex items-center space-x-2">
<span className="bg-blue-100 text-blue-800 text-sm font-medium px-2 py-1 rounded">
Soru {item.questionNumber}
</span>
<span className="bg-green-100 text-green-800 text-sm font-medium px-2 py-1 rounded">
{item.points} Puan
</span>
</div>
{isAnswered && (
<FaCheckCircle className="w-5 h-5 text-green-600" />
)}
</div>
{renderAnswerKeyItem(item)}
</div>
);
})}
</div>
{/* Progress Summary */}
<div className="mt-6 pt-4 border-t border-gray-200">
<div className="flex items-center justify-between text-sm text-gray-600">
<span>
İlerleme: {getAnsweredCount()} /{" "}
{exam.answerKeyTemplate?.length || 0}
</span>
<span>Toplam Puan: {exam.totalPoints}</span>
</div>
<div className="mt-2 w-full bg-gray-200 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
style={{
width: `${
(getAnsweredCount() /
(exam.answerKeyTemplate?.length || 1)) *
100
}%`,
}}
/>
</div>
</div>
</div>
</div>
</div>
</div>
);
};

View file

@ -0,0 +1,128 @@
import React from 'react';
import { CalculationQuestion } from '../QuestionTypes/CalculationQuestion';
import { FillBlankQuestion } from '../QuestionTypes/FillBlankQuestion';
import { MatchingQuestion } from '../QuestionTypes/MatchingQuestion';
import { MultipleAnswerQuestion } from '../QuestionTypes/MultipleAnswerQuestion';
import { MultipleChoiceQuestion } from '../QuestionTypes/MultipleChoiceQuestion';
import { OpenEndedQuestion } from '../QuestionTypes/OpenEndedQuestion';
import { OrderingQuestion } from '../QuestionTypes/OrderingQuestion';
import { TrueFalseQuestion } from '../QuestionTypes/TrueFalseQuestion';
import { Question, StudentAnswer } from '@/types/coordinator';
interface QuestionRendererProps {
question: Question;
answer?: StudentAnswer;
onAnswerChange: (questionId: string, answer: string | string[]) => void;
disabled?: boolean;
showCorrectAnswer?: boolean;
}
export const QuestionRenderer: React.FC<QuestionRendererProps> = ({
question,
answer,
onAnswerChange,
disabled = false,
showCorrectAnswer = false
}) => {
const renderQuestion = () => {
const commonProps = {
question,
answer,
onAnswerChange,
disabled,
showCorrectAnswer
};
switch (question.type) {
case 'multiple-choice':
return <MultipleChoiceQuestion {...commonProps} />;
case 'fill-blank':
return <FillBlankQuestion {...commonProps} />;
case 'true-false':
return <TrueFalseQuestion {...commonProps} />;
case 'open-ended':
return <OpenEndedQuestion {...commonProps} />;
case 'multiple-answer':
return <MultipleAnswerQuestion {...commonProps} />;
case 'matching':
return <MatchingQuestion {...commonProps} />;
case 'ordering':
return <OrderingQuestion {...commonProps} />;
case 'calculation':
return <CalculationQuestion {...commonProps} />;
default:
return (
<div className="p-4 border border-gray-200 rounded-lg bg-gray-50">
<p className="text-gray-500">Desteklenmeyen soru tipi: {question.type}</p>
</div>
);
}
};
return (
<div className="bg-white rounded-lg border border-gray-200 p-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-2">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
{getQuestionTypeLabel(question.type)}
</span>
{question.points && (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
{question.points} Puan
</span>
)}
{question.difficulty && (
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
question.difficulty === 'easy'
? 'bg-green-100 text-green-800'
: question.difficulty === 'medium'
? 'bg-yellow-100 text-yellow-800'
: 'bg-red-100 text-red-800'
}`}>
{getDifficultyLabel(question.difficulty)}
</span>
)}
</div>
{question.timeLimit && (
<span className="text-sm text-gray-500">
Süre: {question.timeLimit} dk
</span>
)}
</div>
{renderQuestion()}
</div>
);
};
const getQuestionTypeLabel = (type: string): string => {
const labels: Record<string, string> = {
'multiple-choice': 'Çoktan Seçmeli',
'fill-blank': 'Boşluk Doldurma',
'true-false': 'Doğru-Yanlış',
'open-ended': 'Açık Uçlu',
'multiple-answer': 'Çok Yanıtlı',
'matching': 'Eşleştirme',
'ordering': 'Sıralama',
'calculation': 'Hesaplama'
};
return labels[type] || type;
};
const getDifficultyLabel = (difficulty: string): string => {
const labels: Record<string, string> = {
'easy': 'Kolay',
'medium': 'Orta',
'hard': 'Zor'
};
return labels[difficulty] || difficulty;
};

View file

@ -0,0 +1,81 @@
import React from 'react';
interface SecurityWarningProps {
isVisible: boolean;
onDismiss: () => void;
message: string;
type: 'warning' | 'error' | 'info';
}
export const SecurityWarning: React.FC<SecurityWarningProps> = ({
isVisible,
onDismiss,
message,
type = 'warning'
}) => {
if (!isVisible) return null;
const getColors = () => {
switch (type) {
case 'error':
return 'bg-red-50 border-red-200 text-red-800';
case 'info':
return 'bg-blue-50 border-blue-200 text-blue-800';
default:
return 'bg-yellow-50 border-yellow-200 text-yellow-800';
}
};
const getIcon = () => {
switch (type) {
case 'error':
return (
<svg className="w-5 h-5 text-red-400" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
</svg>
);
case 'info':
return (
<svg className="w-5 h-5 text-blue-400" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
</svg>
);
default:
return (
<svg className="w-5 h-5 text-yellow-400" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
);
}
};
return (
<div className="fixed top-4 left-1/2 transform -translate-x-1/2 z-50 max-w-md w-full mx-4">
<div className={`border rounded-lg p-4 ${getColors()}`}>
<div className="flex items-center">
<div className="flex-shrink-0">
{getIcon()}
</div>
<div className="ml-3">
<p className="text-sm font-medium">
{message}
</p>
</div>
<div className="ml-auto pl-3">
<div className="-mx-1.5 -my-1.5">
<button
onClick={onDismiss}
className="inline-flex rounded-md p-1.5 hover:bg-yellow-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-yellow-50 focus:ring-yellow-600"
>
<span className="sr-only">Dismiss</span>
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd" />
</svg>
</button>
</div>
</div>
</div>
</div>
</div>
);
};

View file

@ -0,0 +1,250 @@
import React, { useState, useEffect } from 'react';
import { QuestionRenderer } from './QuestionRenderer';
import { ExamNavigation } from './ExamNavigation';
import { ExamTimer } from './ExamTimer';
import { SecurityWarning } from './SecurityWarning';
import { Exam, ExamSession, StudentAnswer } from '@/types/coordinator';
import { useExamSecurity } from '@/utils/hooks/useExamSecurity';
interface StudentExamInterfaceProps {
exam: Exam;
studentId: string;
onExamComplete: (session: ExamSession) => void;
onExamSave?: (session: ExamSession) => void;
}
export const StudentExamInterface: React.FC<StudentExamInterfaceProps> = ({
exam,
studentId,
onExamComplete,
onExamSave
}) => {
const [session, setSession] = useState<ExamSession>({
id: `session-${Date.now()}`,
examId: exam.id,
studentId,
startTime: new Date(),
answers: [],
status: 'in-progress',
timeRemaining: exam.timeLimit * 60
});
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
const [securityWarning, setSecurityWarning] = useState<{
show: boolean;
message: string;
type: 'warning' | 'error' | 'info';
}>({ show: false, message: '', type: 'warning' });
// Security configuration
const securityConfig = {
disableRightClick: true,
disableCopyPaste: true,
disableDevTools: true,
fullScreenMode: true,
preventTabSwitch: true
};
useExamSecurity(securityConfig, session.status === 'in-progress');
// Auto-save mechanism
useEffect(() => {
const interval = setInterval(() => {
if (session.status === 'in-progress' && onExamSave) {
onExamSave(session);
}
}, 30000); // Save every 30 seconds
return () => clearInterval(interval);
}, [session, onExamSave]);
const handleAnswerChange = (questionId: string, answer: string | string[]) => {
setSession(prev => {
const existingAnswerIndex = prev.answers.findIndex(a => a.questionId === questionId);
const newAnswer: StudentAnswer = {
questionId,
answer,
timeSpent: 0 // In a real app, track time spent per question
};
const newAnswers = existingAnswerIndex >= 0
? prev.answers.map((a, i) => i === existingAnswerIndex ? newAnswer : a)
: [...prev.answers, newAnswer];
return {
...prev,
answers: newAnswers
};
});
};
const handleTimeUp = () => {
setSecurityWarning({
show: true,
message: 'Süre doldu! Sınav otomatik olarak teslim ediliyor...',
type: 'error'
});
setTimeout(() => {
completeExam();
}, 3000);
};
const completeExam = () => {
const completedSession: ExamSession = {
...session,
endTime: new Date(),
status: 'completed'
};
setSession(completedSession);
onExamComplete(completedSession);
};
const handleSubmitExam = () => {
const unanswered = exam.questions.filter(q =>
!session.answers.find(a => a.questionId === q.id && a.answer)
);
if (unanswered.length > 0) {
const confirmSubmit = window.confirm(
`${unanswered.length} soru cevaplanmamış. Yine de sınavı teslim etmek istiyor musunuz?`
);
if (!confirmSubmit) return;
}
completeExam();
};
const currentQuestion = exam.questions[currentQuestionIndex];
const currentAnswer = session.answers.find(a => a.questionId === currentQuestion?.id);
const navigateQuestion = (direction: 'next' | 'prev' | number) => {
if (typeof direction === 'number') {
setCurrentQuestionIndex(Math.max(0, Math.min(exam.questions.length - 1, direction)));
} else if (direction === 'next') {
setCurrentQuestionIndex(prev => Math.min(exam.questions.length - 1, prev + 1));
} else {
setCurrentQuestionIndex(prev => Math.max(0, prev - 1));
}
};
if (session.status === 'completed') {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
<div className="max-w-md w-full bg-white rounded-lg shadow-lg p-6 text-center">
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
<svg className="w-8 h-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-2">Sınav Tamamlandı!</h2>
<p className="text-gray-600 mb-6">
Sınavınız başarıyla teslim edildi. Sonuçlarınız değerlendirildikten sonra bilgilendirileceksiniz.
</p>
<div className="text-sm text-gray-500">
Başlama: {session.startTime.toLocaleString('tr-TR')}<br />
Bitiş: {session.endTime?.toLocaleString('tr-TR')}
</div>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-50">
<SecurityWarning
isVisible={securityWarning.show}
onDismiss={() => setSecurityWarning(prev => ({ ...prev, show: false }))}
message={securityWarning.message}
type={securityWarning.type}
/>
{/* Header */}
<header className="bg-white border-b border-gray-200 px-4 py-3">
<div className="max-w-7xl mx-auto flex items-center justify-between">
<div>
<h1 className="text-xl font-semibold text-gray-900">{exam.title}</h1>
<p className="text-sm text-gray-600">
Soru {currentQuestionIndex + 1} / {exam.questions.length}
</p>
</div>
<div className="flex items-center space-x-4">
<ExamTimer
initialTime={exam.timeLimit * 60}
onTimeUp={handleTimeUp}
autoStart={true}
/>
<button
onClick={handleSubmitExam}
className="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg font-medium transition-colors"
>
Sınavı Teslim Et
</button>
</div>
</div>
</header>
{/* Main Content */}
<div className="max-w-7xl mx-auto p-4">
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
{/* Question Content */}
<div className="lg:col-span-3 space-y-6">
{currentQuestion && (
<QuestionRenderer
question={currentQuestion}
answer={currentAnswer}
onAnswerChange={handleAnswerChange}
disabled={session.status !== 'in-progress'}
/>
)}
{/* Navigation Controls */}
<div className="flex items-center justify-between bg-white border border-gray-200 rounded-lg p-4">
<button
onClick={() => navigateQuestion('prev')}
disabled={currentQuestionIndex === 0}
className="flex items-center space-x-2 px-4 py-2 bg-gray-100 hover:bg-gray-200 disabled:bg-gray-50 disabled:text-gray-400 text-gray-700 rounded-lg font-medium transition-colors"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
<span>Önceki</span>
</button>
<div className="flex items-center space-x-2">
<span className="text-sm text-gray-600">
{currentQuestionIndex + 1} / {exam.questions.length}
</span>
</div>
<button
onClick={() => navigateQuestion('next')}
disabled={currentQuestionIndex === exam.questions.length - 1}
className="flex items-center space-x-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-50 disabled:text-gray-400 text-white rounded-lg font-medium transition-colors"
>
<span>Sonraki</span>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</button>
</div>
</div>
{/* Navigation Sidebar */}
<div className="lg:col-span-1">
<ExamNavigation
questions={exam.questions}
answers={session.answers}
currentQuestionIndex={currentQuestionIndex}
onQuestionSelect={navigateQuestion}
/>
</div>
</div>
</div>
</div>
);
};

View file

@ -0,0 +1,245 @@
import React, { useState } from 'react'
import { ExamCreator } from './AdminPanel/ExamCreator'
import {
FaPlus,
FaSearch,
FaFilter,
FaEdit,
FaTrash,
FaClock,
FaUsers,
FaCalendar,
FaPlay,
} from 'react-icons/fa'
import { useNavigate } from 'react-router-dom'
import { generateMockExam } from '@/mocks/mockExams'
import { generateMockPools } from '@/mocks/mockPools'
import { Exam, QuestionPool } from '@/types/coordinator'
const Exams: React.FC = () => {
const navigate = useNavigate()
const [exams, setExams] = useState<Exam[]>(generateMockExam())
const [pools] = useState<QuestionPool[]>(generateMockPools())
const [searchTerm, setSearchTerm] = useState('')
const [statusFilter, setStatusFilter] = useState('')
const [isCreating, setIsCreating] = useState(false)
const [editingExam, setEditingExam] = useState<Exam | null>(null)
const examList = exams.filter((exam) => exam.type === 'exam')
const filteredExams = examList.filter((exam) => {
const matchesSearch =
exam.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
exam.description.toLowerCase().includes(searchTerm.toLowerCase())
const matchesStatus =
!statusFilter ||
(statusFilter === 'active' && exam.isActive) ||
(statusFilter === 'inactive' && !exam.isActive)
return matchesSearch && matchesStatus
})
const handleCreateExam = (
examData: Omit<Exam, 'id' | 'creationTime' | 'lastModificationTime'>,
) => {
const newExam: Exam = {
...examData,
type: 'exam',
id: `exam-${Date.now()}`,
creationTime: new Date(),
lastModificationTime: new Date(),
}
setExams((prev) => [...prev, newExam])
setIsCreating(false)
}
const handleUpdateExam = (
examData: Omit<Exam, 'id' | 'creationTime' | 'lastModificationTime'>,
) => {
if (editingExam) {
const updatedExam: Exam = {
...examData,
id: editingExam.id,
creationTime: editingExam.creationTime,
lastModificationTime: new Date(),
}
setExams((prev) => prev.map((exam) => (exam.id === updatedExam.id ? updatedExam : exam)))
setEditingExam(null)
setIsCreating(false)
}
}
const handleEdit = (exam: Exam) => {
setEditingExam(exam)
setIsCreating(true)
}
const handleCancel = () => {
setIsCreating(false)
setEditingExam(null)
}
if (isCreating) {
return (
<ExamCreator
pools={pools}
onCreateExam={editingExam ? handleUpdateExam : handleCreateExam}
onCancel={handleCancel}
editingExam={editingExam || undefined}
/>
)
}
return (
<div className="space-y-4">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-xl font-bold text-gray-900">Sınav Yönetimi</h1>
<p className="text-sm text-gray-600 mt-0.5">Sınavları oluşturun ve yönetin</p>
</div>
<button
onClick={() => setIsCreating(true)}
className="flex items-center space-x-2 bg-blue-600 hover:bg-blue-700 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-colors"
>
<FaPlus className="w-3.5 h-3.5" />
<span>Yeni Sınav</span>
</button>
</div>
{/* Filters */}
<div className="bg-white border border-gray-200 rounded-lg p-3">
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<div className="relative">
<FaSearch className="absolute left-3 top-2 h-4 w-4 text-gray-400" />
<input
type="text"
placeholder="Sınav ara..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-8 text-sm w-full border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="relative">
<FaFilter className="absolute left-3 top-2 h-4 w-4 text-gray-400" />
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="pl-8 text-sm w-full border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500 appearance-none"
>
<option value="">Tüm durumlar</option>
<option value="active">Aktif</option>
<option value="inactive">Pasif</option>
</select>
</div>
<div className="text-sm text-gray-600 flex items-center">
Toplam: {filteredExams.length} sınav
</div>
</div>
</div>
{/* Exams List */}
<div className="space-y-3">
{filteredExams.length > 0 ? (
filteredExams.map((exam) => (
<div
key={exam.id}
className="bg-white border border-gray-200 rounded-lg p-4 hover:shadow-md transition-shadow"
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center space-x-2 mb-1.5">
<h3 className="text-base font-semibold text-gray-900">{exam.title}</h3>
<span
className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${
exam.isActive ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'
}`}
>
{exam.isActive ? 'Aktif' : 'Pasif'}
</span>
</div>
<p className="text-sm text-gray-600 mb-2">{exam.description}</p>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 text-sm">
<div className="flex items-center space-x-2">
<FaClock className="w-4 h-4 text-gray-400" />
<span className="text-gray-600">{exam.timeLimit} dakika</span>
</div>
<div className="flex items-center space-x-2">
<FaUsers className="w-4 h-4 text-gray-400" />
<span className="text-gray-600">{exam.questions.length} soru</span>
</div>
<div className="flex items-center space-x-2">
<FaCalendar className="w-4 h-4 text-gray-400" />
<span className="text-gray-600">Toplam: {exam.totalPoints} puan</span>
</div>
<div className="flex items-center space-x-2">
<span className="text-gray-600">Geçme: %{exam.passingScore}</span>
</div>
</div>
{exam.startTime && exam.endTime && (
<div className="mt-2 p-2 bg-blue-50 rounded">
<div className="text-xs text-blue-700">
<strong>Başlangıç:</strong> {exam.startTime.toLocaleString('tr-TR')} -
<strong> Bitiş:</strong> {exam.endTime.toLocaleString('tr-TR')}
</div>
</div>
)}
</div>
<div className="flex items-center space-x-1.5 ml-4">
<button
onClick={() => navigate(`/exam/${exam.id}`)}
className="flex items-center space-x-1 px-2.5 py-1.5 bg-green-600 hover:bg-green-700 text-white text-xs rounded-lg font-medium transition-colors"
>
<FaPlay className="w-3 h-3" />
<span>Başlat</span>
</button>
<button
onClick={() => handleEdit(exam)}
className="p-1.5 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
title="Düzenle"
>
<FaEdit className="w-3.5 h-3.5" />
</button>
<button
onClick={() => {
if (window.confirm('Bu sınavı silmek istediğinizden emin misiniz?')) {
setExams((prev) => prev.filter((e) => e.id !== exam.id))
}
}}
className="p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors"
title="Sil"
>
<FaTrash className="w-3.5 h-3.5" />
</button>
</div>
</div>
</div>
))
) : (
<div className="text-center py-8">
<div className="w-12 h-12 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-3">
<FaSearch className="w-6 h-6 text-gray-400" />
</div>
<h3 className="text-base font-medium text-gray-900 mb-1">Sınav bulunamadı</h3>
<p className="text-sm text-gray-600">
{searchTerm || statusFilter
? 'Arama kriterlerinizi değiştirin veya yeni sınav oluşturun.'
: 'İlk sınavınızı oluşturun.'}
</p>
</div>
)}
</div>
</div>
)
}
export default Exams

View file

@ -0,0 +1,116 @@
import { Question, StudentAnswer } from '@/types/coordinator';
import React, { useState, useEffect } from 'react';
interface CalculationQuestionProps {
question: Question;
answer?: StudentAnswer;
onAnswerChange: (questionId: string, answer: string) => void;
disabled?: boolean;
showCorrectAnswer?: boolean;
}
export const CalculationQuestion: React.FC<CalculationQuestionProps> = ({
question,
answer,
onAnswerChange,
disabled = false,
showCorrectAnswer = false
}) => {
const [result, setResult] = useState<string>('');
useEffect(() => {
if (answer?.answer) {
setResult(answer.answer as string);
}
}, [answer?.answer]);
const handleResultChange = (value: string) => {
if (!disabled) {
setResult(value);
onAnswerChange(question.id, value);
}
};
const correctAnswer = question.correctAnswer as string || '';
const isResultCorrect = showCorrectAnswer &&
result.trim() === correctAnswer.trim();
return (
<div className="space-y-4">
<div className="prose max-w-none">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
{question.title}
</h3>
{question.content && (
<p className="text-gray-700 mb-4">{question.content}</p>
)}
{question.mediaUrl && (
<div className="mb-4">
{question.mediaType === 'image' ? (
<img
src={question.mediaUrl}
alt="Question media"
className="max-w-full h-auto rounded-lg shadow-sm"
/>
) : question.mediaType === 'video' ? (
<video
src={question.mediaUrl}
controls
className="max-w-full h-auto rounded-lg shadow-sm"
/>
) : null}
</div>
)}
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Cevap (Sayısal):
</label>
<input
type="text"
value={result}
onChange={(e) => handleResultChange(e.target.value)}
disabled={disabled}
placeholder="Sayısal cevabınızı yazın (örn: 3, 15.5)"
className={`w-full text-sm p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
disabled ? 'bg-gray-50 cursor-not-allowed' : 'bg-white'
} ${
isResultCorrect
? 'border-green-500 bg-green-50'
: showCorrectAnswer && result
? 'border-red-500 bg-red-50'
: ''
}`}
/>
</div>
</div>
{showCorrectAnswer && correctAnswer && (
<div className="mt-4">
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
<h4 className="text-sm font-medium text-blue-800 mb-2">
Doğru Cevap:
</h4>
<p className="text-sm text-blue-700 font-mono">
{correctAnswer}
</p>
</div>
</div>
)}
{question.explanation && showCorrectAnswer && (
<div className="mt-4 p-4 bg-gray-50 border border-gray-200 rounded-lg">
<h4 className="text-sm font-medium text-gray-800 mb-2">
ıklama:
</h4>
<p className="text-sm text-gray-700">
{question.explanation}
</p>
</div>
)}
</div>
);
};

View file

@ -0,0 +1,115 @@
import { Question, StudentAnswer } from '@/types/coordinator';
import React, { useState, useEffect } from 'react';
interface FillBlankQuestionProps {
question: Question;
answer?: StudentAnswer;
onAnswerChange: (questionId: string, answer: string[]) => void;
disabled?: boolean;
showCorrectAnswer?: boolean;
}
export const FillBlankQuestion: React.FC<FillBlankQuestionProps> = ({
question,
answer,
onAnswerChange,
disabled = false,
showCorrectAnswer = false
}) => {
const [blanks, setBlanks] = useState<string[]>([]);
// Parse content to find blanks marked with _____ or [blank]
const parseContent = (content: string): { parts: string[]; blankCount: number } => {
const parts = content.split(/(_____|\[blank\])/g);
const blankCount = parts.filter(part => part === '_____' || part === '[blank]').length;
return { parts, blankCount };
};
const { parts, blankCount } = parseContent(question.content);
useEffect(() => {
if (answer?.answer && Array.isArray(answer.answer)) {
setBlanks(answer.answer);
} else {
setBlanks(new Array(blankCount).fill(''));
}
}, [answer?.answer, blankCount]);
const handleBlankChange = (index: number, value: string) => {
if (!disabled) {
const newBlanks = [...blanks];
newBlanks[index] = value;
setBlanks(newBlanks);
onAnswerChange(question.id, newBlanks);
}
};
const correctAnswers = (question.correctAnswer as string || '').split('|').filter(Boolean);
let blankIndex = 0;
return (
<div className="space-y-4">
<div className="prose max-w-none">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
{question.title}
</h3>
{question.mediaUrl && (
<div className="mb-4">
{question.mediaType === 'image' ? (
<img
src={question.mediaUrl}
alt="Question media"
className="max-w-full h-auto rounded-lg shadow-sm"
/>
) : question.mediaType === 'video' ? (
<video
src={question.mediaUrl}
controls
className="max-w-full h-auto rounded-lg shadow-sm"
/>
) : null}
</div>
)}
</div>
<div className="text-base leading-relaxed">
{parts.map((part, index) => {
if (part === '_____' || part === '[blank]') {
const currentBlankIndex = blankIndex++;
const currentAnswer = blanks[currentBlankIndex] || '';
const correctAnswer = correctAnswers[currentBlankIndex] || '';
const isCorrect = showCorrectAnswer && currentAnswer.toLowerCase().trim() === correctAnswer.toLowerCase().trim();
const isIncorrect = showCorrectAnswer && currentAnswer && !isCorrect;
return (
<span key={index} className="inline-block mx-1">
<input
type="text"
value={currentAnswer}
onChange={(e) => handleBlankChange(currentBlankIndex, e.target.value)}
disabled={disabled}
className={`inline-block text-sm px-3 py-1 border-b-2 bg-transparent focus:outline-none focus:border-blue-500 min-w-24 text-center ${
disabled ? 'cursor-not-allowed opacity-50' : ''
} ${
isCorrect
? 'border-green-500 text-green-700'
: isIncorrect
? 'border-red-500 text-red-700'
: 'border-gray-300'
}`}
placeholder="____"
/>
{showCorrectAnswer && correctAnswer && (
<div className="text-xs text-gray-600 mt-1 text-center">
{isCorrect ? '✓' : `Doğru cevap: ${correctAnswer}`}
</div>
)}
</span>
);
}
return <span key={index}>{part}</span>;
})}
</div>
</div>
);
};

View file

@ -0,0 +1,173 @@
import { Question, StudentAnswer } from '@/types/coordinator';
import React, { useState, useEffect } from 'react';
interface MatchingQuestionProps {
question: Question;
answer?: StudentAnswer;
onAnswerChange: (questionId: string, answer: string[]) => void;
disabled?: boolean;
showCorrectAnswer?: boolean;
}
interface MatchingPair {
id: string;
left: string;
right: string;
}
export const MatchingQuestion: React.FC<MatchingQuestionProps> = ({
question,
answer,
onAnswerChange,
disabled = false,
showCorrectAnswer = false
}) => {
const [matches, setMatches] = useState<Record<string, string>>({});
// Parse options into left and right items
const matchingPairs: MatchingPair[] = question.options?.map(option => ({
id: option.id,
left: option.text.split('|')[0] || option.text,
right: option.text.split('|')[1] || ''
})) || [];
const leftItems = matchingPairs.map(pair => ({ id: pair.id, text: pair.left }));
const rightItems = matchingPairs
.map(pair => ({ id: pair.id, text: pair.right }))
.sort(() => Math.random() - 0.5); // Shuffle right items
useEffect(() => {
if (answer?.answer && Array.isArray(answer.answer)) {
const matchObj: Record<string, string> = {};
answer.answer.forEach((match, index) => {
if (leftItems[index]) {
matchObj[leftItems[index].id] = match;
}
});
setMatches(matchObj);
}
}, [answer?.answer]);
const handleMatch = (leftId: string, rightId: string) => {
if (!disabled) {
const newMatches = { ...matches };
// Remove any existing match for this right item
Object.keys(newMatches).forEach(key => {
if (newMatches[key] === rightId) {
delete newMatches[key];
}
});
// Add new match
newMatches[leftId] = rightId;
setMatches(newMatches);
// Convert to array format
const answerArray = leftItems.map(item => newMatches[item.id] || '');
onAnswerChange(question.id, answerArray);
}
};
const correctMatches = question.correctAnswer as string[] || [];
return (
<div className="space-y-4">
<div className="prose max-w-none">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
{question.title}
</h3>
<p className="text-sm text-gray-600 mb-2">
Sol taraftaki öğeleri sağ taraftaki uygun öğelerle eşleştirin
</p>
{question.content && (
<p className="text-gray-700 mb-4">{question.content}</p>
)}
</div>
<div className="grid grid-cols-2 gap-6">
{/* Left Column */}
<div className="space-y-3">
<h4 className="font-medium text-gray-700 mb-3">Sol Taraf</h4>
{leftItems.map((item, index) => {
const matchedRightId = matches[item.id];
const correctRightId = correctMatches[index];
const isCorrect = showCorrectAnswer && matchedRightId === correctRightId;
const isIncorrect = showCorrectAnswer && matchedRightId && matchedRightId !== correctRightId;
return (
<div
key={item.id}
className={`p-3 rounded-lg border-2 ${
isCorrect
? 'border-green-500 bg-green-50'
: isIncorrect
? 'border-red-500 bg-red-50'
: matchedRightId
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 bg-white'
}`}
>
<div className="flex items-center justify-between">
<span className="text-sm font-medium">{item.text}</span>
{matchedRightId && (
<div className="text-xs text-gray-500">
{rightItems.find(r => r.id === matchedRightId)?.text}
</div>
)}
</div>
{showCorrectAnswer && correctRightId && (
<div className="text-xs mt-1 text-gray-600">
Doğru eşleşme: {rightItems.find(r => r.id === correctRightId)?.text}
</div>
)}
</div>
);
})}
</div>
{/* Right Column */}
<div className="space-y-3">
<h4 className="font-medium text-gray-700 mb-3">Sağ Taraf</h4>
{rightItems.map((item) => {
const isMatched = Object.values(matches).includes(item.id);
return (
<div key={item.id} className="space-y-2">
<div
className={`p-3 rounded-lg border-2 cursor-pointer transition-all ${
disabled ? 'cursor-not-allowed opacity-50' : ''
} ${
isMatched
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 bg-white hover:border-gray-300'
}`}
>
<span className="text-sm font-medium">{item.text}</span>
</div>
{/* Match buttons for each left item */}
<div className="flex flex-wrap gap-1">
{leftItems.map((leftItem) => (
<button
key={leftItem.id}
onClick={() => handleMatch(leftItem.id, item.id)}
disabled={disabled}
className={`px-2 py-1 text-xs rounded transition-all ${
matches[leftItem.id] === item.id
? 'bg-blue-500 text-white'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
} ${disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'}`}
>
{leftItem.text.substring(0, 10)}...
</button>
))}
</div>
</div>
);
})}
</div>
</div>
</div>
);
};

View file

@ -0,0 +1,120 @@
import { Question, StudentAnswer } from '@/types/coordinator';
import React, { useState, useEffect } from 'react';
interface MultipleAnswerQuestionProps {
question: Question;
answer?: StudentAnswer;
onAnswerChange: (questionId: string, answer: string[]) => void;
disabled?: boolean;
showCorrectAnswer?: boolean;
}
export const MultipleAnswerQuestion: React.FC<MultipleAnswerQuestionProps> = ({
question,
answer,
onAnswerChange,
disabled = false,
showCorrectAnswer = false
}) => {
const [selectedOptions, setSelectedOptions] = useState<string[]>([]);
useEffect(() => {
if (answer?.answer && Array.isArray(answer.answer)) {
setSelectedOptions(answer.answer);
}
}, [answer?.answer]);
const handleOptionToggle = (optionId: string) => {
if (!disabled) {
const newSelection = selectedOptions.includes(optionId)
? selectedOptions.filter(id => id !== optionId)
: [...selectedOptions, optionId];
setSelectedOptions(newSelection);
onAnswerChange(question.id, newSelection);
}
};
const correctAnswers = question.correctAnswer as string[] || [];
return (
<div className="space-y-4">
<div className="prose max-w-none">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
{question.title}
</h3>
<p className="text-sm text-gray-600 mb-2">
(Birden fazla seçenek işaretleyebilirsiniz)
</p>
{question.content && (
<p className="text-gray-700 mb-4">{question.content}</p>
)}
{question.mediaUrl && (
<div className="mb-4">
{question.mediaType === 'image' ? (
<img
src={question.mediaUrl}
alt="Question media"
className="max-w-full h-auto rounded-lg shadow-sm"
/>
) : question.mediaType === 'video' ? (
<video
src={question.mediaUrl}
controls
className="max-w-full h-auto rounded-lg shadow-sm"
/>
) : null}
</div>
)}
</div>
<div className="space-y-3">
{question.options?.map((option) => {
const isSelected = selectedOptions.includes(option.id);
const isCorrect = showCorrectAnswer && correctAnswers.includes(option.id);
const isIncorrect = showCorrectAnswer && isSelected && !correctAnswers.includes(option.id);
return (
<div
key={option.id}
className={`relative flex items-center p-4 rounded-lg border-2 cursor-pointer transition-all ${
disabled ? 'cursor-not-allowed opacity-50' : ''
} ${
isSelected
? isCorrect
? 'border-green-500 bg-green-50'
: isIncorrect
? 'border-red-500 bg-red-50'
: 'border-blue-500 bg-blue-50'
: isCorrect && showCorrectAnswer
? 'border-green-500 bg-green-50'
: 'border-gray-200 hover:border-gray-300 bg-white'
}`}
onClick={() => handleOptionToggle(option.id)}
>
<div className="flex items-center">
<input
type="checkbox"
checked={isSelected}
onChange={() => handleOptionToggle(option.id)}
disabled={disabled}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<label className="ml-3 text-sm font-medium text-gray-900 cursor-pointer">
{option.text}
</label>
</div>
{showCorrectAnswer && isCorrect && (
<div className="absolute right-4 text-green-600">
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
</div>
)}
</div>
);
})}
</div>
</div>
);
};

View file

@ -0,0 +1,105 @@
import { Question, StudentAnswer } from '@/types/coordinator';
import React from 'react';
interface MultipleChoiceQuestionProps {
question: Question;
answer?: StudentAnswer;
onAnswerChange: (questionId: string, answer: string) => void;
disabled?: boolean;
showCorrectAnswer?: boolean;
}
export const MultipleChoiceQuestion: React.FC<MultipleChoiceQuestionProps> = ({
question,
answer,
onAnswerChange,
disabled = false,
showCorrectAnswer = false
}) => {
const selectedAnswer = answer?.answer as string || '';
const handleOptionSelect = (optionId: string) => {
if (!disabled) {
onAnswerChange(question.id, optionId);
}
};
return (
<div className="space-y-4">
<div className="prose max-w-none">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
{question.title}
</h3>
{question.content && (
<p className="text-gray-700 mb-4">{question.content}</p>
)}
{question.mediaUrl && (
<div className="mb-4">
{question.mediaType === 'image' ? (
<img
src={question.mediaUrl}
alt="Question media"
className="max-w-full h-auto rounded-lg shadow-sm"
/>
) : question.mediaType === 'video' ? (
<video
src={question.mediaUrl}
controls
className="max-w-full h-auto rounded-lg shadow-sm"
/>
) : null}
</div>
)}
</div>
<div className="space-y-3">
{question.options?.map((option) => {
const isSelected = selectedAnswer === option.id;
const isCorrect = showCorrectAnswer && option.isCorrect;
const isIncorrect = showCorrectAnswer && isSelected && !option.isCorrect;
return (
<div
key={option.id}
className={`relative flex items-center p-4 rounded-lg border-2 cursor-pointer transition-all ${
disabled ? 'cursor-not-allowed opacity-50' : ''
} ${
isSelected
? isCorrect
? 'border-green-500 bg-green-50'
: isIncorrect
? 'border-red-500 bg-red-50'
: 'border-blue-500 bg-blue-50'
: isCorrect && showCorrectAnswer
? 'border-green-500 bg-green-50'
: 'border-gray-200 hover:border-gray-300 bg-white'
}`}
onClick={() => handleOptionSelect(option.id)}
>
<div className="flex items-center">
<input
type="radio"
name={`question-${question.id}`}
checked={isSelected}
onChange={() => handleOptionSelect(option.id)}
disabled={disabled}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300"
/>
<label className="ml-3 text-sm font-medium text-gray-900 cursor-pointer">
{option.text}
</label>
</div>
{showCorrectAnswer && isCorrect && (
<div className="absolute right-4 text-green-600">
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
</div>
)}
</div>
);
})}
</div>
</div>
);
};

View file

@ -0,0 +1,105 @@
import { Question, StudentAnswer } from '@/types/coordinator';
import React, { useState, useEffect } from 'react';
interface OpenEndedQuestionProps {
question: Question;
answer?: StudentAnswer;
onAnswerChange: (questionId: string, answer: string) => void;
disabled?: boolean;
showCorrectAnswer?: boolean;
}
export const OpenEndedQuestion: React.FC<OpenEndedQuestionProps> = ({
question,
answer,
onAnswerChange,
disabled = false,
showCorrectAnswer = false
}) => {
const [text, setText] = useState<string>('');
useEffect(() => {
if (answer?.answer && typeof answer.answer === 'string') {
setText(answer.answer);
}
}, [answer?.answer]);
const handleTextChange = (value: string) => {
if (!disabled) {
setText(value);
onAnswerChange(question.id, value);
}
};
const wordCount = text.trim().split(/\s+/).filter(word => word.length > 0).length;
const charCount = text.length;
return (
<div className="space-y-4">
<div className="prose max-w-none">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
{question.title}
</h3>
{question.content && (
<p className="text-gray-700 mb-4">{question.content}</p>
)}
{question.mediaUrl && (
<div className="mb-4">
{question.mediaType === 'image' ? (
<img
src={question.mediaUrl}
alt="Question media"
className="max-w-full h-auto rounded-lg shadow-sm"
/>
) : question.mediaType === 'video' ? (
<video
src={question.mediaUrl}
controls
className="max-w-full h-auto rounded-lg shadow-sm"
/>
) : null}
</div>
)}
</div>
<div className="space-y-2">
<textarea
value={text}
onChange={(e) => handleTextChange(e.target.value)}
disabled={disabled}
placeholder="Cevabınızı buraya yazın..."
className={`w-full text-sm p-4 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-vertical min-h-32 ${
disabled ? 'bg-gray-50 cursor-not-allowed' : 'bg-white'
}`}
/>
<div className="flex justify-between text-sm text-gray-500">
<span>{wordCount} kelime</span>
<span>{charCount} karakter</span>
</div>
</div>
{showCorrectAnswer && question.correctAnswer && (
<div className="mt-4 p-4 bg-green-50 border border-green-200 rounded-lg">
<h4 className="text-sm font-medium text-green-800 mb-2">
Örnek Cevap:
</h4>
<p className="text-sm text-green-700">
{question.correctAnswer as string}
</p>
</div>
)}
{question.explanation && showCorrectAnswer && (
<div className="mt-4 p-4 bg-blue-50 border border-blue-200 rounded-lg">
<h4 className="text-sm font-medium text-blue-800 mb-2">
ıklama:
</h4>
<p className="text-sm text-blue-700">
{question.explanation}
</p>
</div>
)}
</div>
);
};

View file

@ -0,0 +1,191 @@
import { Question, StudentAnswer } from '@/types/coordinator';
import React, { useState, useEffect } from 'react';
interface OrderingQuestionProps {
question: Question;
answer?: StudentAnswer;
onAnswerChange: (questionId: string, answer: string[]) => void;
disabled?: boolean;
showCorrectAnswer?: boolean;
}
interface OrderItem {
id: string;
text: string;
originalOrder: number;
}
export const OrderingQuestion: React.FC<OrderingQuestionProps> = ({
question,
answer,
onAnswerChange,
disabled = false,
showCorrectAnswer = false
}) => {
const [orderedItems, setOrderedItems] = useState<OrderItem[]>([]);
useEffect(() => {
if (question.options) {
const items: OrderItem[] = question.options.map((option, index) => ({
id: option.id,
text: option.text,
originalOrder: option.order || index
}));
if (answer?.answer && Array.isArray(answer.answer)) {
// Restore order from saved answer
const orderedIds = answer.answer;
const restored = orderedIds
.map(id => items.find(item => item.id === id))
.filter(Boolean) as OrderItem[];
setOrderedItems(restored);
} else {
// Shuffle items initially
const shuffled = [...items].sort(() => Math.random() - 0.5);
setOrderedItems(shuffled);
}
}
}, [question.options, answer?.answer]);
const handleDragStart = (e: React.DragEvent, index: number) => {
if (!disabled) {
e.dataTransfer.setData('text/plain', index.toString());
}
};
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
};
const handleDrop = (e: React.DragEvent, dropIndex: number) => {
if (!disabled) {
e.preventDefault();
const dragIndex = parseInt(e.dataTransfer.getData('text/plain'));
if (dragIndex !== dropIndex) {
const newItems = [...orderedItems];
const draggedItem = newItems[dragIndex];
newItems.splice(dragIndex, 1);
newItems.splice(dropIndex, 0, draggedItem);
setOrderedItems(newItems);
onAnswerChange(question.id, newItems.map(item => item.id));
}
}
};
const moveItem = (fromIndex: number, direction: 'up' | 'down') => {
if (disabled) return;
const toIndex = direction === 'up' ? fromIndex - 1 : fromIndex + 1;
if (toIndex < 0 || toIndex >= orderedItems.length) return;
const newItems = [...orderedItems];
[newItems[fromIndex], newItems[toIndex]] = [newItems[toIndex], newItems[fromIndex]];
setOrderedItems(newItems);
onAnswerChange(question.id, newItems.map(item => item.id));
};
const correctOrder = question.correctAnswer as string[] || [];
return (
<div className="space-y-4">
<div className="prose max-w-none">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
{question.title}
</h3>
<p className="text-sm text-gray-600 mb-2">
Aşağıdaki öğeleri doğru sıraya göre düzenleyin
</p>
{question.content && (
<p className="text-gray-700 mb-4">{question.content}</p>
)}
</div>
<div className="space-y-2">
{orderedItems.map((item, index) => {
const correctIndex = correctOrder.indexOf(item.id);
const isInCorrectPosition = showCorrectAnswer && correctIndex === index;
const isInWrongPosition = showCorrectAnswer && correctIndex !== -1 && correctIndex !== index;
return (
<div
key={item.id}
draggable={!disabled}
onDragStart={(e) => handleDragStart(e, index)}
onDragOver={handleDragOver}
onDrop={(e) => handleDrop(e, index)}
className={`flex items-center p-4 rounded-lg border-2 transition-all ${
disabled ? 'cursor-not-allowed opacity-50' : 'cursor-move'
} ${
isInCorrectPosition
? 'border-green-500 bg-green-50'
: isInWrongPosition
? 'border-red-500 bg-red-50'
: 'border-gray-200 bg-white hover:border-gray-300'
}`}
>
<div className="flex items-center space-x-3 flex-1">
<div className="flex flex-col space-y-1">
<svg className="w-4 h-4 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z" />
</svg>
<svg className="w-4 h-4 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z" />
</svg>
</div>
<div className="flex items-center space-x-3 flex-1">
<div className="bg-blue-100 text-blue-800 text-sm font-medium px-2 py-1 rounded">
{index + 1}
</div>
<span className="text-sm font-medium">{item.text}</span>
</div>
</div>
{!disabled && (
<div className="flex flex-col space-y-1">
<button
onClick={() => moveItem(index, 'up')}
disabled={index === 0}
className="p-1 text-gray-400 hover:text-gray-600 disabled:opacity-30"
>
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M14.707 12.707a1 1 0 01-1.414 0L10 9.414l-3.293 3.293a1 1 0 01-1.414-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 010 1.414z" clipRule="evenodd" />
</svg>
</button>
<button
onClick={() => moveItem(index, 'down')}
disabled={index === orderedItems.length - 1}
className="p-1 text-gray-400 hover:text-gray-600 disabled:opacity-30"
>
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
</svg>
</button>
</div>
)}
{showCorrectAnswer && (
<div className="ml-4">
{isInCorrectPosition ? (
<div className="text-green-600">
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
</div>
) : correctIndex !== -1 ? (
<div className="text-red-600 text-xs">
Doğru sıra: {correctIndex + 1}
</div>
) : null}
</div>
)}
</div>
);
})}
</div>
</div>
);
};

View file

@ -0,0 +1,101 @@
import { Question, StudentAnswer } from '@/types/coordinator';
import React from 'react';
interface TrueFalseQuestionProps {
question: Question;
answer?: StudentAnswer;
onAnswerChange: (questionId: string, answer: string) => void;
disabled?: boolean;
showCorrectAnswer?: boolean;
}
export const TrueFalseQuestion: React.FC<TrueFalseQuestionProps> = ({
question,
answer,
onAnswerChange,
disabled = false,
showCorrectAnswer = false
}) => {
const selectedAnswer = answer?.answer as string || '';
const correctAnswer = question.correctAnswer as string;
const handleAnswerSelect = (value: string) => {
if (!disabled) {
onAnswerChange(question.id, value);
}
};
const options = [
{ value: 'true', label: 'Doğru' },
{ value: 'false', label: 'Yanlış' }
];
return (
<div className="space-y-4">
<div className="prose max-w-none">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
{question.title}
</h3>
{question.content && (
<p className="text-gray-700 mb-4">{question.content}</p>
)}
{question.mediaUrl && (
<div className="mb-4">
{question.mediaType === 'image' ? (
<img
src={question.mediaUrl}
alt="Question media"
className="max-w-full h-auto rounded-lg shadow-sm"
/>
) : question.mediaType === 'video' ? (
<video
src={question.mediaUrl}
controls
className="max-w-full h-auto rounded-lg shadow-sm"
/>
) : null}
</div>
)}
</div>
<div className="flex space-x-4">
{options.map((option) => {
const isSelected = selectedAnswer === option.value;
const isCorrect = showCorrectAnswer && correctAnswer === option.value;
const isIncorrect = showCorrectAnswer && isSelected && correctAnswer !== option.value;
return (
<button
key={option.value}
onClick={() => handleAnswerSelect(option.value)}
disabled={disabled}
className={`flex-1 flex items-center justify-center p-6 rounded-lg border-2 font-medium transition-all ${
disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer hover:shadow-md'
} ${
isSelected
? isCorrect
? 'border-green-500 bg-green-50 text-green-700'
: isIncorrect
? 'border-red-500 bg-red-50 text-red-700'
: 'border-blue-500 bg-blue-50 text-blue-700'
: isCorrect && showCorrectAnswer
? 'border-green-500 bg-green-50 text-green-700'
: 'border-gray-200 bg-white text-gray-700 hover:border-gray-300'
}`}
>
<div className="text-center">
<div className="text-2xl mb-2">
{option.value === 'true' ? '✓' : '✗'}
</div>
<div>{option.label}</div>
{showCorrectAnswer && isCorrect && (
<div className="text-sm mt-2 text-green-600">Doğru Cevap</div>
)}
</div>
</button>
);
})}
</div>
</div>
);
};

View file

@ -0,0 +1,223 @@
import { generateMockTags } from '@/mocks/mockTags'
import React, { useState } from 'react'
import { FaPlus, FaSearch, FaEdit, FaTrash, FaTag } from 'react-icons/fa'
interface TagItem {
id: string
name: string
description: string
color: string
usageCount: number
creationTime: Date
}
const Tags: React.FC = () => {
const [tags, setTags] = useState<TagItem[]>(generateMockTags())
const [searchTerm, setSearchTerm] = useState('')
const [isCreating, setIsCreating] = useState(false)
const [editingTag, setEditingTag] = useState<TagItem | null>(null)
const [formData, setFormData] = useState({
name: '',
description: '',
color: '#3B82F6',
})
const filteredTags = tags.filter(
(tag) =>
tag.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
tag.description.toLowerCase().includes(searchTerm.toLowerCase()),
)
const handleSubmit = () => {
if (!formData.name.trim()) return
if (editingTag) {
setTags((prev) =>
prev.map((tag) => (tag.id === editingTag.id ? { ...editingTag, ...formData } : tag)),
)
setEditingTag(null)
} else {
const newTag: TagItem = {
...formData,
id: `tag-${Date.now()}`,
usageCount: 0,
creationTime: new Date(),
}
setTags((prev) => [...prev, newTag])
setIsCreating(false)
}
setFormData({ name: '', description: '', color: '#3B82F6' })
}
const handleEdit = (tag: TagItem) => {
setEditingTag(tag)
setFormData({
name: tag.name,
description: tag.description,
color: tag.color,
})
setIsCreating(true)
}
const handleCancel = () => {
setIsCreating(false)
setEditingTag(null)
setFormData({ name: '', description: '', color: '#3B82F6' })
}
return (
<div className="space-y-4">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-xl font-bold text-gray-900">Etiket Yönetimi</h1>
<p className="text-sm text-gray-600 mt-0.5">Soru ve içerik etiketlerini yönetin</p>
</div>
<button
onClick={() => setIsCreating(true)}
className="flex items-center space-x-2 bg-blue-600 hover:bg-blue-700 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-colors"
>
<FaPlus className="w-3.5 h-3.5" />
<span>Yeni Etiket</span>
</button>
</div>
{/* Search */}
<div className="bg-white border border-gray-200 rounded-lg p-3">
<div className="relative">
<FaSearch className="absolute left-3 top-2 h-4 w-4 text-gray-400" />
<input
type="text"
placeholder="Etiket ara..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-8 text-sm w-full border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
{/* Create/Edit Form */}
{(isCreating || editingTag) && (
<div className="bg-white border border-gray-200 rounded-lg p-4">
<h3 className="text-base font-semibold mb-3">
{editingTag ? 'Etiket Düzenle' : 'Yeni Etiket Oluştur'}
</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 mb-3">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Etiket Adı</label>
<input
type="text"
value={formData.name}
onChange={(e) => setFormData((prev) => ({ ...prev, name: e.target.value }))}
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Etiket adını girin"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">Renk</label>
<input
type="color"
value={formData.color}
onChange={(e) => setFormData((prev) => ({ ...prev, color: e.target.value }))}
className="w-full text-sm h-9 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">ıklama</label>
<input
type="text"
value={formData.description}
onChange={(e) =>
setFormData((prev) => ({
...prev,
description: e.target.value,
}))
}
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Açıklama girin"
/>
</div>
</div>
<div className="flex space-x-2">
<button
onClick={handleCancel}
className="px-3 py-1.5 border border-gray-300 rounded-lg text-sm text-gray-700 hover:bg-gray-50 font-medium transition-colors"
>
İptal
</button>
<button
onClick={handleSubmit}
className="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white text-sm rounded-lg font-medium transition-colors"
>
{editingTag ? 'Güncelle' : 'Oluştur'}
</button>
</div>
</div>
)}
{/* Tags List */}
<div className="bg-white border border-gray-200 rounded-lg">
{filteredTags.length > 0 ? (
<div className="divide-y divide-gray-200">
{filteredTags.map((tag) => (
<div key={tag.id} className="p-3 hover:bg-gray-50">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2.5">
<div
className="w-3.5 h-3.5 rounded-full"
style={{ backgroundColor: tag.color }}
/>
<div>
<h3 className="text-sm font-medium text-gray-900">{tag.name}</h3>
<p className="text-xs text-gray-600">{tag.description}</p>
</div>
</div>
<div className="flex items-center space-x-3">
<span className="text-xs text-gray-500">{tag.usageCount} kullanım</span>
<div className="flex space-x-1">
<button
onClick={() => handleEdit(tag)}
className="p-1.5 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
>
<FaEdit className="w-3.5 h-3.5" />
</button>
<button
onClick={() => {
if (window.confirm('Bu etiketi silmek istediğinizden emin misiniz?')) {
setTags((prev) => prev.filter((t) => t.id !== tag.id))
}
}}
className="p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors"
>
<FaTrash className="w-3.5 h-3.5" />
</button>
</div>
</div>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-8">
<FaTag className="w-10 h-10 text-gray-400 mx-auto mb-3" />
<h3 className="text-base font-medium text-gray-900 mb-1">Etiket bulunamadı</h3>
<p className="text-sm text-gray-600">
{searchTerm
? 'Arama kriterlerinizi değiştirin veya yeni etiket oluşturun.'
: 'İlk etiketinizi oluşturun.'}
</p>
</div>
)}
</div>
</div>
)
}
export default Tags

View file

@ -0,0 +1,237 @@
import React, { useState } from 'react'
import { TestCreator } from './AdminPanel/TestCreator'
import {
FaPlus,
FaSearch,
FaFilter,
FaEdit,
FaTrash,
FaFileAlt,
FaImage,
FaCalendar,
FaUsers,
FaPlay,
} from 'react-icons/fa'
import { useNavigate } from 'react-router-dom'
import { Exam } from '@/types/coordinator'
import { generateMockPDFTest } from '@/mocks/mockTests'
const Tests: React.FC = () => {
const navigate = useNavigate()
const [tests, setTests] = useState<Exam[]>([generateMockPDFTest()])
const [searchTerm, setSearchTerm] = useState('')
const [statusFilter, setStatusFilter] = useState('')
const [isCreating, setIsCreating] = useState(false)
const [editingTest, setEditingTest] = useState<Exam | null>(null)
const filteredTests = tests.filter((test) => {
const matchesSearch =
test.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
test.description.toLowerCase().includes(searchTerm.toLowerCase())
const matchesStatus =
!statusFilter ||
(statusFilter === 'active' && test.isActive) ||
(statusFilter === 'inactive' && !test.isActive)
return matchesSearch && matchesStatus && test.type === 'test'
})
const handleCreateTest = (
testData: Omit<Exam, 'id' | 'creationTime' | 'lastModificationTime'>,
) => {
const newTest: Exam = {
...testData,
id: `test-${Date.now()}`,
creationTime: new Date(),
lastModificationTime: new Date(),
}
setTests((prev) => [...prev, newTest])
setIsCreating(false)
}
const handleUpdateTest = (
testData: Omit<Exam, 'id' | 'creationTime' | 'lastModificationTime'>,
) => {
if (editingTest) {
const updatedTest: Exam = {
...testData,
id: editingTest.id,
creationTime: editingTest.creationTime,
lastModificationTime: new Date(),
}
setTests((prev) => prev.map((test) => (test.id === updatedTest.id ? updatedTest : test)))
setEditingTest(null)
}
}
const handleEdit = (test: Exam) => {
setEditingTest(test)
setIsCreating(true)
}
const handleCancel = () => {
setIsCreating(false)
setEditingTest(null)
}
if (isCreating) {
return (
<TestCreator
onCreateTest={editingTest ? handleUpdateTest : handleCreateTest}
onCancel={handleCancel}
editingTest={editingTest || undefined}
/>
)
}
return (
<div className="space-y-4">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-xl font-bold text-gray-900">Test Yönetimi</h1>
<p className="text-sm text-gray-600 mt-0.5">PDF testler ve cevap anahtarlarını yönetin</p>
</div>
<button
onClick={() => setIsCreating(true)}
className="flex items-center space-x-2 bg-blue-600 hover:bg-blue-700 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-colors"
>
<FaPlus className="w-3.5 h-3.5" />
<span>Yeni Test</span>
</button>
</div>
{/* Filters */}
<div className="bg-white border border-gray-200 rounded-lg p-3">
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<div className="relative">
<FaSearch className="absolute left-3 top-2 h-4 w-4 text-gray-400" />
<input
type="text"
placeholder="Test ara..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-8 text-sm w-full border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="relative">
<FaFilter className="absolute left-3 top-2 h-4 w-4 text-gray-400" />
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="pl-8 text-sm w-full border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500 appearance-none"
>
<option value="">Tüm durumlar</option>
<option value="active">Aktif</option>
<option value="inactive">Pasif</option>
</select>
</div>
<div className="text-sm text-gray-600 flex items-center">
Toplam: {filteredTests.length} test
</div>
</div>
</div>
{/* Tests List */}
<div className="space-y-3">
{filteredTests.length > 0 ? (
filteredTests.map((test) => (
<div
key={test.id}
className="bg-white border border-gray-200 rounded-lg p-4 hover:shadow-md transition-shadow"
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center space-x-2 mb-1.5">
<h3 className="text-base font-semibold text-gray-900">{test.title}</h3>
<span
className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${
test.isActive ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'
}`}
>
{test.isActive ? 'Aktif' : 'Pasif'}
</span>
</div>
<p className="text-sm text-gray-600 mb-2">{test.description}</p>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 text-sm">
<div className="flex items-center space-x-2">
{test.testDocument?.type === 'pdf' ? (
<FaFileAlt className="w-4 h-4 text-red-600" />
) : (
<FaImage className="w-4 h-4 text-blue-600" />
)}
<span className="text-gray-600">
{test.testDocument?.type === 'pdf' ? 'PDF' : 'Resim'}
<span>: {test.testDocument?.name}</span>
</span>
</div>
<div className="flex items-center space-x-2">
<FaCalendar className="w-4 h-4 text-gray-400" />
<span className="text-gray-600">{test.timeLimit} dakika</span>
</div>
<div className="flex items-center space-x-2">
<FaUsers className="w-4 h-4 text-gray-400" />
<span className="text-gray-600">
{test.answerKeyTemplate?.length || 0} soru
</span>
</div>
<div className="flex items-center space-x-2">
<span className="text-gray-600">Toplam: {test.totalPoints} puan</span>
</div>
</div>
</div>
<div className="flex items-center space-x-1.5 ml-4">
<button
onClick={() => navigate(`/test/${test.id}`)}
className="flex items-center space-x-1 px-2.5 py-1.5 bg-green-600 hover:bg-green-700 text-white text-xs rounded-lg font-medium transition-colors"
>
<FaPlay className="w-3 h-3" />
<span>Başlat</span>
</button>
<button
onClick={() => handleEdit(test)}
className="p-1.5 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
title="Düzenle"
>
<FaEdit className="w-3.5 h-3.5" />
</button>
<button
onClick={() => {
if (window.confirm('Bu testi silmek istediğinizden emin misiniz?')) {
setTests((prev) => prev.filter((t) => t.id !== test.id))
}
}}
className="p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors"
title="Sil"
>
<FaTrash className="w-3.5 h-3.5" />
</button>
</div>
</div>
</div>
))
) : (
<div className="text-center py-8">
<FaFileAlt className="w-10 h-10 text-gray-400 mx-auto mb-3" />
<h3 className="text-base font-medium text-gray-900 mb-1">Test bulunamadı</h3>
<p className="text-sm text-gray-600">
{searchTerm || statusFilter
? 'Arama kriterlerinizi değiştirin veya yeni test oluşturun.'
: 'İlk testinizi oluşturun.'}
</p>
</div>
)}
</div>
</div>
)
}
export default Tests