Compare commits
4 Commits
9faf3f7618
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95880ca080 | ||
|
|
42adeb290c | ||
|
|
8c270fc4ea | ||
| 8e4ce66cc4 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -32,6 +32,7 @@ yarn-error.log*
|
|||||||
|
|
||||||
# env files (can opt-in for committing if needed)
|
# env files (can opt-in for committing if needed)
|
||||||
.env*
|
.env*
|
||||||
|
/runtimeConfig.json
|
||||||
|
|
||||||
# vercel
|
# vercel
|
||||||
.vercel
|
.vercel
|
||||||
|
|||||||
16
package-lock.json
generated
16
package-lock.json
generated
@@ -15,7 +15,7 @@
|
|||||||
"react-dom": "^19"
|
"react-dom": "^19"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20",
|
"@types/node": "^22",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"prettier": "^3",
|
"prettier": "^3",
|
||||||
@@ -589,13 +589,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "20.17.50",
|
"version": "22.15.21",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.50.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz",
|
||||||
"integrity": "sha512-Mxiq0ULv/zo1OzOhwPqOA13I81CV/W3nvd3ChtQZRT5Cwz3cr0FKo/wMSsbTqL3EXpaBAEQhva2B8ByRkOIh9A==",
|
"integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.19.2"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
@@ -1091,9 +1091,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "6.19.8",
|
"version": "6.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
"react-dom": "^19"
|
"react-dom": "^19"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20",
|
"@types/node": "^22",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"prettier": "^3",
|
"prettier": "^3",
|
||||||
|
|||||||
55
src/app/api/proxy/functions.ts
Normal file
55
src/app/api/proxy/functions.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import type { NextRequest } from 'next/server'
|
||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
|
||||||
|
/// Resend request to backend.
|
||||||
|
/// Body is being sent as stream.
|
||||||
|
export async function redirectRequest(
|
||||||
|
req: NextRequest,
|
||||||
|
proxyUrl: string,
|
||||||
|
backendBaseUrl: string
|
||||||
|
): Promise<NextResponse> {
|
||||||
|
const reqUrl = req.url // full URL e.g. http://localhost:3000/api/proxy?id=123
|
||||||
|
|
||||||
|
// Find position of proxyPath in reqUrl
|
||||||
|
const proxyIndex = reqUrl.indexOf(proxyUrl)
|
||||||
|
if (proxyIndex === -1) {
|
||||||
|
return new NextResponse('Invalid proxy path', { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace '/api/proxy/parserBackend' with backend URL, preserve everything after
|
||||||
|
// e.g. '/api/proxy/extra/path?id=123' -> 'http://backend-server/extra/path?id=123'
|
||||||
|
const backendUrl = backendBaseUrl + reqUrl.substring(proxyIndex + proxyUrl.length)
|
||||||
|
// console.log('redirecting', req.url, 'to', backendUrl)
|
||||||
|
|
||||||
|
const backendRes = await fetch(backendUrl, {
|
||||||
|
method: req.method,
|
||||||
|
headers: req.headers,
|
||||||
|
body: req.body,
|
||||||
|
// @ts-ignore
|
||||||
|
duplex: 'half', // some property required by nodejs but not defined in @types/node
|
||||||
|
})
|
||||||
|
|
||||||
|
return new NextResponse(backendRes.body, {
|
||||||
|
status: backendRes.status,
|
||||||
|
headers: {
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Content-Type': backendRes.headers.get('content-type') || 'application/octet-stream',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Browsers send OPTIONS request sometimes to check webserver allowed headers and methods.
|
||||||
|
/// My backend doesn't support such requests so use proxy to answer to it that all headers and methods are allowed
|
||||||
|
export async function responseToOPTIONS(req: NextRequest): Promise<NextResponse> {
|
||||||
|
// console.log('proxy responding to CORS OPTIONS', req.url)
|
||||||
|
|
||||||
|
return new NextResponse(null, {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Access-Control-Allow-Methods': 'GET, POST, PUT, PATCH, DELETE, HEAD',
|
||||||
|
'Access-Control-Allow-Headers':
|
||||||
|
req.headers.get('access-control-request-headers') || 'Content-Type, Authorization',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
3
src/app/api/proxy/paths.ts
Normal file
3
src/app/api/proxy/paths.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export const PROXY_PATHS = {
|
||||||
|
saveParserBackend: '/api/proxy/saveParserBackend',
|
||||||
|
}
|
||||||
17
src/app/api/proxy/saveParserBackend/[[...path]]/route.ts
Normal file
17
src/app/api/proxy/saveParserBackend/[[...path]]/route.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import type { NextRequest } from 'next/server'
|
||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
import { redirectRequest, responseToOPTIONS as respondToOPTIONS } from '@/app/api/proxy/functions'
|
||||||
|
import { PROXY_PATHS } from '@/app/api/proxy/paths'
|
||||||
|
import { appConfig } from '@/lib/configLoader'
|
||||||
|
|
||||||
|
async function redirect(req: NextRequest): Promise<NextResponse> {
|
||||||
|
return await redirectRequest(req, PROXY_PATHS.saveParserBackend, appConfig.services.saveParserBackend.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GET = redirect
|
||||||
|
export const POST = redirect
|
||||||
|
export const PUT = redirect
|
||||||
|
export const PATCH = redirect
|
||||||
|
export const DELETE = redirect
|
||||||
|
export const HEAD = redirect
|
||||||
|
export const OPTIONS = respondToOPTIONS
|
||||||
@@ -5,6 +5,8 @@ import { AllCommunityModule, themeQuartz } from 'ag-grid-community'
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { colorSchemeDark } from 'ag-grid-community'
|
import { colorSchemeDark } from 'ag-grid-community'
|
||||||
|
|
||||||
|
// TODO: create tabs with general info, tables, plots
|
||||||
|
|
||||||
export default function BrowsePage() {
|
export default function BrowsePage() {
|
||||||
const [agTheme, setAgTheme] = useState(themeQuartz)
|
const [agTheme, setAgTheme] = useState(themeQuartz)
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ html,
|
|||||||
body {
|
body {
|
||||||
max-width: 100vw;
|
max-width: 100vw;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
padding: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
setError(null)
|
||||||
try {
|
try {
|
||||||
console.log('Log in:', formData)
|
console.log('Log in:', formData)
|
||||||
const result = await authService.login(formData.email, formData.password)
|
const result = await authService.login(formData.email, formData.password)
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export default function RegisterPage() {
|
|||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
setError(null)
|
||||||
try {
|
try {
|
||||||
if (formData.password !== formData.confirmPassword) {
|
if (formData.password !== formData.confirmPassword) {
|
||||||
throw new Error('Passwords do not match.')
|
throw new Error('Passwords do not match.')
|
||||||
@@ -34,7 +35,6 @@ export default function RegisterPage() {
|
|||||||
console.log('Register', formData)
|
console.log('Register', formData)
|
||||||
const result = await authService.register(formData.name, formData.email, formData.password)
|
const result = await authService.register(formData.name, formData.email, formData.password)
|
||||||
console.log('Registered:', result)
|
console.log('Registered:', result)
|
||||||
|
|
||||||
router.push('/login') // Redirect after success
|
router.push('/login') // Redirect after success
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error(err.message)
|
console.error(err.message)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { font_Hack } from '@/utils/myFonts'
|
import { font_Hack } from '@/lib/myFonts'
|
||||||
|
|
||||||
export default function Navbar() {
|
export default function Navbar() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { saveParserBackendService, UploadSaveResponse } from '@/services/SaveParserBackendService'
|
import { saveParserBackendService, SaveStatusResponse, UploadSaveResponse } from '@/services/SaveParserBackendService'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import FormError from './FormError'
|
import FormError from './FormError'
|
||||||
|
|
||||||
@@ -9,71 +9,119 @@ export enum GameEnum {
|
|||||||
Stellaris = 'stellaris',
|
Stellaris = 'stellaris',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO: move to separate page
|
||||||
|
//TODO: add redirect to ?id={id} to see status of long parsing save
|
||||||
|
|
||||||
export default function SaveFileUploadingDialog() {
|
export default function SaveFileUploadingDialog() {
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
||||||
const [game, setGame] = useState<GameEnum>(GameEnum.EU4)
|
const [game, setGame] = useState<GameEnum>(GameEnum.EU4)
|
||||||
|
const [uploadDisabled, setUploadDisabled] = useState(false)
|
||||||
|
const [saveStatus, setSaveStatus] = useState<SaveStatusResponse | null>(null)
|
||||||
const [uploadResult, setUploadResult] = useState<UploadSaveResponse | null>(null)
|
const [uploadResult, setUploadResult] = useState<UploadSaveResponse | null>(null)
|
||||||
|
|
||||||
|
const handleGameSelect = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
setGame(e.target.value as GameEnum)
|
||||||
|
}
|
||||||
|
|
||||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
if (e.target.files && e.target.files[0]) {
|
if (e.target.files && e.target.files[0]) {
|
||||||
setSelectedFile(e.target.files[0])
|
setSelectedFile(e.target.files[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleUpload = async () => {
|
const handleUpload = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setError(null)
|
||||||
|
setUploadDisabled(true)
|
||||||
try {
|
try {
|
||||||
if (!selectedFile) return
|
if (!selectedFile) return
|
||||||
|
|
||||||
console.log('Uploading file:', selectedFile.name)
|
console.log('Uploading file:', selectedFile.name)
|
||||||
const result = await saveParserBackendService.uploadSave(game, selectedFile)
|
const uploadResponse = await saveParserBackendService.uploadSave(game, selectedFile)
|
||||||
console.log('Got response:', result)
|
console.log('Upload response:', uploadResponse)
|
||||||
setUploadResult(result)
|
setUploadResult(uploadResponse)
|
||||||
|
|
||||||
|
const pollInterval = 1000 // ms
|
||||||
|
const intervalId = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
const statusResponse = await saveParserBackendService.getSaveStatus(uploadResponse.id)
|
||||||
|
console.log('Save status:', statusResponse.status)
|
||||||
|
setSaveStatus(statusResponse)
|
||||||
|
if (statusResponse.status.toLowerCase() === 'done') {
|
||||||
|
clearInterval(intervalId)
|
||||||
|
setUploadDisabled(false)
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('Error while polling status:', err)
|
||||||
|
clearInterval(intervalId)
|
||||||
|
setError(err.message)
|
||||||
|
setUploadDisabled(false)
|
||||||
|
}
|
||||||
|
}, pollInterval)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error(err.message)
|
console.error(err.message)
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
|
setUploadDisabled(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const gameEnumOptions = Object.values(GameEnum).map((value) => (
|
||||||
|
<option key={value} value={value}>
|
||||||
|
{value}
|
||||||
|
</option>
|
||||||
|
))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mt-5" style={{ maxWidth: '500px' }}>
|
<div className="container mt-5" style={{ maxWidth: '500px' }}>
|
||||||
<h3 className="mb-2 fw-semibold">Select game save file</h3>
|
|
||||||
<FormError message={error} />
|
<FormError message={error} />
|
||||||
|
|
||||||
<div className="mb-3">
|
<form onSubmit={handleUpload} className="text-start">
|
||||||
<label htmlFor="gameSelect" className="form-label">
|
<label htmlFor="gameSelect" className="form-label">
|
||||||
Select Game
|
Select Game
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
id="gameSelect"
|
id="gameSelect"
|
||||||
className="form-select"
|
name="gameSelect"
|
||||||
|
className="form-select mb-3"
|
||||||
value={game}
|
value={game}
|
||||||
onChange={(e) => setGame(e.target.value as GameEnum)}
|
onChange={handleGameSelect}
|
||||||
|
required
|
||||||
>
|
>
|
||||||
{Object.values(GameEnum).map((value) => (
|
{gameEnumOptions}
|
||||||
<option key={value} value={value}>
|
|
||||||
{value}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<label htmlFor="fileInput" className="form-label">
|
||||||
|
Select File
|
||||||
|
</label>
|
||||||
|
<input id="fileInput" type="file" className="form-control mb-3" onChange={handleFileChange} />
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="btn btn-primary w-100 mt-1 mb-3"
|
||||||
|
type="submit"
|
||||||
|
disabled={!selectedFile || uploadDisabled}
|
||||||
|
>
|
||||||
|
Upload File
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="container mt-5 text-center">
|
||||||
|
{/* TODO: extract this to a component
|
||||||
|
<ConditionalView cond={uploadResult}> <LabeledValue val={}/> </ConditionalView> */}
|
||||||
|
{uploadResult && (
|
||||||
|
<div className="mb-3">
|
||||||
|
Save Id: <strong>{uploadResult.id}</strong>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{saveStatus && (
|
||||||
|
<div className="mb-3">
|
||||||
|
Status: <strong>{saveStatus.status}</strong>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* add link to <ConditionalView cond={done}> /browse?id=${saveStatus.id} */}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input type="file" className="form-control mb-3" onChange={handleFileChange} />
|
|
||||||
<button className="btn btn-primary" onClick={handleUpload} disabled={!selectedFile}>
|
|
||||||
Upload File
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{selectedFile && (
|
|
||||||
<div className="mt-3">
|
|
||||||
File: <strong>{selectedFile.name}</strong>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{uploadResult && (
|
|
||||||
<div className="mt-3">
|
|
||||||
Save Id: <strong>{uploadResult.id}</strong>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
12
src/config/AppConfig.ts
Normal file
12
src/config/AppConfig.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
'server'
|
||||||
|
export const defaultConfig = {
|
||||||
|
services: {
|
||||||
|
saveParserBackend: {
|
||||||
|
url: 'http://127.0.0.1:5226',
|
||||||
|
},
|
||||||
|
// TODO: implement auth api
|
||||||
|
auth: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AppConfig = typeof defaultConfig
|
||||||
5
src/instrumentation.ts
Normal file
5
src/instrumentation.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import '@/lib/customConsoleLog'
|
||||||
|
|
||||||
|
export function register() {
|
||||||
|
console.log('hello from instrumentation.ts!')
|
||||||
|
}
|
||||||
23
src/lib/configLoader.ts
Normal file
23
src/lib/configLoader.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
'server'
|
||||||
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
import { AppConfig, defaultConfig } from '@/config/AppConfig'
|
||||||
|
|
||||||
|
const configFilePath = path.join(process.cwd(), 'runtimeConfig.json')
|
||||||
|
|
||||||
|
function loadConfig(): AppConfig {
|
||||||
|
let config = defaultConfig
|
||||||
|
|
||||||
|
if (fs.existsSync(configFilePath)) {
|
||||||
|
const raw = fs.readFileSync(configFilePath, 'utf-8')
|
||||||
|
const parsed = JSON.parse(raw)
|
||||||
|
config = { ...defaultConfig, ...parsed }
|
||||||
|
} else {
|
||||||
|
fs.mkdirSync(path.dirname(configFilePath), { recursive: true })
|
||||||
|
}
|
||||||
|
fs.writeFileSync(configFilePath, JSON.stringify(defaultConfig, null, 4))
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
export const appConfig = loadConfig()
|
||||||
13
src/lib/customConsoleLog.ts
Normal file
13
src/lib/customConsoleLog.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
let _custom_console_log_injected = false
|
||||||
|
if (!_custom_console_log_injected) {
|
||||||
|
_custom_console_log_injected = true
|
||||||
|
const originalLog = console.log
|
||||||
|
|
||||||
|
function customLog(message?: any, ...optionalParams: any[]) {
|
||||||
|
const now = new Date()
|
||||||
|
const timestamp = now.toTimeString().split(' ')[0] // hh:mm:ss
|
||||||
|
originalLog(`[${timestamp}] ${message}`, ...optionalParams, ':3')
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log = customLog
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//TODO: controllable rate limits
|
||||||
|
|
||||||
class AuthService {
|
class AuthService {
|
||||||
private API_BASE = '/api/auth'
|
private API_BASE = '/api/auth'
|
||||||
|
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { PROXY_PATHS } from '@/app/api/proxy/paths'
|
||||||
|
|
||||||
export interface UploadSaveResponse {
|
export interface UploadSaveResponse {
|
||||||
id: string
|
id: string
|
||||||
}
|
}
|
||||||
@@ -10,20 +12,29 @@ export interface SaveStatusResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class SaveParserBackendService {
|
export class SaveParserBackendService {
|
||||||
private API_BASE = 'http://localhost:5226'
|
private API_BASE = PROXY_PATHS.saveParserBackend
|
||||||
|
|
||||||
|
private async tryGetResponseJson(res: Response): Promise<string> {
|
||||||
|
try {
|
||||||
|
const body = await res.json()
|
||||||
|
return JSON.stringify(body)
|
||||||
|
} catch {}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: save size limit 30 mb
|
||||||
async uploadSave(game: string, file: File): Promise<UploadSaveResponse> {
|
async uploadSave(game: string, file: File): Promise<UploadSaveResponse> {
|
||||||
const url = `${this.API_BASE}/uploadSave?game=${encodeURIComponent(game)}`
|
const url = `${this.API_BASE}/uploadSave?game=${encodeURIComponent(game)}`
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'text/plain',
|
'Content-Type': 'application/octet-stream',
|
||||||
},
|
},
|
||||||
body: file,
|
body: file,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to upload save: ${response.statusText} \n${await response.text()}`)
|
throw new Error(`Failed to upload save: ${response.statusText} ${await this.tryGetResponseJson(response)}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return await response.json()
|
return await response.json()
|
||||||
@@ -34,7 +45,9 @@ export class SaveParserBackendService {
|
|||||||
const response = await fetch(url)
|
const response = await fetch(url)
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to get save status: ${response.statusText} \n${await response.text()}`)
|
throw new Error(
|
||||||
|
`Failed to get save status: ${response.statusText} ${await this.tryGetResponseJson(response)}`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return await response.json()
|
return await response.json()
|
||||||
@@ -45,7 +58,9 @@ export class SaveParserBackendService {
|
|||||||
const response = await fetch(url)
|
const response = await fetch(url)
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to get parsed save: ${response.statusText} \n${await response.text()}`)
|
throw new Error(
|
||||||
|
`Failed to get parsed save: ${response.statusText} ${await this.tryGetResponseJson(response)}`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return await response.json()
|
return await response.json()
|
||||||
|
|||||||
Reference in New Issue
Block a user