Initial commit

This commit is contained in:
2025-10-22 10:23:49 -07:00
commit ed6cf5e2af
78 changed files with 14686 additions and 0 deletions

31
api/Dockerfile Normal file
View 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
View 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
View 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
View 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
View 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"]
}