350 lines
11 KiB
C#
350 lines
11 KiB
C#
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using DramaLing.Api.Data;
|
|
using DramaLing.Api.Models.Entities;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using System.Security.Claims;
|
|
|
|
namespace DramaLing.Api.Controllers;
|
|
|
|
[ApiController]
|
|
[Route("api/[controller]")]
|
|
[Authorize]
|
|
public class FlashcardsController : ControllerBase
|
|
{
|
|
private readonly DramaLingDbContext _context;
|
|
|
|
public FlashcardsController(DramaLingDbContext context)
|
|
{
|
|
_context = context;
|
|
}
|
|
|
|
private Guid GetUserId()
|
|
{
|
|
var userIdString = User.FindFirst(ClaimTypes.NameIdentifier)?.Value ??
|
|
User.FindFirst("sub")?.Value;
|
|
|
|
if (Guid.TryParse(userIdString, out var userId))
|
|
return userId;
|
|
|
|
throw new UnauthorizedAccessException("Invalid user ID");
|
|
}
|
|
|
|
private async Task<Guid> GetOrCreateDefaultCardSetAsync(Guid userId)
|
|
{
|
|
// 嘗試找到預設卡組
|
|
var defaultCardSet = await _context.CardSets
|
|
.FirstOrDefaultAsync(cs => cs.UserId == userId && cs.IsDefault);
|
|
|
|
if (defaultCardSet != null)
|
|
return defaultCardSet.Id;
|
|
|
|
// 如果沒有預設卡組,創建一個
|
|
var newDefaultCardSet = new CardSet
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
UserId = userId,
|
|
Name = "未分類",
|
|
Description = "系統預設卡組,用於存放尚未分類的詞卡",
|
|
Color = "bg-slate-700",
|
|
IsDefault = true
|
|
};
|
|
|
|
_context.CardSets.Add(newDefaultCardSet);
|
|
await _context.SaveChangesAsync();
|
|
return newDefaultCardSet.Id;
|
|
}
|
|
|
|
[HttpGet]
|
|
public async Task<ActionResult> GetFlashcards(
|
|
[FromQuery] Guid? setId,
|
|
[FromQuery] string? search,
|
|
[FromQuery] bool favoritesOnly = false,
|
|
[FromQuery] int limit = 50,
|
|
[FromQuery] int offset = 0)
|
|
{
|
|
try
|
|
{
|
|
var userId = GetUserId();
|
|
|
|
var query = _context.Flashcards
|
|
.Include(f => f.CardSet)
|
|
.Where(f => f.UserId == userId);
|
|
|
|
if (setId.HasValue)
|
|
query = query.Where(f => f.CardSetId == setId);
|
|
|
|
if (!string.IsNullOrEmpty(search))
|
|
query = query.Where(f => f.Word.Contains(search) || f.Translation.Contains(search));
|
|
|
|
if (favoritesOnly)
|
|
query = query.Where(f => f.IsFavorite);
|
|
|
|
var total = await query.CountAsync();
|
|
var flashcards = await query
|
|
.OrderByDescending(f => f.CreatedAt)
|
|
.Skip(offset)
|
|
.Take(Math.Min(limit, 100))
|
|
.Select(f => new
|
|
{
|
|
f.Id,
|
|
f.Word,
|
|
f.Translation,
|
|
f.Definition,
|
|
f.PartOfSpeech,
|
|
f.Pronunciation,
|
|
f.Example,
|
|
f.ExampleTranslation,
|
|
f.MasteryLevel,
|
|
f.TimesReviewed,
|
|
f.IsFavorite,
|
|
f.NextReviewDate,
|
|
f.CreatedAt,
|
|
CardSet = new
|
|
{
|
|
f.CardSet.Name,
|
|
f.CardSet.Color
|
|
}
|
|
})
|
|
.ToListAsync();
|
|
|
|
return Ok(new
|
|
{
|
|
Success = true,
|
|
Data = new
|
|
{
|
|
Flashcards = flashcards,
|
|
Total = total,
|
|
HasMore = offset + limit < total
|
|
}
|
|
});
|
|
}
|
|
catch (UnauthorizedAccessException)
|
|
{
|
|
return Unauthorized(new { Success = false, Error = "Unauthorized" });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return StatusCode(500, new
|
|
{
|
|
Success = false,
|
|
Error = "Failed to fetch flashcards",
|
|
Timestamp = DateTime.UtcNow
|
|
});
|
|
}
|
|
}
|
|
|
|
[HttpPost]
|
|
public async Task<ActionResult> CreateFlashcard([FromBody] CreateFlashcardRequest request)
|
|
{
|
|
try
|
|
{
|
|
var userId = GetUserId();
|
|
|
|
// 確定要使用的卡組ID
|
|
Guid cardSetId;
|
|
if (request.CardSetId.HasValue)
|
|
{
|
|
// 如果指定了卡組,驗證是否屬於用戶
|
|
var cardSet = await _context.CardSets
|
|
.FirstOrDefaultAsync(cs => cs.Id == request.CardSetId.Value && cs.UserId == userId);
|
|
|
|
if (cardSet == null)
|
|
return NotFound(new { Success = false, Error = "Card set not found" });
|
|
|
|
cardSetId = request.CardSetId.Value;
|
|
}
|
|
else
|
|
{
|
|
// 如果沒有指定卡組,使用或創建預設卡組
|
|
cardSetId = await GetOrCreateDefaultCardSetAsync(userId);
|
|
}
|
|
|
|
var flashcard = new Flashcard
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
UserId = userId,
|
|
CardSetId = cardSetId,
|
|
Word = request.Word.Trim(),
|
|
Translation = request.Translation.Trim(),
|
|
Definition = request.Definition.Trim(),
|
|
PartOfSpeech = request.PartOfSpeech?.Trim(),
|
|
Pronunciation = request.Pronunciation?.Trim(),
|
|
Example = request.Example?.Trim(),
|
|
ExampleTranslation = request.ExampleTranslation?.Trim()
|
|
};
|
|
|
|
_context.Flashcards.Add(flashcard);
|
|
await _context.SaveChangesAsync();
|
|
|
|
return Ok(new
|
|
{
|
|
Success = true,
|
|
Data = flashcard,
|
|
Message = "Flashcard created successfully"
|
|
});
|
|
}
|
|
catch (UnauthorizedAccessException)
|
|
{
|
|
return Unauthorized(new { Success = false, Error = "Unauthorized" });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return StatusCode(500, new
|
|
{
|
|
Success = false,
|
|
Error = "Failed to create flashcard",
|
|
Timestamp = DateTime.UtcNow
|
|
});
|
|
}
|
|
}
|
|
|
|
[HttpGet("{id}")]
|
|
public async Task<ActionResult> GetFlashcard(Guid id)
|
|
{
|
|
try
|
|
{
|
|
var userId = GetUserId();
|
|
|
|
var flashcard = await _context.Flashcards
|
|
.Include(f => f.CardSet)
|
|
.FirstOrDefaultAsync(f => f.Id == id && f.UserId == userId);
|
|
|
|
if (flashcard == null)
|
|
return NotFound(new { Success = false, Error = "Flashcard not found" });
|
|
|
|
return Ok(new { Success = true, Data = flashcard });
|
|
}
|
|
catch (UnauthorizedAccessException)
|
|
{
|
|
return Unauthorized(new { Success = false, Error = "Unauthorized" });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return StatusCode(500, new
|
|
{
|
|
Success = false,
|
|
Error = "Failed to fetch flashcard",
|
|
Timestamp = DateTime.UtcNow
|
|
});
|
|
}
|
|
}
|
|
|
|
[HttpPut("{id}")]
|
|
public async Task<ActionResult> UpdateFlashcard(Guid id, [FromBody] UpdateFlashcardRequest request)
|
|
{
|
|
try
|
|
{
|
|
var userId = GetUserId();
|
|
|
|
var flashcard = await _context.Flashcards
|
|
.FirstOrDefaultAsync(f => f.Id == id && f.UserId == userId);
|
|
|
|
if (flashcard == null)
|
|
return NotFound(new { Success = false, Error = "Flashcard not found" });
|
|
|
|
// 更新欄位
|
|
if (!string.IsNullOrEmpty(request.Word))
|
|
flashcard.Word = request.Word.Trim();
|
|
if (!string.IsNullOrEmpty(request.Translation))
|
|
flashcard.Translation = request.Translation.Trim();
|
|
if (!string.IsNullOrEmpty(request.Definition))
|
|
flashcard.Definition = request.Definition.Trim();
|
|
if (request.PartOfSpeech != null)
|
|
flashcard.PartOfSpeech = request.PartOfSpeech?.Trim();
|
|
if (request.Pronunciation != null)
|
|
flashcard.Pronunciation = request.Pronunciation?.Trim();
|
|
if (request.Example != null)
|
|
flashcard.Example = request.Example?.Trim();
|
|
if (request.ExampleTranslation != null)
|
|
flashcard.ExampleTranslation = request.ExampleTranslation?.Trim();
|
|
if (request.IsFavorite.HasValue)
|
|
flashcard.IsFavorite = request.IsFavorite.Value;
|
|
|
|
flashcard.UpdatedAt = DateTime.UtcNow;
|
|
await _context.SaveChangesAsync();
|
|
|
|
return Ok(new
|
|
{
|
|
Success = true,
|
|
Data = flashcard,
|
|
Message = "Flashcard updated successfully"
|
|
});
|
|
}
|
|
catch (UnauthorizedAccessException)
|
|
{
|
|
return Unauthorized(new { Success = false, Error = "Unauthorized" });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return StatusCode(500, new
|
|
{
|
|
Success = false,
|
|
Error = "Failed to update flashcard",
|
|
Timestamp = DateTime.UtcNow
|
|
});
|
|
}
|
|
}
|
|
|
|
[HttpDelete("{id}")]
|
|
public async Task<ActionResult> DeleteFlashcard(Guid id)
|
|
{
|
|
try
|
|
{
|
|
var userId = GetUserId();
|
|
|
|
var flashcard = await _context.Flashcards
|
|
.FirstOrDefaultAsync(f => f.Id == id && f.UserId == userId);
|
|
|
|
if (flashcard == null)
|
|
return NotFound(new { Success = false, Error = "Flashcard not found" });
|
|
|
|
_context.Flashcards.Remove(flashcard);
|
|
await _context.SaveChangesAsync();
|
|
|
|
return Ok(new
|
|
{
|
|
Success = true,
|
|
Message = "Flashcard deleted successfully"
|
|
});
|
|
}
|
|
catch (UnauthorizedAccessException)
|
|
{
|
|
return Unauthorized(new { Success = false, Error = "Unauthorized" });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return StatusCode(500, new
|
|
{
|
|
Success = false,
|
|
Error = "Failed to delete flashcard",
|
|
Timestamp = DateTime.UtcNow
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// DTOs
|
|
public class CreateFlashcardRequest
|
|
{
|
|
public Guid? CardSetId { get; set; }
|
|
public string Word { get; set; } = string.Empty;
|
|
public string Translation { get; set; } = string.Empty;
|
|
public string Definition { get; set; } = string.Empty;
|
|
public string? PartOfSpeech { get; set; }
|
|
public string? Pronunciation { get; set; }
|
|
public string? Example { get; set; }
|
|
public string? ExampleTranslation { get; set; }
|
|
}
|
|
|
|
public class UpdateFlashcardRequest
|
|
{
|
|
public string? Word { get; set; }
|
|
public string? Translation { get; set; }
|
|
public string? Definition { get; set; }
|
|
public string? PartOfSpeech { get; set; }
|
|
public string? Pronunciation { get; set; }
|
|
public string? Example { get; set; }
|
|
public string? ExampleTranslation { get; set; }
|
|
public bool? IsFavorite { get; set; }
|
|
} |