Compare commits

..

3 Commits

Author SHA1 Message Date
Altair-sh
95880ca080 changed style of SaveFileUploadingDialog 2025-05-23 02:57:49 +05:00
Altair-sh
42adeb290c added some //TODO: 2025-05-23 02:04:15 +05:00
Altair-sh
8c270fc4ea fixed bugs 2025-05-23 01:51:50 +05:00
17 changed files with 117 additions and 52 deletions

1
.gitignore vendored
View File

@@ -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

View File

@@ -8,7 +8,7 @@ export async function redirectRequest(
proxyUrl: string, proxyUrl: string,
backendBaseUrl: string backendBaseUrl: string
): Promise<NextResponse> { ): Promise<NextResponse> {
const reqUrl = req.url // full URL e.g. http://localhost:3000/api/proxy?id=123&foo=bar const reqUrl = req.url // full URL e.g. http://localhost:3000/api/proxy?id=123
// Find position of proxyPath in reqUrl // Find position of proxyPath in reqUrl
const proxyIndex = reqUrl.indexOf(proxyUrl) const proxyIndex = reqUrl.indexOf(proxyUrl)
@@ -17,9 +17,9 @@ export async function redirectRequest(
} }
// Replace '/api/proxy/parserBackend' with backend URL, preserve everything after // Replace '/api/proxy/parserBackend' with backend URL, preserve everything after
// e.g. '/api/proxy/extra/path?id=123' -> 'http://your-backend-server/extra/path?id=123' // e.g. '/api/proxy/extra/path?id=123' -> 'http://backend-server/extra/path?id=123'
const backendUrl = backendBaseUrl + reqUrl.substring(proxyIndex + proxyUrl.length) const backendUrl = backendBaseUrl + reqUrl.substring(proxyIndex + proxyUrl.length)
console.log('redirecting', req.url, 'to', backendUrl) // console.log('redirecting', req.url, 'to', backendUrl)
const backendRes = await fetch(backendUrl, { const backendRes = await fetch(backendUrl, {
method: req.method, method: req.method,
@@ -41,7 +41,7 @@ export async function redirectRequest(
/// Browsers send OPTIONS request sometimes to check webserver allowed headers and methods. /// 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 /// 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> { export async function responseToOPTIONS(req: NextRequest): Promise<NextResponse> {
console.log('proxy responding to CORS OPTIONS', req.url) // console.log('proxy responding to CORS OPTIONS', req.url)
return new NextResponse(null, { return new NextResponse(null, {
status: 200, status: 200,

View File

@@ -1,8 +1,8 @@
import type { NextRequest } from 'next/server' import type { NextRequest } from 'next/server'
import { NextResponse } from 'next/server' import { NextResponse } from 'next/server'
import { redirectRequest, responseToOPTIONS as respondToOPTIONS } from '@/app/api/proxy/proxyFunctions' import { redirectRequest, responseToOPTIONS as respondToOPTIONS } from '@/app/api/proxy/functions'
import { appConfig } from '@/app/lib/configLoader' import { PROXY_PATHS } from '@/app/api/proxy/paths'
import { PROXY_PATHS } from '../../paths' import { appConfig } from '@/lib/configLoader'
async function redirect(req: NextRequest): Promise<NextResponse> { async function redirect(req: NextRequest): Promise<NextResponse> {
return await redirectRequest(req, PROXY_PATHS.saveParserBackend, appConfig.services.saveParserBackend.url) return await redirectRequest(req, PROXY_PATHS.saveParserBackend, appConfig.services.saveParserBackend.url)

View File

@@ -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)

View File

@@ -15,7 +15,6 @@ html,
body { body {
max-width: 100vw; max-width: 100vw;
overflow-x: hidden; overflow-x: hidden;
padding: 10px;
} }
body { body {

View File

@@ -1,6 +1,5 @@
import 'bootstrap/dist/css/bootstrap.min.css' import 'bootstrap/dist/css/bootstrap.min.css'
import './globals.css' import './globals.css'
import '@/app/lib/customConsoleLog'
import type { Metadata } from 'next' import type { Metadata } from 'next'
import { ModuleRegistry, AllCommunityModule } from 'ag-grid-community' import { ModuleRegistry, AllCommunityModule } from 'ag-grid-community'
import Navbar from '@/components/Navbar' import Navbar from '@/components/Navbar'

View File

@@ -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)

View File

@@ -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)

View File

@@ -1,7 +1,7 @@
'use client' 'use client'
import Link from 'next/link' import Link from 'next/link'
import { font_Hack } from '@/app/lib/myFonts' import { font_Hack } from '@/lib/myFonts'
export default function Navbar() { export default function Navbar() {
return ( return (

View File

@@ -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>
) )
} }

View File

@@ -1 +0,0 @@
/runtimeConfig.json

View File

@@ -4,6 +4,7 @@ export const defaultConfig = {
saveParserBackend: { saveParserBackend: {
url: 'http://127.0.0.1:5226', url: 'http://127.0.0.1:5226',
}, },
// TODO: implement auth api
auth: {}, auth: {},
}, },
} }

5
src/instrumentation.ts Normal file
View File

@@ -0,0 +1,5 @@
import '@/lib/customConsoleLog'
export function register() {
console.log('hello from instrumentation.ts!')
}

View File

@@ -1,9 +1,9 @@
'server' 'server'
import fs from 'fs' import fs from 'fs'
import path from 'path' import path from 'path'
import { AppConfig, defaultConfig } from '@/config/defaultConfig' import { AppConfig, defaultConfig } from '@/config/AppConfig'
const configFilePath = path.join(process.cwd(), 'config', 'runtimeConfig.json') const configFilePath = path.join(process.cwd(), 'runtimeConfig.json')
function loadConfig(): AppConfig { function loadConfig(): AppConfig {
let config = defaultConfig let config = defaultConfig

View File

@@ -1,8 +1,13 @@
'server' let _custom_console_log_injected = false
const originalLog = console.log if (!_custom_console_log_injected) {
_custom_console_log_injected = true
const originalLog = console.log
console.log = (message?: any, ...optionalParams: any[]) => { function customLog(message?: any, ...optionalParams: any[]) {
const now = new Date() const now = new Date()
const timestamp = now.toTimeString().split(' ')[0] // hh:mm:ss const timestamp = now.toTimeString().split(' ')[0] // hh:mm:ss
originalLog(`[${timestamp}] ${message}`, ...optionalParams) originalLog(`[${timestamp}] ${message}`, ...optionalParams, ':3')
}
console.log = customLog
} }

View File

@@ -1,3 +1,5 @@
//TODO: controllable rate limits
class AuthService { class AuthService {
private API_BASE = '/api/auth' private API_BASE = '/api/auth'

View File

@@ -1,3 +1,5 @@
import { PROXY_PATHS } from '@/app/api/proxy/paths'
export interface UploadSaveResponse { export interface UploadSaveResponse {
id: string id: string
} }
@@ -10,7 +12,7 @@ export interface SaveStatusResponse {
} }
export class SaveParserBackendService { export class SaveParserBackendService {
private API_BASE = '/api/proxy/parserBackend' private API_BASE = PROXY_PATHS.saveParserBackend
private async tryGetResponseJson(res: Response): Promise<string> { private async tryGetResponseJson(res: Response): Promise<string> {
try { try {
@@ -20,6 +22,7 @@ export class SaveParserBackendService {
return '' 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, {