Skip to content
Merged
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
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"java.compile.nullAnalysis.mode": "automatic"
"java.compile.nullAnalysis.mode": "automatic",
"java.configuration.updateBuildConfiguration": "automatic"
}
7 changes: 7 additions & 0 deletions bin/main/application.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
spring.application.name=schemalab
spring.datasource.url=${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/schemalab}
spring.datasource.username=${SPRING_DATASOURCE_USERNAME:schemalab}
spring.datasource.password=${SPRING_DATASOURCE_PASSWORD:schemalab}
spring.datasource.driver-class-name=org.postgresql.Driver
spring.jpa.hibernate.ddl-auto=update
server.port=8080
7 changes: 3 additions & 4 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,18 @@ repositories {
}

dependencies {
implementation("org.springframework.boot:spring-boot-h2console")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-webmvc")
runtimeOnly("org.postgresql:postgresql")

compileOnly("org.projectlombok:lombok:1.18.46")
annotationProcessor("org.projectlombok:lombok:1.18.46")

testCompileOnly("org.projectlombok:lombok:1.18.46")
testAnnotationProcessor("org.projectlombok:lombok:1.18.46")


runtimeOnly("com.h2database:h2")
testRuntimeOnly("com.h2database:h2")
testImplementation("org.springframework.boot:spring-boot-starter-data-jpa-test")
testImplementation("org.springframework.boot:spring-boot-starter-security-test")
testImplementation("org.springframework.boot:spring-boot-starter-webmvc-test")
Expand Down
26 changes: 26 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,31 @@
services:
postgres:
image: postgres:17
environment:
POSTGRES_DB: schemalab
POSTGRES_USER: schemalab
POSTGRES_PASSWORD: schemalab
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U schemalab"]
interval: 5s
timeout: 5s
retries: 5

backend:
build: .
ports:
- "8080:8080"
environment:
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/schemalab
SPRING_DATASOURCE_USERNAME: schemalab
SPRING_DATASOURCE_PASSWORD: schemalab
depends_on:
postgres:
condition: service_healthy

runner:
build: ./runner
Expand All @@ -14,3 +37,6 @@ services:
depends_on:
- backend
- runner

volumes:
postgres_data:
30 changes: 24 additions & 6 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,35 @@
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
import { AuthProvider, useAuth } from './context/AuthContext'
import ChallengeListPage from './pages/ChallengeListPage'
import ChallengePage from './pages/ChallengePage'
import AboutPage from './pages/AboutPage'
import LoginPage from './pages/LoginPage'
import RegisterPage from './pages/RegisterPage'

function ProtectedRoute({ children }) {
const { user } = useAuth()
return user ? children : <Navigate to="/login" replace />
}

function AppRoutes() {
return (
<Routes>
<Route path="/" element={<Navigate to="/challenges" replace />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} />
<Route path="/challenges" element={<ProtectedRoute><ChallengeListPage /></ProtectedRoute>} />
<Route path="/challenges/:id" element={<ProtectedRoute><ChallengePage /></ProtectedRoute>} />
<Route path="/about" element={<AboutPage />} />
</Routes>
)
}

export default function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Navigate to="/challenges" replace />} />
<Route path="/challenges" element={<ChallengeListPage />} />
<Route path="/challenges/:id" element={<ChallengePage />} />
<Route path="/about" element={<AboutPage />} />
</Routes>
<AuthProvider>
<AppRoutes />
</AuthProvider>
</BrowserRouter>
)
}
13 changes: 8 additions & 5 deletions frontend/src/api/axios.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import axios from 'axios'

const api = axios.create({
baseURL: '/api',
headers: {
Authorization: 'Basic ' + btoa('dev:dev'),
},
const api = axios.create({ baseURL: '/api' })

api.interceptors.request.use(config => {
const credentials = localStorage.getItem('auth_credentials')
if (credentials) {
config.headers.Authorization = `Basic ${credentials}`
}
return config
})

export default api
49 changes: 49 additions & 0 deletions frontend/src/components/NavBar.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Link, useNavigate } from 'react-router-dom'
import { useAuth } from '../context/AuthContext'

export default function NavBar() {
const { user, logout } = useAuth()
const navigate = useNavigate()

function handleLogout() {
logout()
navigate('/login')
}

return (
<nav style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '0 20px',
height: 48,
borderBottom: '1px solid #2d3748',
flexShrink: 0,
}}>
<Link to="/challenges" style={{ fontWeight: 700, fontSize: 15, color: '#f7fafc' }}>
SchemaLab
</Link>
<div style={{ display: 'flex', alignItems: 'center', gap: 20 }}>
<Link to="/about" style={{ fontSize: 13, color: '#718096' }}>About</Link>
{user && (
<>
<span style={{ fontSize: 13, color: '#a0aec0' }}>{user.username}</span>
<button
onClick={handleLogout}
style={{
fontSize: 13,
color: '#718096',
background: 'none',
border: 'none',
cursor: 'pointer',
padding: 0,
}}
>
Logout
</button>
</>
)}
</div>
</nav>
)
}
43 changes: 43 additions & 0 deletions frontend/src/context/AuthContext.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { createContext, useContext, useState } from 'react'
import api from '../api/axios'

const AuthContext = createContext(null)

export function AuthProvider({ children }) {
const [user, setUser] = useState(() => {
const stored = localStorage.getItem('auth_user')
return stored ? JSON.parse(stored) : null
})

async function login(username, password) {
const credentials = btoa(`${username}:${password}`)
const res = await api.post('/auth/login', null, {
headers: { Authorization: `Basic ${credentials}` },
})
localStorage.setItem('auth_credentials', credentials)
localStorage.setItem('auth_user', JSON.stringify(res.data))
setUser(res.data)
return res.data
}

async function register(username, password) {
await api.post('/auth/register', { username, password })
return login(username, password)
}

function logout() {
localStorage.removeItem('auth_credentials')
localStorage.removeItem('auth_user')
setUser(null)
}

return (
<AuthContext.Provider value={{ user, login, logout, register }}>
{children}
</AuthContext.Provider>
)
}

export function useAuth() {
return useContext(AuthContext)
}
9 changes: 7 additions & 2 deletions frontend/src/pages/AboutPage.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import NavBar from '../components/NavBar'

export default function AboutPage() {
return (
<div style={{ maxWidth: 720, margin: '0 auto', padding: '40px 16px' }}>
<>
<NavBar />
<div style={{ maxWidth: 720, margin: '0 auto', padding: '40px 16px' }}>
<h1 style={{ fontSize: 28, fontWeight: 700, marginBottom: 8, color: '#f7fafc' }}>
About SchemaLab
</h1>
Expand Down Expand Up @@ -85,6 +89,7 @@ export default function AboutPage() {
))}
</div>
</section>
</div>
</div>
</>
)
}
86 changes: 42 additions & 44 deletions frontend/src/pages/ChallengeListPage.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react'
import { Link } from 'react-router-dom'
import api from '../api/axios'
import NavBar from '../components/NavBar'

const DIFFICULTY_COLOR = {
EASY: '#68d391',
Expand All @@ -24,52 +25,49 @@ export default function ChallengeListPage() {
}, [])

return (
<div style={{ maxWidth: 800, margin: '0 auto', padding: '40px 16px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 8 }}>
<h1 style={{ fontSize: 28, fontWeight: 700, color: '#f7fafc' }}>
SchemaLab
</h1>
<Link to="/about" style={{ fontSize: 13, color: '#718096' }}>About</Link>
</div>
<p style={{ color: '#a0aec0', marginBottom: 32 }}>
Practice schema matching and schema versioning challenges.
</p>
<>
<NavBar />
<div style={{ maxWidth: 800, margin: '0 auto', padding: '40px 16px' }}>
<p style={{ color: '#a0aec0', marginBottom: 32 }}>
Practice schema matching and schema versioning challenges.
</p>

{error && <p style={{ color: '#fc8181' }}>{error}</p>}
{error && <p style={{ color: '#fc8181' }}>{error}</p>}

<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ borderBottom: '1px solid #2d3748', color: '#718096', fontSize: 13, textAlign: 'left' }}>
<th style={{ padding: '8px 12px' }}>#</th>
<th style={{ padding: '8px 12px' }}>Title</th>
<th style={{ padding: '8px 12px' }}>Type</th>
<th style={{ padding: '8px 12px' }}>Difficulty</th>
</tr>
</thead>
<tbody>
{challenges.map((c, i) => (
<tr
key={c.id}
style={{ borderBottom: '1px solid #1a202c' }}
>
<td style={{ padding: '12px 12px', color: '#718096', fontSize: 14 }}>{i + 1}</td>
<td style={{ padding: '12px 12px' }}>
<Link to={`/challenges/${c.id}`} style={{ fontWeight: 500 }}>
{c.title}
</Link>
</td>
<td style={{ padding: '12px 12px', color: '#a0aec0', fontSize: 14 }}>
{TYPE_LABEL[c.type] ?? c.type}
</td>
<td style={{ padding: '12px 12px', fontSize: 14 }}>
<span style={{ color: DIFFICULTY_COLOR[c.difficulty] ?? '#e2e8f0' }}>
{c.difficulty}
</span>
</td>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ borderBottom: '1px solid #2d3748', color: '#718096', fontSize: 13, textAlign: 'left' }}>
<th style={{ padding: '8px 12px' }}>#</th>
<th style={{ padding: '8px 12px' }}>Title</th>
<th style={{ padding: '8px 12px' }}>Type</th>
<th style={{ padding: '8px 12px' }}>Difficulty</th>
</tr>
))}
</tbody>
</table>
</div>
</thead>
<tbody>
{challenges.map((c, i) => (
<tr
key={c.id}
style={{ borderBottom: '1px solid #1a202c' }}
>
<td style={{ padding: '12px 12px', color: '#718096', fontSize: 14 }}>{i + 1}</td>
<td style={{ padding: '12px 12px' }}>
<Link to={`/challenges/${c.id}`} style={{ fontWeight: 500 }}>
{c.title}
</Link>
</td>
<td style={{ padding: '12px 12px', color: '#a0aec0', fontSize: 14 }}>
{TYPE_LABEL[c.type] ?? c.type}
</td>
<td style={{ padding: '12px 12px', fontSize: 14 }}>
<span style={{ color: DIFFICULTY_COLOR[c.difficulty] ?? '#e2e8f0' }}>
{c.difficulty}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</>
)
}
6 changes: 5 additions & 1 deletion frontend/src/pages/ChallengePage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from 'react'
import { useParams, Link } from 'react-router-dom'
import Editor from '@monaco-editor/react'
import api from '../api/axios'
import NavBar from '../components/NavBar'

function sortKeys(val) {
if (Array.isArray(val)) return val.map(sortKeys)
Expand Down Expand Up @@ -144,7 +145,9 @@ export default function ChallengePage() {
if (!challenge) return <div style={{ padding: 40, color: '#a0aec0' }}>Loading…</div>

return (
<div style={{ display: 'flex', height: '100vh', overflow: 'hidden' }}>
<div style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
<NavBar />
<div style={{ display: 'flex', flex: 1, overflow: 'hidden' }}>
{/* Left column */}
<div style={{
width: '60%',
Expand Down Expand Up @@ -327,6 +330,7 @@ export default function ChallengePage() {
)}
</div>
</div>
</div>
</div>
)
}
Expand Down
Loading
Loading