Fix: Use Next.js API proxy to avoid mixed-content/CORS errors
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 54s
CI / test-go-edu-search (push) Successful in 53s
CI / test-python-klausur (push) Failing after 2m57s
CI / test-python-agent-core (push) Successful in 43s
CI / test-nodejs-website (push) Successful in 46s
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 54s
CI / test-go-edu-search (push) Successful in 53s
CI / test-python-klausur (push) Failing after 2m57s
CI / test-python-agent-core (push) Successful in 43s
CI / test-nodejs-website (push) Successful in 46s
HTTPS pages cannot fetch from HTTP backend ports. Added Next.js API route proxies for /api/vocabulary, /api/learning-units, /api/progress that forward to backend-lehrer internally (same Docker network, HTTP). All frontend pages now use same-origin requests (getApiBase = '') instead of direct port:8001 connections. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
36
studio-v2/app/api/learning-units/[...path]/route.ts
Normal file
36
studio-v2/app/api/learning-units/[...path]/route.ts
Normal file
@@ -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<NextResponse> {
|
||||||
|
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
|
||||||
29
studio-v2/app/api/learning-units/route.ts
Normal file
29
studio-v2/app/api/learning-units/route.ts
Normal file
@@ -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<NextResponse> {
|
||||||
|
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
|
||||||
34
studio-v2/app/api/progress/[...path]/route.ts
Normal file
34
studio-v2/app/api/progress/[...path]/route.ts
Normal file
@@ -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<NextResponse> {
|
||||||
|
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
|
||||||
36
studio-v2/app/api/vocabulary/[...path]/route.ts
Normal file
36
studio-v2/app/api/vocabulary/[...path]/route.ts
Normal file
@@ -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<NextResponse> {
|
||||||
|
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
|
||||||
29
studio-v2/app/api/vocabulary/route.ts
Normal file
29
studio-v2/app/api/vocabulary/route.ts
Normal file
@@ -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<NextResponse> {
|
||||||
|
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
|
||||||
@@ -15,11 +15,11 @@ interface QAItem {
|
|||||||
incorrect_count: number
|
incorrect_count: number
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBackendUrl() {
|
function getApiBase() {
|
||||||
if (typeof window === 'undefined') return 'http://localhost:8001'
|
return '' // Same-origin proxy
|
||||||
const { hostname, protocol } = window.location
|
|
||||||
if (hostname === 'localhost') return 'http://localhost:8001'
|
|
||||||
return `${protocol}//${hostname}:8001`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function FlashcardsPage() {
|
export default function FlashcardsPage() {
|
||||||
@@ -45,7 +45,7 @@ export default function FlashcardsPage() {
|
|||||||
const loadQA = async () => {
|
const loadQA = async () => {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
try {
|
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}`)
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
|
||||||
const data = await resp.json()
|
const data = await resp.json()
|
||||||
setItems(data.qa_items || [])
|
setItems(data.qa_items || [])
|
||||||
@@ -63,7 +63,7 @@ export default function FlashcardsPage() {
|
|||||||
// Update Leitner progress
|
// Update Leitner progress
|
||||||
try {
|
try {
|
||||||
await fetch(
|
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' }
|
{ method: 'POST' }
|
||||||
)
|
)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ interface MCQuestion {
|
|||||||
explanation?: string
|
explanation?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBackendUrl() {
|
function getApiBase() {
|
||||||
if (typeof window === 'undefined') return 'http://localhost:8001'
|
return '' // Same-origin proxy
|
||||||
const { hostname, protocol } = window.location
|
|
||||||
if (hostname === 'localhost') return 'http://localhost:8001'
|
|
||||||
return `${protocol}//${hostname}:8001`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function QuizPage() {
|
export default function QuizPage() {
|
||||||
@@ -43,7 +43,7 @@ export default function QuizPage() {
|
|||||||
const loadMC = async () => {
|
const loadMC = async () => {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
try {
|
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}`)
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
|
||||||
const data = await resp.json()
|
const data = await resp.json()
|
||||||
setQuestions(data.questions || [])
|
setQuestions(data.questions || [])
|
||||||
|
|||||||
@@ -5,16 +5,16 @@ import { useParams, useRouter } from 'next/navigation'
|
|||||||
import { useTheme } from '@/lib/ThemeContext'
|
import { useTheme } from '@/lib/ThemeContext'
|
||||||
import { AudioButton } from '@/components/learn/AudioButton'
|
import { AudioButton } from '@/components/learn/AudioButton'
|
||||||
|
|
||||||
function getBackendUrl() {
|
function getApiBase() {
|
||||||
if (typeof window === 'undefined') return 'http://localhost:8001'
|
return '' // Same-origin proxy
|
||||||
const { hostname, protocol } = window.location
|
|
||||||
if (hostname === 'localhost') return 'http://localhost:8001'
|
|
||||||
return `${protocol}//${hostname}:8001`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getKlausurApiUrl() {
|
function getKlausurApiUrl() {
|
||||||
if (typeof window === 'undefined') return 'http://localhost:8086'
|
if (typeof window === 'undefined') return 'http://localhost:8086'
|
||||||
const { hostname, protocol } = window.location
|
|
||||||
if (hostname === 'localhost') return 'http://localhost:8086'
|
if (hostname === 'localhost') return 'http://localhost:8086'
|
||||||
return `${protocol}//${hostname}/klausur-api`
|
return `${protocol}//${hostname}/klausur-api`
|
||||||
}
|
}
|
||||||
@@ -39,7 +39,7 @@ export default function StoryPage() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// First get the QA data to extract vocabulary
|
// 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 }[] = []
|
let vocabulary: { english: string; german: string }[] = []
|
||||||
|
|
||||||
if (qaResp.ok) {
|
if (qaResp.ok) {
|
||||||
@@ -58,7 +58,7 @@ export default function StoryPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Generate story
|
// 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',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ vocabulary, language, grade_level: '5-8' }),
|
body: JSON.stringify({ vocabulary, language, grade_level: '5-8' }),
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ interface QAItem {
|
|||||||
leitner_box: number
|
leitner_box: number
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBackendUrl() {
|
function getApiBase() {
|
||||||
if (typeof window === 'undefined') return 'http://localhost:8001'
|
return '' // Same-origin proxy
|
||||||
const { hostname, protocol } = window.location
|
|
||||||
if (hostname === 'localhost') return 'http://localhost:8001'
|
|
||||||
return `${protocol}//${hostname}:8001`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TypePage() {
|
export default function TypePage() {
|
||||||
@@ -44,7 +44,7 @@ export default function TypePage() {
|
|||||||
const loadQA = async () => {
|
const loadQA = async () => {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
try {
|
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}`)
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
|
||||||
const data = await resp.json()
|
const data = await resp.json()
|
||||||
setItems(data.qa_items || [])
|
setItems(data.qa_items || [])
|
||||||
@@ -61,7 +61,7 @@ export default function TypePage() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await fetch(
|
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' }
|
{ method: 'POST' }
|
||||||
)
|
)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -17,11 +17,8 @@ interface LearningUnit {
|
|||||||
created_at: string
|
created_at: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBackendUrl() {
|
function getApiBase() {
|
||||||
if (typeof window === 'undefined') return 'http://localhost:8001'
|
return '' // Same-origin proxy via /api/learning-units/...
|
||||||
const { hostname, protocol } = window.location
|
|
||||||
if (hostname === 'localhost') return 'http://localhost:8001'
|
|
||||||
return `${protocol}//${hostname}:8001`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function LearnPage() {
|
export default function LearnPage() {
|
||||||
@@ -41,7 +38,7 @@ export default function LearnPage() {
|
|||||||
const loadUnits = async () => {
|
const loadUnits = async () => {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
try {
|
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}`)
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
|
||||||
const data = await resp.json()
|
const data = await resp.json()
|
||||||
setUnits(data)
|
setUnits(data)
|
||||||
@@ -54,7 +51,7 @@ export default function LearnPage() {
|
|||||||
|
|
||||||
const handleDelete = async (unitId: string) => {
|
const handleDelete = async (unitId: string) => {
|
||||||
try {
|
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) {
|
if (resp.ok) {
|
||||||
setUnits((prev) => prev.filter((u) => u.id !== unitId))
|
setUnits((prev) => prev.filter((u) => u.id !== unitId))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,11 +22,9 @@ interface VocabWord {
|
|||||||
tags: string[]
|
tags: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBackendUrl() {
|
/** Use Next.js API proxy to avoid mixed-content/CORS issues */
|
||||||
if (typeof window === 'undefined') return 'http://localhost:8001'
|
function getApiBase() {
|
||||||
const { hostname, protocol } = window.location
|
return '' // Same-origin: /api/vocabulary/... proxied by Next.js
|
||||||
if (hostname === 'localhost') return 'http://localhost:8001'
|
|
||||||
return `${protocol}//${hostname}:8001`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function VocabularyPage() {
|
export default function VocabularyPage() {
|
||||||
@@ -55,7 +53,7 @@ export default function VocabularyPage() {
|
|||||||
|
|
||||||
// Load filters on mount
|
// Load filters on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch(`${getBackendUrl()}/api/vocabulary/filters`)
|
fetch(`${getApiBase()}/api/vocabulary/filters`)
|
||||||
.then(r => r.ok ? r.json() : null)
|
.then(r => r.ok ? r.json() : null)
|
||||||
.then(d => { if (d) setFilters(d) })
|
.then(d => { if (d) setFilters(d) })
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
@@ -73,12 +71,12 @@ export default function VocabularyPage() {
|
|||||||
try {
|
try {
|
||||||
let url: string
|
let url: string
|
||||||
if (query.trim()) {
|
if (query.trim()) {
|
||||||
url = `${getBackendUrl()}/api/vocabulary/search?q=${encodeURIComponent(query)}&limit=30`
|
url = `${getApiBase()}/api/vocabulary/search?q=${encodeURIComponent(query)}&limit=30`
|
||||||
} else {
|
} else {
|
||||||
const params = new URLSearchParams({ limit: '30' })
|
const params = new URLSearchParams({ limit: '30' })
|
||||||
if (posFilter) params.set('pos', posFilter)
|
if (posFilter) params.set('pos', posFilter)
|
||||||
if (diffFilter) params.set('difficulty', String(diffFilter))
|
if (diffFilter) params.set('difficulty', String(diffFilter))
|
||||||
url = `${getBackendUrl()}/api/vocabulary/browse?${params}`
|
url = `${getApiBase()}/api/vocabulary/browse?${params}`
|
||||||
}
|
}
|
||||||
const resp = await fetch(url)
|
const resp = await fetch(url)
|
||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
@@ -107,7 +105,7 @@ export default function VocabularyPage() {
|
|||||||
if (!unitTitle.trim() || selectedWords.length === 0) return
|
if (!unitTitle.trim() || selectedWords.length === 0) return
|
||||||
setIsCreating(true)
|
setIsCreating(true)
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`${getBackendUrl()}/api/vocabulary/units`, {
|
const resp = await fetch(`${getApiBase()}/api/vocabulary/units`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
|||||||
@@ -31,11 +31,11 @@ interface LearningProgressProps {
|
|||||||
glassCard: string
|
glassCard: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBackendUrl() {
|
function getApiBase() {
|
||||||
if (typeof window === 'undefined') return 'http://localhost:8001'
|
return '' // Same-origin proxy
|
||||||
const { hostname, protocol } = window.location
|
|
||||||
if (hostname === 'localhost') return 'http://localhost:8001'
|
|
||||||
return `${protocol}//${hostname}:8001`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LearningProgress({ isDark, glassCard }: LearningProgressProps) {
|
export function LearningProgress({ isDark, glassCard }: LearningProgressProps) {
|
||||||
@@ -51,8 +51,8 @@ export function LearningProgress({ isDark, glassCard }: LearningProgressProps) {
|
|||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
try {
|
try {
|
||||||
const [unitsResp, progressResp] = await Promise.all([
|
const [unitsResp, progressResp] = await Promise.all([
|
||||||
fetch(`${getBackendUrl()}/api/learning-units/`),
|
fetch(`${getApiBase()}/api/learning-units/`),
|
||||||
fetch(`${getBackendUrl()}/api/progress/`),
|
fetch(`${getApiBase()}/api/progress/`),
|
||||||
])
|
])
|
||||||
|
|
||||||
if (unitsResp.ok) setUnits(await unitsResp.json())
|
if (unitsResp.ok) setUnits(await unitsResp.json())
|
||||||
|
|||||||
Reference in New Issue
Block a user