Skip to content
Draft
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
90 changes: 89 additions & 1 deletion apps/admin-backend/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { serve } from '@hono/node-server'
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { config } from 'dotenv'
import { drizzle } from 'drizzle-orm/postgres-js'
import { scenarioTable } from './infrastructure/database/schema.js'
Expand All @@ -10,10 +11,16 @@ config({ path: '.env' })

const client = postgres(process.env.DATABASE_URL!, { prepare: false })
const db = drizzle(client)
// const db = drizzle(process.env.DATABASE_URL!)

const app = new Hono()

// Enable CORS for admin frontend
app.use('*', cors({
origin: process.env.ADMIN_FRONTEND_URL || 'http://localhost:5173',
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowHeaders: ['Content-Type', 'Authorization'],
}))

app.get('/json-schema', async (c) => {
const schema = await db.select().from(scenarioTable)
const jsonSchema = schema[0]
Expand Down Expand Up @@ -60,6 +67,17 @@ app.post('/api/scenarios/publish', async (c) => {
}
})

// Get all scenarios (list)
app.get('/api/scenarios', async (c) => {
try {
const { desc } = await import('drizzle-orm')
const scenarios = await db.select().from(scenarioTable).orderBy(desc(scenarioTable.updatedAt))
return c.json(scenarios)
} catch (error: any) {
return c.json({ error: error.message }, 500)
}
})

// Get scenario by key
app.get('/api/scenarios/:key', async (c) => {
try {
Expand All @@ -77,6 +95,76 @@ app.get('/api/scenarios/:key', async (c) => {
}
})

// Update scenario by key
app.put('/api/scenarios/:key', async (c) => {
try {
const key = c.req.param('key')
const updates = await c.req.json()
const { eq } = await import('drizzle-orm')

// Check if scenario exists
const existing = await db.select().from(scenarioTable).where(eq(scenarioTable.key, key)).limit(1)
if (existing.length === 0) {
return c.json({ error: 'Scenario not found' }, 404)
}

// Update the scenario
const result = await db.update(scenarioTable)
.set({
...updates,
updatedAt: new Date(),
buildNumber: existing[0].buildNumber + 1, // Increment build number
})
.where(eq(scenarioTable.key, key))
.returning()

return c.json(result[0])
} catch (error: any) {
return c.json({ error: error.message }, 500)
}
})

// Delete scenario by key
app.delete('/api/scenarios/:key', async (c) => {
try {
const key = c.req.param('key')
const { eq } = await import('drizzle-orm')

// Check if scenario exists
const existing = await db.select().from(scenarioTable).where(eq(scenarioTable.key, key)).limit(1)
if (existing.length === 0) {
return c.json({ error: 'Scenario not found' }, 404)
}

await db.delete(scenarioTable).where(eq(scenarioTable.key, key))
return c.json({ message: 'Scenario deleted successfully' })
} catch (error: any) {
return c.json({ error: error.message }, 500)
}
})

// Validate JSX code without transpiling
app.post('/api/scenarios/validate', async (c) => {
try {
const { jsxCode } = await c.req.json()

if (!jsxCode) {
return c.json({ error: 'jsxCode is required' }, 400)
}

// Try to transpile to validate
await transpile(jsxCode)

return c.json({ valid: true, message: 'JSX code is valid' })
} catch (error: any) {
return c.json({
valid: false,
message: 'JSX code is invalid',
error: error.message
}, 400)
}
})

serve(
{
fetch: app.fetch,
Expand Down
5 changes: 5 additions & 0 deletions apps/admin/src/components/layout/data/sidebar-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@ export const sidebarData: SidebarData = {
url: '/editor',
icon: IconCode,
},
{
title: 'Scenarios',
url: '/scenarios',
icon: IconCode,
},
{
title: 'Avito Design System',
url: '/avito-design-system',
Expand Down
2 changes: 2 additions & 0 deletions apps/admin/src/components/ui/badge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ const badgeVariants = cva(
secondary: 'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90',
destructive:
'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
success:
'border-transparent bg-green-600 text-white [a&]:hover:bg-green-600/90 focus-visible:ring-green-600/20 dark:focus-visible:ring-green-600/40',
outline: 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground',
},
},
Expand Down
134 changes: 134 additions & 0 deletions apps/admin/src/features/scenarios/api/scenarios.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import type {
Scenario,
CreateScenarioData,
UpdateScenarioData,
CompileResult,
ValidationResult
} from '../types'

const API_BASE = 'http://localhost:3050/api'

export const scenarioService = {
async getAll(): Promise<Scenario[]> {
const response = await fetch(`${API_BASE}/scenarios`)
if (!response.ok) {
throw new Error('Failed to fetch scenarios')
}
return response.json()
},

async getByKey(key: string): Promise<Scenario> {
const response = await fetch(`${API_BASE}/scenarios/${encodeURIComponent(key)}`)
if (!response.ok) {
throw new Error('Failed to fetch scenario')
}
return response.json()
},

async create(data: CreateScenarioData): Promise<Scenario> {
// First compile the JSX code
const compiled = await this.compile(data.jsxCode)

// Then publish the compiled scenario
const response = await fetch(`${API_BASE}/scenarios/publish`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
key: data.key || compiled.key,
main: compiled.main,
components: compiled.components,
version: compiled.version,
metadata: {
...data.metadata,
jsxCode: data.jsxCode,
},
}),
})

if (!response.ok) {
const error = await response.json()
throw new Error(error.error || 'Failed to create scenario')
}

return response.json()
},

async update(key: string, data: UpdateScenarioData): Promise<Scenario> {
let updates: any = {
metadata: data.metadata,
}

// If JSX code is being updated, compile it first
if (data.jsxCode) {
const compiled = await this.compile(data.jsxCode)
updates = {
...updates,
mainComponent: compiled.main,
components: compiled.components,
metadata: {
...data.metadata,
jsxCode: data.jsxCode,
},
}
}

const response = await fetch(`${API_BASE}/scenarios/${encodeURIComponent(key)}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(updates),
})

if (!response.ok) {
const error = await response.json()
throw new Error(error.error || 'Failed to update scenario')
}

return response.json()
},

async delete(key: string): Promise<void> {
const response = await fetch(`${API_BASE}/scenarios/${encodeURIComponent(key)}`, {
method: 'DELETE',
})

if (!response.ok) {
const error = await response.json()
throw new Error(error.error || 'Failed to delete scenario')
}
},

async compile(jsxCode: string): Promise<CompileResult> {
const response = await fetch(`${API_BASE}/scenarios/compile`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ jsxCode }),
})

if (!response.ok) {
const error = await response.json()
throw new Error(error.error || 'Failed to compile JSX')
}

return response.json()
},

async validate(jsxCode: string): Promise<ValidationResult> {
const response = await fetch(`${API_BASE}/scenarios/validate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ jsxCode }),
})

// Note: validation endpoint returns 400 for invalid code
const result = await response.json()
return result
},
}
Loading