Initial commit
This commit is contained in:
31
api/Dockerfile
Normal file
31
api/Dockerfile
Normal file
@@ -0,0 +1,31 @@
|
||||
# Multi-stage build for Hono API
|
||||
FROM node:20-alpine AS base
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies
|
||||
FROM base AS deps
|
||||
COPY package.json ./
|
||||
RUN npm install
|
||||
|
||||
# Build TypeScript
|
||||
FROM base AS builder
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# Production image
|
||||
FROM base AS runner
|
||||
ENV NODE_ENV=production
|
||||
|
||||
# Copy built files and production dependencies
|
||||
COPY --from=builder /app/dist ./dist
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY package.json ./
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --retries=3 --start-period=40s \
|
||||
CMD node -e "require('http').get('http://127.0.0.1:3000/health', (r) => r.statusCode === 200 ? process.exit(0) : process.exit(1))"
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["npm", "start"]
|
||||
20
api/package.json
Normal file
20
api/package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "astro-hono-api",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"hono": "^4.0.0",
|
||||
"@hono/node-server": "^1.8.0",
|
||||
"@directus/sdk": "^17.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.0",
|
||||
"tsx": "^4.7.0",
|
||||
"typescript": "^5.3.0"
|
||||
}
|
||||
}
|
||||
189
api/src/index.ts
Normal file
189
api/src/index.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
import { serve } from '@hono/node-server'
|
||||
import { Hono } from 'hono'
|
||||
import { cors } from 'hono/cors'
|
||||
import {
|
||||
getPosts,
|
||||
getPost,
|
||||
getProfile,
|
||||
getWorkProjects,
|
||||
getWorkProject,
|
||||
getSkills,
|
||||
getSocialLinks
|
||||
} from './lib/directus.js'
|
||||
|
||||
const app = new Hono()
|
||||
|
||||
// CORS for frontend
|
||||
app.use('/*', cors({
|
||||
origin: [
|
||||
'http://astro-hono.homelab.local',
|
||||
'http://localhost:4321',
|
||||
'https://b28.dev',
|
||||
'https://www.b28.dev'
|
||||
],
|
||||
credentials: true,
|
||||
}))
|
||||
|
||||
// Health check
|
||||
app.get('/health', (c) => {
|
||||
return c.json({
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
service: 'astro-hono-api'
|
||||
})
|
||||
})
|
||||
|
||||
// Root endpoint
|
||||
app.get('/', (c) => {
|
||||
return c.json({
|
||||
message: 'Astro-Hono API with Directus CMS',
|
||||
version: '1.0.0',
|
||||
endpoints: {
|
||||
health: '/health',
|
||||
greeting: '/api/greeting',
|
||||
user: '/api/user/:name',
|
||||
posts: '/api/posts',
|
||||
post: '/api/posts/:id',
|
||||
profile: '/api/profile',
|
||||
workProjects: '/api/work-projects',
|
||||
workProject: '/api/work-projects/:slug',
|
||||
skills: '/api/skills',
|
||||
socialLinks: '/api/social-links'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// API routes
|
||||
app.get('/api/greeting', (c) => {
|
||||
const time = new Date().getHours()
|
||||
let greeting = 'Hello'
|
||||
|
||||
if (time < 12) greeting = 'Good morning'
|
||||
else if (time < 18) greeting = 'Good afternoon'
|
||||
else greeting = 'Good evening'
|
||||
|
||||
return c.json({
|
||||
greeting,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
})
|
||||
|
||||
app.get('/api/user/:name', (c) => {
|
||||
const name = c.req.param('name')
|
||||
return c.json({
|
||||
name,
|
||||
message: `Hello, ${name}!`,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
})
|
||||
|
||||
// Directus CMS routes
|
||||
app.get('/api/posts', async (c) => {
|
||||
try {
|
||||
const posts = await getPosts()
|
||||
return c.json({
|
||||
data: posts,
|
||||
count: posts.length
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching posts:', error)
|
||||
return c.json({ error: 'Failed to fetch posts' }, 500)
|
||||
}
|
||||
})
|
||||
|
||||
app.get('/api/posts/:id', async (c) => {
|
||||
try {
|
||||
const id = parseInt(c.req.param('id'))
|
||||
if (isNaN(id)) {
|
||||
return c.json({ error: 'Invalid post ID' }, 400)
|
||||
}
|
||||
const post = await getPost(id)
|
||||
return c.json({ data: post })
|
||||
} catch (error) {
|
||||
console.error('Error fetching post:', error)
|
||||
return c.json({ error: 'Post not found' }, 404)
|
||||
}
|
||||
})
|
||||
|
||||
// Portfolio routes
|
||||
app.get('/api/profile', async (c) => {
|
||||
try {
|
||||
const profile = await getProfile()
|
||||
return c.json({ data: profile })
|
||||
} catch (error) {
|
||||
console.error('Error fetching profile:', error)
|
||||
return c.json({ error: 'Failed to fetch profile' }, 500)
|
||||
}
|
||||
})
|
||||
|
||||
app.get('/api/work-projects', async (c) => {
|
||||
try {
|
||||
const workProjects = await getWorkProjects()
|
||||
return c.json({
|
||||
data: workProjects,
|
||||
count: workProjects.length
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching work projects:', error)
|
||||
return c.json({ error: 'Failed to fetch work projects' }, 500)
|
||||
}
|
||||
})
|
||||
|
||||
app.get('/api/work-projects/:slug', async (c) => {
|
||||
try {
|
||||
const slug = c.req.param('slug')
|
||||
const project = await getWorkProject(slug)
|
||||
if (!project) {
|
||||
return c.json({ error: 'Work project not found' }, 404)
|
||||
}
|
||||
return c.json({ data: project })
|
||||
} catch (error) {
|
||||
console.error('Error fetching work project:', error)
|
||||
return c.json({ error: 'Work project not found' }, 404)
|
||||
}
|
||||
})
|
||||
|
||||
app.get('/api/skills', async (c) => {
|
||||
try {
|
||||
const skills = await getSkills()
|
||||
return c.json({
|
||||
data: skills,
|
||||
count: skills.length
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching skills:', error)
|
||||
return c.json({ error: 'Failed to fetch skills' }, 500)
|
||||
}
|
||||
})
|
||||
|
||||
app.get('/api/social-links', async (c) => {
|
||||
try {
|
||||
const socialLinks = await getSocialLinks()
|
||||
return c.json({
|
||||
data: socialLinks,
|
||||
count: socialLinks.length
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching social links:', error)
|
||||
return c.json({ error: 'Failed to fetch social links' }, 500)
|
||||
}
|
||||
})
|
||||
|
||||
// 404 handler
|
||||
app.notFound((c) => {
|
||||
return c.json({ error: 'Not found' }, 404)
|
||||
})
|
||||
|
||||
// Error handler
|
||||
app.onError((err, c) => {
|
||||
console.error(`Error: ${err.message}`)
|
||||
return c.json({ error: 'Internal server error' }, 500)
|
||||
})
|
||||
|
||||
const port = 3000
|
||||
console.log(`🚀 API server running on port ${port}`)
|
||||
|
||||
serve({
|
||||
fetch: app.fetch,
|
||||
port
|
||||
})
|
||||
196
api/src/lib/directus.ts
Normal file
196
api/src/lib/directus.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
import { createDirectus, rest, readItems, readItem, readSingleton } from '@directus/sdk'
|
||||
|
||||
// Directus schema types
|
||||
interface Post {
|
||||
id: number
|
||||
title: string
|
||||
content: string
|
||||
published: boolean
|
||||
date_created: string
|
||||
date_updated: string | null
|
||||
}
|
||||
|
||||
interface Profile {
|
||||
id: number
|
||||
full_name: string
|
||||
first_name: string
|
||||
last_name: string
|
||||
title: string
|
||||
description: string
|
||||
email: string
|
||||
location?: string
|
||||
portfolio_year?: string
|
||||
availability?: boolean
|
||||
availability_text?: string
|
||||
current_role_title?: string
|
||||
current_role_company?: string
|
||||
current_role_duration?: string
|
||||
work_section_title?: string
|
||||
work_section_date_range?: string
|
||||
connect_title?: string
|
||||
connect_description?: string
|
||||
footer_copyright?: string
|
||||
footer_attribution?: string
|
||||
footer_year?: string
|
||||
}
|
||||
|
||||
interface WorkProject {
|
||||
id: number
|
||||
status: string
|
||||
title: string
|
||||
slug: string
|
||||
company: string
|
||||
role: string
|
||||
year: string
|
||||
duration?: string
|
||||
description: string
|
||||
content?: string
|
||||
technologies: string[]
|
||||
featured: boolean
|
||||
order?: number
|
||||
team?: string
|
||||
live_url?: string
|
||||
case_study_url?: string
|
||||
date_created?: string
|
||||
date_updated?: string | null
|
||||
}
|
||||
|
||||
interface Skill {
|
||||
id: number
|
||||
name: string
|
||||
category?: string
|
||||
proficiency?: string
|
||||
order?: number
|
||||
featured: boolean
|
||||
}
|
||||
|
||||
interface SocialLink {
|
||||
id: number
|
||||
name: string
|
||||
handle: string
|
||||
url: string
|
||||
icon?: string
|
||||
order?: number
|
||||
visible: boolean
|
||||
}
|
||||
|
||||
interface Schema {
|
||||
posts: Post[]
|
||||
profile: Profile
|
||||
work_projects: WorkProject[]
|
||||
skills: Skill[]
|
||||
social_links: SocialLink[]
|
||||
}
|
||||
|
||||
// Directus client configuration
|
||||
const directusUrl = process.env.DIRECTUS_URL || 'http://directus:8055'
|
||||
|
||||
export const directus = createDirectus<Schema>(directusUrl).with(rest())
|
||||
|
||||
// Helper functions for common queries
|
||||
|
||||
// Posts
|
||||
export const getPosts = async () => {
|
||||
try {
|
||||
return await directus.request(
|
||||
readItems('posts', {
|
||||
filter: {
|
||||
published: { _eq: true }
|
||||
},
|
||||
sort: ['-date_created'],
|
||||
limit: 10
|
||||
})
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Error fetching posts:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export const getPost = async (id: number) => {
|
||||
try {
|
||||
return await directus.request(readItem('posts', id))
|
||||
} catch (error) {
|
||||
console.error(`Error fetching post ${id}:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Profile (singleton)
|
||||
export const getProfile = async () => {
|
||||
try {
|
||||
return await directus.request(readSingleton('profile'))
|
||||
} catch (error) {
|
||||
console.error('Error fetching profile:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Work Projects
|
||||
export const getWorkProjects = async () => {
|
||||
try {
|
||||
return await directus.request(
|
||||
readItems('work_projects', {
|
||||
filter: {
|
||||
status: { _eq: 'published' }
|
||||
},
|
||||
sort: ['order'],
|
||||
limit: -1
|
||||
})
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Error fetching work projects:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export const getWorkProject = async (slug: string) => {
|
||||
try {
|
||||
const results = await directus.request(
|
||||
readItems('work_projects', {
|
||||
filter: {
|
||||
slug: { _eq: slug },
|
||||
status: { _eq: 'published' }
|
||||
},
|
||||
limit: 1
|
||||
})
|
||||
)
|
||||
return results[0] || null
|
||||
} catch (error) {
|
||||
console.error(`Error fetching work project ${slug}:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Skills
|
||||
export const getSkills = async () => {
|
||||
try {
|
||||
return await directus.request(
|
||||
readItems('skills', {
|
||||
sort: ['order'],
|
||||
limit: -1
|
||||
})
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Error fetching skills:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Social Links
|
||||
export const getSocialLinks = async () => {
|
||||
try {
|
||||
return await directus.request(
|
||||
readItems('social_links', {
|
||||
filter: {
|
||||
visible: { _eq: true }
|
||||
},
|
||||
sort: ['order'],
|
||||
limit: -1
|
||||
})
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Error fetching social links:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
19
api/tsconfig.json
Normal file
19
api/tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2022"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user