Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 162 additions & 0 deletions app/app/api/cards/[cardId]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { NextRequest, NextResponse } from "next/server";
import { getCardStorage } from "@/lib/services/cardStorage";
import { createBaseCard, validateCardInput } from "@/lib/services/cardTemplating";

/**
* GET /api/cards/[cardId]
* Retrieves a specific card by ID.
*/
export async function GET(
request: NextRequest,
{ params }: { params: { cardId: string } }
) {
try {
const userId = request.headers.get("x-user-id") ?? "demo-user";

if (!userId) {
return NextResponse.json(
{ error: "Unauthorized" },
{ status: 401 }
);
}

const storage = getCardStorage();
const card = await storage.getCard(userId, params.cardId);

if (!card) {
return NextResponse.json(
{ error: "Card not found" },
{ status: 404 }
);
}

return NextResponse.json(
{
success: true,
card
},
{ status: 200 }
);

} catch (error) {
console.error("Error fetching card:", error);
return NextResponse.json(
{
error: "Failed to fetch card",
details: error instanceof Error ? error.message : "Unknown error"
},
{ status: 500 }
);
}
}

/**
* PUT /api/cards/[cardId]
* Updates an existing card (creates a new version).
*/
export async function PUT(
request: NextRequest,
{ params }: { params: { cardId: string } }
) {
try {
const userId = request.headers.get("x-user-id") ?? "demo-user";

if (!userId) {
return NextResponse.json(
{ error: "Unauthorized" },
{ status: 401 }
);
}

const storage = getCardStorage();
const existingCard = await storage.getCard(userId, params.cardId);

if (!existingCard) {
return NextResponse.json(
{ error: "Card not found" },
{ status: 404 }
);
}

const body = await request.json();

// Validate input
try {
validateCardInput(body);
} catch (validationError) {
return NextResponse.json(
{
error: "Invalid input",
details: validationError instanceof Error ? validationError.message : "Unknown error"
},
{ status: 400 }
);
}

// Create new version of the card with the same ID
const updatedCard = createBaseCard(body);
updatedCard.cardId = params.cardId; // Preserve the original card ID

await storage.updateCard(userId, updatedCard);

return NextResponse.json(
{
success: true,
card: updatedCard,
message: "Card updated successfully"
},
{ status: 200 }
);

} catch (error) {
console.error("Error updating card:", error);
return NextResponse.json(
{
error: "Failed to update card",
details: error instanceof Error ? error.message : "Unknown error"
},
{ status: 500 }
);
}
}

/**
* DELETE /api/cards/[cardId]
* Deletes a card from the user's profile.
*/
export async function DELETE(
request: NextRequest,
{ params }: { params: { cardId: string } }
) {
try {
const userId = request.headers.get("x-user-id") ?? "demo-user";

if (!userId) {
return NextResponse.json(
{ error: "Unauthorized" },
{ status: 401 }
);
}

const storage = getCardStorage();
await storage.deleteCard(userId, params.cardId);

return NextResponse.json(
{
success: true,
message: "Card deleted successfully"
},
{ status: 200 }
);

} catch (error) {
console.error("Error deleting card:", error);
return NextResponse.json(
{
error: "Failed to delete card",
details: error instanceof Error ? error.message : "Unknown error"
},
{ status: 500 }
);
}
}
105 changes: 105 additions & 0 deletions app/app/api/cards/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { NextRequest, NextResponse } from "next/server";
import { createBaseCard, validateCardInput } from "@/lib/services/cardTemplating";
import { getCardStorage } from "@/lib/services/cardStorage";

/**
* POST /api/cards
* Creates a new card and saves it to the user's profile.
*
* Request body should match BaseCardInput interface.
* Returns the created card with its generated ID.
*/
export async function POST(request: NextRequest) {
try {
// TODO: Replace with actual auth - get userId from session
const userId = request.headers.get("x-user-id") ?? "demo-user";

if (!userId) {
return NextResponse.json(
{ error: "Unauthorized" },
{ status: 401 }
);
}

const body = await request.json();

// Validate input
try {
validateCardInput(body);
} catch (validationError) {
return NextResponse.json(
{
error: "Invalid input",
details: validationError instanceof Error ? validationError.message : "Unknown error"
},
{ status: 400 }
);
}

// Create the card
const card = createBaseCard(body);

// Save to storage
const storage = getCardStorage();
await storage.saveCard(userId, card);

return NextResponse.json(
{
success: true,
card,
message: "Card created successfully"
},
{ status: 201 }
);

} catch (error) {
console.error("Error creating card:", error);
return NextResponse.json(
{
error: "Failed to create card",
details: error instanceof Error ? error.message : "Unknown error"
},
{ status: 500 }
);
}
}

/**
* GET /api/cards
* Retrieves all cards for the authenticated user.
*/
export async function GET(request: NextRequest) {
try {
// TODO: Replace with actual auth
const userId = request.headers.get("x-user-id") ?? "demo-user";

if (!userId) {
return NextResponse.json(
{ error: "Unauthorized" },
{ status: 401 }
);
}

const storage = getCardStorage();
const cards = await storage.getUserCards(userId);

return NextResponse.json(
{
success: true,
cards,
count: cards.length
},
{ status: 200 }
);

} catch (error) {
console.error("Error fetching cards:", error);
return NextResponse.json(
{
error: "Failed to fetch cards",
details: error instanceof Error ? error.message : "Unknown error"
},
{ status: 500 }
);
}
}
76 changes: 76 additions & 0 deletions app/lib/services/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Card Templating & Storage Services

This directory contains the core services for creating and persisting language learning cards.

## Quick Start

```typescript
import { createBaseCard } from "./cardTemplating";
import { getCardStorage } from "./cardStorage";

// 1. Create a card from AI-generated data
const card = createBaseCard({
language: "Spanish",
lemma: "hablar",
partOfSpeech: "verb",
definitions: [
{ definition: "to speak, to talk" }
],
examples: [
{ sentence: "Ella habla tres idiomas." }
],
ipa: "aˈβlaɾ"
});

// 2. Save to user profile
const storage = getCardStorage();
await storage.saveCard("user-123", card);
```

## Architecture

### cardTemplating.ts
- **`createBaseCard(input)`** - Templates linguistic data into standardized JSON
- **`validateCardInput(input)`** - Validates input before card creation
- **`createBaseCardBatch(inputs)`** - Bulk card creation

### cardStorage.ts
- **Abstract interface** - `CardStorageProvider` for database-agnostic persistence
- **In-memory implementation** - For testing and development
- **Factory function** - `getCardStorage()` returns configured provider

## API Routes

- `POST /api/cards` - Create new card
- `GET /api/cards` - Get all user's cards
- `GET /api/cards/[cardId]` - Get specific card
- `PUT /api/cards/[cardId]` - Update card (versioned)
- `DELETE /api/cards/[cardId]` - Delete card

## Best Practices

✅ **DO:**
- Keep cards immutable once learned
- Version cards instead of regenerating IDs
- Validate input before card creation
- Use the abstract storage interface for flexibility
- Attach learning progress separately from cards

❌ **DON'T:**
- Let LLMs write directly to database
- Regenerate card IDs on edits
- Store progress data inside cards
- Skip input validation

## Testing

```typescript
import { InMemoryCardStorage } from "./cardStorage";

const storage = new InMemoryCardStorage();
const card = createBaseCard(testInput);
await storage.saveCard("test-user", card);

const retrieved = await storage.getCard("test-user", card.cardId);
expect(retrieved).toEqual(card);
```
Loading