diff --git a/studio-v2/app/api/learning-units/[...path]/route.ts b/studio-v2/app/api/learning-units/[...path]/route.ts new file mode 100644 index 0000000..9f5d6b8 --- /dev/null +++ b/studio-v2/app/api/learning-units/[...path]/route.ts @@ -0,0 +1,36 @@ +import { NextRequest, NextResponse } from 'next/server' + +const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-lehrer:8001' + +async function proxyRequest( + request: NextRequest, + { params }: { params: Promise<{ path: string[] }> } +): Promise { + const { path } = await params + const pathStr = path.join('/') + const searchParams = request.nextUrl.searchParams.toString() + const url = `${BACKEND_URL}/api/learning-units/${pathStr}${searchParams ? `?${searchParams}` : ''}` + + try { + const fetchOptions: RequestInit = { + method: request.method, + headers: { 'Content-Type': 'application/json' }, + } + if (request.method !== 'GET' && request.method !== 'HEAD') { + fetchOptions.body = await request.text() + } + const resp = await fetch(url, fetchOptions) + const data = await resp.text() + return new NextResponse(data, { + status: resp.status, + headers: { 'Content-Type': resp.headers.get('Content-Type') || 'application/json' }, + }) + } catch (e) { + return NextResponse.json({ error: String(e) }, { status: 502 }) + } +} + +export const GET = proxyRequest +export const POST = proxyRequest +export const PUT = proxyRequest +export const DELETE = proxyRequest diff --git a/studio-v2/app/api/learning-units/route.ts b/studio-v2/app/api/learning-units/route.ts new file mode 100644 index 0000000..fbad085 --- /dev/null +++ b/studio-v2/app/api/learning-units/route.ts @@ -0,0 +1,29 @@ +import { NextRequest, NextResponse } from 'next/server' + +const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-lehrer:8001' + +async function proxyRequest(request: NextRequest): Promise { + const searchParams = request.nextUrl.searchParams.toString() + const url = `${BACKEND_URL}/api/learning-units/${searchParams ? `?${searchParams}` : ''}` + + try { + const fetchOptions: RequestInit = { + method: request.method, + headers: { 'Content-Type': 'application/json' }, + } + if (request.method !== 'GET' && request.method !== 'HEAD') { + fetchOptions.body = await request.text() + } + const resp = await fetch(url, fetchOptions) + const data = await resp.text() + return new NextResponse(data, { + status: resp.status, + headers: { 'Content-Type': resp.headers.get('Content-Type') || 'application/json' }, + }) + } catch (e) { + return NextResponse.json({ error: String(e) }, { status: 502 }) + } +} + +export const GET = proxyRequest +export const POST = proxyRequest diff --git a/studio-v2/app/api/progress/[...path]/route.ts b/studio-v2/app/api/progress/[...path]/route.ts new file mode 100644 index 0000000..a91e4c8 --- /dev/null +++ b/studio-v2/app/api/progress/[...path]/route.ts @@ -0,0 +1,34 @@ +import { NextRequest, NextResponse } from 'next/server' + +const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-lehrer:8001' + +async function proxyRequest( + request: NextRequest, + { params }: { params: Promise<{ path: string[] }> } +): Promise { + const { path } = await params + const pathStr = path.join('/') + const searchParams = request.nextUrl.searchParams.toString() + const url = `${BACKEND_URL}/api/progress/${pathStr}${searchParams ? `?${searchParams}` : ''}` + + try { + const fetchOptions: RequestInit = { + method: request.method, + headers: { 'Content-Type': 'application/json' }, + } + if (request.method !== 'GET' && request.method !== 'HEAD') { + fetchOptions.body = await request.text() + } + const resp = await fetch(url, fetchOptions) + const data = await resp.text() + return new NextResponse(data, { + status: resp.status, + headers: { 'Content-Type': resp.headers.get('Content-Type') || 'application/json' }, + }) + } catch (e) { + return NextResponse.json({ error: String(e) }, { status: 502 }) + } +} + +export const GET = proxyRequest +export const POST = proxyRequest diff --git a/studio-v2/app/api/vocabulary/[...path]/route.ts b/studio-v2/app/api/vocabulary/[...path]/route.ts new file mode 100644 index 0000000..e8ecec3 --- /dev/null +++ b/studio-v2/app/api/vocabulary/[...path]/route.ts @@ -0,0 +1,36 @@ +import { NextRequest, NextResponse } from 'next/server' + +const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-lehrer:8001' + +async function proxyRequest( + request: NextRequest, + { params }: { params: Promise<{ path: string[] }> } +): Promise { + const { path } = await params + const pathStr = path.join('/') + const searchParams = request.nextUrl.searchParams.toString() + const url = `${BACKEND_URL}/api/vocabulary/${pathStr}${searchParams ? `?${searchParams}` : ''}` + + try { + const fetchOptions: RequestInit = { + method: request.method, + headers: { 'Content-Type': 'application/json' }, + } + if (request.method !== 'GET' && request.method !== 'HEAD') { + fetchOptions.body = await request.text() + } + const resp = await fetch(url, fetchOptions) + const data = await resp.text() + return new NextResponse(data, { + status: resp.status, + headers: { 'Content-Type': resp.headers.get('Content-Type') || 'application/json' }, + }) + } catch (e) { + return NextResponse.json({ error: String(e) }, { status: 502 }) + } +} + +export const GET = proxyRequest +export const POST = proxyRequest +export const PUT = proxyRequest +export const DELETE = proxyRequest diff --git a/studio-v2/app/api/vocabulary/route.ts b/studio-v2/app/api/vocabulary/route.ts new file mode 100644 index 0000000..d3d23eb --- /dev/null +++ b/studio-v2/app/api/vocabulary/route.ts @@ -0,0 +1,29 @@ +import { NextRequest, NextResponse } from 'next/server' + +const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-lehrer:8001' + +async function proxyRequest(request: NextRequest): Promise { + const searchParams = request.nextUrl.searchParams.toString() + const url = `${BACKEND_URL}/api/vocabulary/${searchParams ? `?${searchParams}` : ''}` + + try { + const fetchOptions: RequestInit = { + method: request.method, + headers: { 'Content-Type': 'application/json' }, + } + if (request.method !== 'GET' && request.method !== 'HEAD') { + fetchOptions.body = await request.text() + } + const resp = await fetch(url, fetchOptions) + const data = await resp.text() + return new NextResponse(data, { + status: resp.status, + headers: { 'Content-Type': resp.headers.get('Content-Type') || 'application/json' }, + }) + } catch (e) { + return NextResponse.json({ error: String(e) }, { status: 502 }) + } +} + +export const GET = proxyRequest +export const POST = proxyRequest diff --git a/studio-v2/app/learn/[unitId]/flashcards/page.tsx b/studio-v2/app/learn/[unitId]/flashcards/page.tsx index 96e0f2b..1cafe1b 100644 --- a/studio-v2/app/learn/[unitId]/flashcards/page.tsx +++ b/studio-v2/app/learn/[unitId]/flashcards/page.tsx @@ -15,11 +15,11 @@ interface QAItem { incorrect_count: number } -function getBackendUrl() { - if (typeof window === 'undefined') return 'http://localhost:8001' - const { hostname, protocol } = window.location - if (hostname === 'localhost') return 'http://localhost:8001' - return `${protocol}//${hostname}:8001` +function getApiBase() { + return '' // Same-origin proxy + + + } export default function FlashcardsPage() { @@ -45,7 +45,7 @@ export default function FlashcardsPage() { const loadQA = async () => { setIsLoading(true) try { - const resp = await fetch(`${getBackendUrl()}/api/learning-units/${unitId}/qa`) + const resp = await fetch(`${getApiBase()}/api/learning-units/${unitId}/qa`) if (!resp.ok) throw new Error(`HTTP ${resp.status}`) const data = await resp.json() setItems(data.qa_items || []) @@ -63,7 +63,7 @@ export default function FlashcardsPage() { // Update Leitner progress try { await fetch( - `${getBackendUrl()}/api/learning-units/${unitId}/leitner/update?item_id=${item.id}&correct=${correct}`, + `${getApiBase()}/api/learning-units/${unitId}/leitner/update?item_id=${item.id}&correct=${correct}`, { method: 'POST' } ) } catch (err) { diff --git a/studio-v2/app/learn/[unitId]/quiz/page.tsx b/studio-v2/app/learn/[unitId]/quiz/page.tsx index 9900d01..1dc93b3 100644 --- a/studio-v2/app/learn/[unitId]/quiz/page.tsx +++ b/studio-v2/app/learn/[unitId]/quiz/page.tsx @@ -13,11 +13,11 @@ interface MCQuestion { explanation?: string } -function getBackendUrl() { - if (typeof window === 'undefined') return 'http://localhost:8001' - const { hostname, protocol } = window.location - if (hostname === 'localhost') return 'http://localhost:8001' - return `${protocol}//${hostname}:8001` +function getApiBase() { + return '' // Same-origin proxy + + + } export default function QuizPage() { @@ -43,7 +43,7 @@ export default function QuizPage() { const loadMC = async () => { setIsLoading(true) try { - const resp = await fetch(`${getBackendUrl()}/api/learning-units/${unitId}/mc`) + const resp = await fetch(`${getApiBase()}/api/learning-units/${unitId}/mc`) if (!resp.ok) throw new Error(`HTTP ${resp.status}`) const data = await resp.json() setQuestions(data.questions || []) diff --git a/studio-v2/app/learn/[unitId]/story/page.tsx b/studio-v2/app/learn/[unitId]/story/page.tsx index 0fe8235..92fcb77 100644 --- a/studio-v2/app/learn/[unitId]/story/page.tsx +++ b/studio-v2/app/learn/[unitId]/story/page.tsx @@ -5,16 +5,16 @@ import { useParams, useRouter } from 'next/navigation' import { useTheme } from '@/lib/ThemeContext' import { AudioButton } from '@/components/learn/AudioButton' -function getBackendUrl() { - if (typeof window === 'undefined') return 'http://localhost:8001' - const { hostname, protocol } = window.location - if (hostname === 'localhost') return 'http://localhost:8001' - return `${protocol}//${hostname}:8001` +function getApiBase() { + return '' // Same-origin proxy + + + } function getKlausurApiUrl() { if (typeof window === 'undefined') return 'http://localhost:8086' - const { hostname, protocol } = window.location + if (hostname === 'localhost') return 'http://localhost:8086' return `${protocol}//${hostname}/klausur-api` } @@ -39,7 +39,7 @@ export default function StoryPage() { try { // First get the QA data to extract vocabulary - const qaResp = await fetch(`${getBackendUrl()}/api/learning-units/${unitId}/qa`) + const qaResp = await fetch(`${getApiBase()}/api/learning-units/${unitId}/qa`) let vocabulary: { english: string; german: string }[] = [] if (qaResp.ok) { @@ -58,7 +58,7 @@ export default function StoryPage() { } // Generate story - const resp = await fetch(`${getBackendUrl()}/api/learning-units/${unitId}/generate-story`, { + const resp = await fetch(`${getApiBase()}/api/learning-units/${unitId}/generate-story`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ vocabulary, language, grade_level: '5-8' }), diff --git a/studio-v2/app/learn/[unitId]/type/page.tsx b/studio-v2/app/learn/[unitId]/type/page.tsx index 09f8e9e..b6c6ced 100644 --- a/studio-v2/app/learn/[unitId]/type/page.tsx +++ b/studio-v2/app/learn/[unitId]/type/page.tsx @@ -13,11 +13,11 @@ interface QAItem { leitner_box: number } -function getBackendUrl() { - if (typeof window === 'undefined') return 'http://localhost:8001' - const { hostname, protocol } = window.location - if (hostname === 'localhost') return 'http://localhost:8001' - return `${protocol}//${hostname}:8001` +function getApiBase() { + return '' // Same-origin proxy + + + } export default function TypePage() { @@ -44,7 +44,7 @@ export default function TypePage() { const loadQA = async () => { setIsLoading(true) try { - const resp = await fetch(`${getBackendUrl()}/api/learning-units/${unitId}/qa`) + const resp = await fetch(`${getApiBase()}/api/learning-units/${unitId}/qa`) if (!resp.ok) throw new Error(`HTTP ${resp.status}`) const data = await resp.json() setItems(data.qa_items || []) @@ -61,7 +61,7 @@ export default function TypePage() { try { await fetch( - `${getBackendUrl()}/api/learning-units/${unitId}/leitner/update?item_id=${item.id}&correct=${correct}`, + `${getApiBase()}/api/learning-units/${unitId}/leitner/update?item_id=${item.id}&correct=${correct}`, { method: 'POST' } ) } catch (err) { diff --git a/studio-v2/app/learn/page.tsx b/studio-v2/app/learn/page.tsx index b234370..4c4ff68 100644 --- a/studio-v2/app/learn/page.tsx +++ b/studio-v2/app/learn/page.tsx @@ -17,11 +17,8 @@ interface LearningUnit { created_at: string } -function getBackendUrl() { - if (typeof window === 'undefined') return 'http://localhost:8001' - const { hostname, protocol } = window.location - if (hostname === 'localhost') return 'http://localhost:8001' - return `${protocol}//${hostname}:8001` +function getApiBase() { + return '' // Same-origin proxy via /api/learning-units/... } export default function LearnPage() { @@ -41,7 +38,7 @@ export default function LearnPage() { const loadUnits = async () => { setIsLoading(true) try { - const resp = await fetch(`${getBackendUrl()}/api/learning-units/`) + const resp = await fetch(`${getApiBase()}/api/learning-units/`) if (!resp.ok) throw new Error(`HTTP ${resp.status}`) const data = await resp.json() setUnits(data) @@ -54,7 +51,7 @@ export default function LearnPage() { const handleDelete = async (unitId: string) => { try { - const resp = await fetch(`${getBackendUrl()}/api/learning-units/${unitId}`, { method: 'DELETE' }) + const resp = await fetch(`${getApiBase()}/api/learning-units/${unitId}`, { method: 'DELETE' }) if (resp.ok) { setUnits((prev) => prev.filter((u) => u.id !== unitId)) } diff --git a/studio-v2/app/vocabulary/page.tsx b/studio-v2/app/vocabulary/page.tsx index 7246624..36468d9 100644 --- a/studio-v2/app/vocabulary/page.tsx +++ b/studio-v2/app/vocabulary/page.tsx @@ -22,11 +22,9 @@ interface VocabWord { tags: string[] } -function getBackendUrl() { - if (typeof window === 'undefined') return 'http://localhost:8001' - const { hostname, protocol } = window.location - if (hostname === 'localhost') return 'http://localhost:8001' - return `${protocol}//${hostname}:8001` +/** Use Next.js API proxy to avoid mixed-content/CORS issues */ +function getApiBase() { + return '' // Same-origin: /api/vocabulary/... proxied by Next.js } export default function VocabularyPage() { @@ -55,7 +53,7 @@ export default function VocabularyPage() { // Load filters on mount useEffect(() => { - fetch(`${getBackendUrl()}/api/vocabulary/filters`) + fetch(`${getApiBase()}/api/vocabulary/filters`) .then(r => r.ok ? r.json() : null) .then(d => { if (d) setFilters(d) }) .catch(() => {}) @@ -73,12 +71,12 @@ export default function VocabularyPage() { try { let url: string if (query.trim()) { - url = `${getBackendUrl()}/api/vocabulary/search?q=${encodeURIComponent(query)}&limit=30` + url = `${getApiBase()}/api/vocabulary/search?q=${encodeURIComponent(query)}&limit=30` } else { const params = new URLSearchParams({ limit: '30' }) if (posFilter) params.set('pos', posFilter) if (diffFilter) params.set('difficulty', String(diffFilter)) - url = `${getBackendUrl()}/api/vocabulary/browse?${params}` + url = `${getApiBase()}/api/vocabulary/browse?${params}` } const resp = await fetch(url) if (resp.ok) { @@ -107,7 +105,7 @@ export default function VocabularyPage() { if (!unitTitle.trim() || selectedWords.length === 0) return setIsCreating(true) try { - const resp = await fetch(`${getBackendUrl()}/api/vocabulary/units`, { + const resp = await fetch(`${getApiBase()}/api/vocabulary/units`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ diff --git a/studio-v2/components/dashboard/LearningProgress.tsx b/studio-v2/components/dashboard/LearningProgress.tsx index f829fa2..bb8cf76 100644 --- a/studio-v2/components/dashboard/LearningProgress.tsx +++ b/studio-v2/components/dashboard/LearningProgress.tsx @@ -31,11 +31,11 @@ interface LearningProgressProps { glassCard: string } -function getBackendUrl() { - if (typeof window === 'undefined') return 'http://localhost:8001' - const { hostname, protocol } = window.location - if (hostname === 'localhost') return 'http://localhost:8001' - return `${protocol}//${hostname}:8001` +function getApiBase() { + return '' // Same-origin proxy + + + } export function LearningProgress({ isDark, glassCard }: LearningProgressProps) { @@ -51,8 +51,8 @@ export function LearningProgress({ isDark, glassCard }: LearningProgressProps) { setIsLoading(true) try { const [unitsResp, progressResp] = await Promise.all([ - fetch(`${getBackendUrl()}/api/learning-units/`), - fetch(`${getBackendUrl()}/api/progress/`), + fetch(`${getApiBase()}/api/learning-units/`), + fetch(`${getApiBase()}/api/progress/`), ]) if (unitsResp.ok) setUnits(await unitsResp.json())