228 lines
No EOL
7.1 KiB
Vue
228 lines
No EOL
7.1 KiB
Vue
<template>
|
|
<div class="max-w-4xl mx-auto">
|
|
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-6">Create New Article</h1>
|
|
|
|
<form @submit.prevent="createArticle" class="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-8">
|
|
<!-- Title -->
|
|
<div class="mb-6">
|
|
<label for="title" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
Title
|
|
</label>
|
|
<input
|
|
v-model="form.title"
|
|
id="title"
|
|
type="text"
|
|
required
|
|
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
|
placeholder="Article title"
|
|
>
|
|
</div>
|
|
|
|
<!-- Slug -->
|
|
<div class="mb-6">
|
|
<label for="slug" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
URL Slug
|
|
</label>
|
|
<input
|
|
v-model="form.slug"
|
|
id="slug"
|
|
type="text"
|
|
required
|
|
pattern="[a-z0-9-]+"
|
|
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
|
placeholder="article-url-slug"
|
|
>
|
|
<p class="text-sm text-gray-500 mt-1">Lowercase letters, numbers, and hyphens only</p>
|
|
</div>
|
|
|
|
<!-- Description -->
|
|
<div class="mb-6">
|
|
<label for="description" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
Description
|
|
</label>
|
|
<textarea
|
|
v-model="form.description"
|
|
id="description"
|
|
rows="2"
|
|
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
|
placeholder="Brief description of the article"
|
|
></textarea>
|
|
</div>
|
|
|
|
<!-- Category -->
|
|
<div class="mb-6">
|
|
<label for="category" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
Category
|
|
</label>
|
|
<select
|
|
v-model="form.category"
|
|
id="category"
|
|
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
|
>
|
|
<option value="operations">Operations</option>
|
|
<option value="funding">Funding</option>
|
|
<option value="strategy">Strategy</option>
|
|
<option value="templates">Templates</option>
|
|
<option value="programs">Programs</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Access Level -->
|
|
<div class="mb-6">
|
|
<label for="accessLevel" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
Access Level
|
|
</label>
|
|
<select
|
|
v-model="form.accessLevel"
|
|
id="accessLevel"
|
|
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
|
>
|
|
<option value="public">Public</option>
|
|
<option value="member">Members Only</option>
|
|
<option value="cohort">Cohort Only</option>
|
|
<option value="admin" v-if="hasPermission('canAdmin')">Admin Only</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Content -->
|
|
<div class="mb-6">
|
|
<label for="content" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
Content (Markdown)
|
|
</label>
|
|
<textarea
|
|
v-model="form.content"
|
|
id="content"
|
|
rows="20"
|
|
required
|
|
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white font-mono text-sm"
|
|
placeholder="Write your article in Markdown..."
|
|
></textarea>
|
|
</div>
|
|
|
|
<!-- Tags -->
|
|
<div class="mb-6">
|
|
<label for="tags" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
Tags (comma-separated)
|
|
</label>
|
|
<input
|
|
v-model="tagsInput"
|
|
id="tags"
|
|
type="text"
|
|
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
|
placeholder="tag1, tag2, tag3"
|
|
>
|
|
</div>
|
|
|
|
<!-- Status -->
|
|
<div class="mb-6">
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
Status
|
|
</label>
|
|
<div class="flex gap-4">
|
|
<label class="flex items-center">
|
|
<input
|
|
v-model="form.status"
|
|
type="radio"
|
|
value="draft"
|
|
class="mr-2"
|
|
>
|
|
<span class="text-gray-700 dark:text-gray-300">Save as Draft</span>
|
|
</label>
|
|
<label class="flex items-center">
|
|
<input
|
|
v-model="form.status"
|
|
type="radio"
|
|
value="published"
|
|
class="mr-2"
|
|
>
|
|
<span class="text-gray-700 dark:text-gray-300">Publish Now</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Error Message -->
|
|
<div v-if="error" class="mb-6 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
|
|
<p class="text-red-800 dark:text-red-200">{{ error }}</p>
|
|
</div>
|
|
|
|
<!-- Submit Buttons -->
|
|
<div class="flex gap-4">
|
|
<button
|
|
type="submit"
|
|
:disabled="loading"
|
|
class="bg-blue-600 text-white px-6 py-2 rounded-md hover:bg-blue-700 disabled:opacity-50"
|
|
>
|
|
{{ loading ? 'Creating...' : 'Create Article' }}
|
|
</button>
|
|
<NuxtLink to="/articles" class="bg-gray-300 dark:bg-gray-600 text-gray-700 dark:text-gray-300 px-6 py-2 rounded-md hover:bg-gray-400 dark:hover:bg-gray-500">
|
|
Cancel
|
|
</NuxtLink>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
const { hasPermission, isAuthenticated } = useAuth()
|
|
const router = useRouter()
|
|
|
|
// Redirect if not authenticated
|
|
if (!isAuthenticated.value) {
|
|
navigateTo('/api/auth/login', { external: true })
|
|
}
|
|
|
|
const form = ref({
|
|
title: '',
|
|
slug: '',
|
|
description: '',
|
|
category: 'operations',
|
|
accessLevel: 'member',
|
|
content: '',
|
|
status: 'draft'
|
|
})
|
|
|
|
const tagsInput = ref('')
|
|
const loading = ref(false)
|
|
const error = ref('')
|
|
|
|
// Auto-generate slug from title
|
|
watch(() => form.value.title, (title) => {
|
|
if (!form.value.slug || form.value.slug === slugify(title.slice(0, -1))) {
|
|
form.value.slug = slugify(title)
|
|
}
|
|
})
|
|
|
|
function slugify(text) {
|
|
return text
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, '-')
|
|
.replace(/^-+|-+$/g, '')
|
|
}
|
|
|
|
async function createArticle() {
|
|
loading.value = true
|
|
error.value = ''
|
|
|
|
try {
|
|
const tags = tagsInput.value
|
|
.split(',')
|
|
.map(t => t.trim())
|
|
.filter(t => t)
|
|
|
|
const response = await $fetch('/api/articles', {
|
|
method: 'POST',
|
|
body: {
|
|
...form.value,
|
|
tags
|
|
}
|
|
})
|
|
|
|
// Redirect to the new article
|
|
await router.push(`/articles/${response.slug}`)
|
|
} catch (err) {
|
|
error.value = err.data?.message || 'Failed to create article'
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
</script> |