feat: bootstrap OSDB full-stack skeleton (story #01)
- Go backend: Echo v4 + pgxpool + embedded golang-migrate; health endpoints at /health and /api/v1/health; TDD health handler (httptest) - Vue 3 frontend: Vite + TypeScript + Pinia + vue-router + Tailwind CSS v4; TDD HelloWorld component (@vue/test-utils + jsdom + vitest) - Infra: docker-compose postgres:16 with env-interpolated credentials; Makefile with dev-db health-wait loop, migrate-up/down, run, test, fmt - embed.FS migrations at backend/migrations/ (000001 no-op baseline) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { RouterView } from 'vue-router'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RouterView />
|
||||
</template>
|
||||
@@ -0,0 +1,8 @@
|
||||
// getHealth calls the backend health endpoint via the Vite proxy (/api → :3000).
|
||||
export async function getHealth(): Promise<{ status: string }> {
|
||||
const res = await fetch('/api/v1/health')
|
||||
if (!res.ok) {
|
||||
throw new Error(`health check failed: ${res.status}`)
|
||||
}
|
||||
return res.json()
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1 @@
|
||||
@import "tailwindcss";
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 8.5 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 496 B |
@@ -0,0 +1,7 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import router from './router'
|
||||
import './assets/main.css'
|
||||
import App from './App.vue'
|
||||
|
||||
createApp(App).use(createPinia()).use(router).mount('#app')
|
||||
@@ -0,0 +1,14 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import HelloWorld from '../views/HelloWorld.vue'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
component: HelloWorld,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,14 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
// appStore: global application state stub.
|
||||
// Extended by later stories (auth, fruit, publication stores).
|
||||
export const useAppStore = defineStore('app', () => {
|
||||
const ready = ref(false)
|
||||
|
||||
function setReady(value: boolean) {
|
||||
ready.value = value
|
||||
}
|
||||
|
||||
return { ready, setReady }
|
||||
})
|
||||
@@ -0,0 +1,23 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia } from 'pinia'
|
||||
import HelloWorld from './HelloWorld.vue'
|
||||
|
||||
// Stub fetch so the component's onMounted health call doesn't hit the network.
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({ status: 'ok' }),
|
||||
}))
|
||||
})
|
||||
|
||||
describe('HelloWorld', () => {
|
||||
it('renders the OSDB greeting', async () => {
|
||||
const wrapper = mount(HelloWorld, {
|
||||
global: {
|
||||
plugins: [createPinia()],
|
||||
},
|
||||
})
|
||||
expect(wrapper.text()).toContain('Hello, OSDB!')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { getHealth } from '../api/health'
|
||||
|
||||
const backendStatus = ref<string | null>(null)
|
||||
const backendError = ref<string | null>(null)
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const data = await getHealth()
|
||||
backendStatus.value = data.status
|
||||
} catch (err) {
|
||||
backendError.value = err instanceof Error ? err.message : 'unknown error'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="flex flex-col items-center justify-center min-h-screen gap-4">
|
||||
<h1 class="text-4xl font-bold">Hello, OSDB!</h1>
|
||||
|
||||
<div v-if="backendStatus" class="text-green-600">
|
||||
Backend: {{ backendStatus }}
|
||||
</div>
|
||||
<div v-else-if="backendError" class="text-red-600">
|
||||
Backend unreachable: {{ backendError }}
|
||||
</div>
|
||||
<div v-else class="text-gray-400">
|
||||
Checking backend…
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
Reference in New Issue
Block a user