first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:43:40 +03:00
commit f34e54c5a5
100 changed files with 27342 additions and 0 deletions

403
views/admin/blog/blog.templ Normal file
View File

@@ -0,0 +1,403 @@
package blog
import (
"gobeyhan/database/models"
"fmt"
)
templ List(posts []models.Post) {
@Layout("Blog Posts") {
<div class="px-4 py-6 sm:px-0">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-semibold text-gray-900">Blog Posts</h1>
<a href="/admin/blog/new" class="bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700">Add Post</a>
</div>
<div class="bg-white shadow overflow-hidden sm:rounded-lg">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">ID</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Title</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Categories</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Created</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
for _, post := range posts {
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{ fmt.Sprintf("%d", post.ID) }</td>
<td class="px-6 py-4 text-sm font-medium text-gray-900">
<div class="max-w-xs truncate">{ post.Title }</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
if len(post.Categories) > 0 {
for i, cat := range post.Categories {
if i > 0 {
<span>, </span>
}
<span class="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">{ cat.Title }</span>
}
} else {
<span class="text-gray-400">-</span>
}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
if post.IsActive {
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">Active</span>
} else {
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800">Inactive</span>
}
if post.IsFront {
<span class="ml-1 px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-purple-100 text-purple-800">Front</span>
}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{ post.CreatedAt.Format("2006-01-02") }
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<a href={ templ.SafeURL(fmt.Sprintf("/admin/blog/%d/edit", post.ID)) } class="text-indigo-600 hover:text-indigo-900 mr-4">Edit</a>
<form id={ fmt.Sprintf("delete-blog-%d", post.ID) } action={ templ.SafeURL(fmt.Sprintf("/admin/blog/%d/delete", post.ID)) } method="POST" class="inline">
<button type="button" class="text-red-600 hover:text-red-900" onclick={ templ.ComponentScript{ Call: fmt.Sprintf("confirmDelete('Delete Post', 'Are you sure you want to delete this post?', 'delete-blog-%d')", post.ID) } }>Delete</button>
</form>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
}
}
templ Create(categories []models.Category, tags []models.Tag, errors map[string]string) {
@Layout("Create Blog Post") {
<div class="px-4 py-6 sm:px-0">
<div class="max-w-4xl mx-auto">
<h1 class="text-2xl font-semibold text-gray-900 mb-6">Create New Post</h1>
<form id="blog-form" action="/admin/blog" method="POST" enctype="multipart/form-data" class="space-y-6 bg-white p-6 rounded-lg shadow">
<div>
<label for="title" class="block text-sm font-medium text-gray-700">Title *</label>
<input type="text" name="title" id="title" required
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border" />
if errors["title"] != "" {
<p class="mt-2 text-sm text-red-600">{ errors["title"] }</p>
}
</div>
<div>
<label for="slug" class="block text-sm font-medium text-gray-700">Slug</label>
<input type="text" name="slug" id="slug"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border" />
<p class="mt-1 text-xs text-gray-400 italic">Leave blank to generate automatically from title.</p>
</div>
<div>
<label for="content" class="block text-sm font-medium text-gray-700">Content *</label>
<div id="editor" class="mt-1 bg-white h-64 border rounded-md"></div>
<input type="hidden" name="content" id="content-input" required />
if errors["content"] != "" {
<p class="mt-2 text-sm text-red-600">{ errors["content"] }</p>
}
</div>
<div>
<label for="keywords" class="block text-sm font-medium text-gray-700">Keywords</label>
<input type="text" name="keywords" id="keywords"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border" />
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label for="image_file" class="block text-sm font-medium text-gray-700">Upload Image</label>
<input type="file" name="image_file" id="image_file" accept="image/*"
class="mt-1 block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-indigo-50 file:text-indigo-700 hover:file:bg-indigo-100" />
</div>
<div>
<label for="image" class="block text-sm font-medium text-gray-700">Manual Image Path (optional)</label>
<input type="text" name="image" id="image"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border" />
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Categories</label>
<div class="grid grid-cols-2 gap-2">
if len(categories) > 0 {
for _, cat := range categories {
<label class="flex items-center">
<input type="checkbox" name="category_ids" value={ fmt.Sprintf("%d", cat.ID) }
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" />
<span class="ml-2 text-sm text-gray-700">{ cat.Title }</span>
</label>
}
} else {
<p class="text-xs text-gray-500 italic col-span-2">No active categories found. <a href="/admin/blog/categories/new" class="text-indigo-600 underline">Add one first.</a></p>
}
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Tags</label>
<div class="grid grid-cols-3 gap-2">
if len(tags) > 0 {
for _, tag := range tags {
<label class="flex items-center">
<input type="checkbox" name="tag_ids" value={ fmt.Sprintf("%d", tag.ID) }
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" />
<span class="ml-2 text-sm text-gray-700">{ tag.Tag }</span>
</label>
}
} else {
<p class="text-xs text-gray-500 italic col-span-3">No active tags found. <a href="/admin/blog/tags/new" class="text-indigo-600 underline">Add one first.</a></p>
}
</div>
</div>
<div class="flex items-center space-x-4">
<label class="flex items-center">
<input type="checkbox" name="is_active" checked
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" />
<span class="ml-2 text-sm text-gray-700">Active</span>
</label>
<label class="flex items-center">
<input type="checkbox" name="is_front" checked
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" />
<span class="ml-2 text-sm text-gray-700">Show on Front Page</span>
</label>
</div>
<div class="flex justify-end gap-3">
<a href="/admin/blog" class="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50">Cancel</a>
<button type="submit" class="bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700">Create Post</button>
</div>
</form>
<script>
autoSlug('title', 'slug');
function initQuill() {
if (typeof Quill === 'undefined') {
setTimeout(initQuill, 100);
return;
}
const editorEl = document.querySelector('#editor');
if (!editorEl || editorEl.classList.contains('ql-container')) return;
const quill = new Quill('#editor', {
theme: 'snow',
modules: {
toolbar: [
[{ 'header': [1, 2, 3, false] }],
['bold', 'italic', 'underline', 'strike'],
['link', 'image', 'video'],
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
['clean']
]
}
});
const form = editorEl.closest('form');
if (form) {
const syncContent = () => {
const contentInput = document.querySelector('#content-input');
if (contentInput) contentInput.value = quill.root.innerHTML;
};
form.addEventListener('submit', syncContent);
form.addEventListener('htmx:configRequest', (event) => {
syncContent();
if (event.detail.parameters) {
event.detail.parameters['content'] = quill.root.innerHTML;
}
});
}
}
initQuill();
</script>
</div>
</div>
}
}
templ Edit(post *models.Post, categories []models.Category, tags []models.Tag, errors map[string]string) {
@Layout("Edit Blog Post") {
<div class="px-4 py-6 sm:px-0">
<div class="max-w-4xl mx-auto">
<h1 class="text-2xl font-semibold text-gray-900 mb-6">Edit Post: { post.Title }</h1>
<form id="blog-form-edit" action={ templ.SafeURL(fmt.Sprintf("/admin/blog/%d", post.ID)) } method="POST" enctype="multipart/form-data" class="space-y-6 bg-white p-6 rounded-lg shadow">
<div>
<label for="title" class="block text-sm font-medium text-gray-700">Title *</label>
<input type="text" name="title" id="title" value={ post.Title } required
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border" />
if errors["title"] != "" {
<p class="mt-2 text-sm text-red-600">{ errors["title"] }</p>
}
</div>
<div>
<label for="slug" class="block text-sm font-medium text-gray-700">Slug</label>
<input type="text" name="slug" id="slug" value={ post.Slug }
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border" />
<p class="mt-1 text-xs text-gray-400 italic">Leave blank to keep existing or regenerate from title.</p>
</div>
<div>
<label for="content" class="block text-sm font-medium text-gray-700">Content *</label>
<div id="editor" class="mt-1 bg-white h-64 border rounded-md">@templ.Raw(post.Content)</div>
<input type="hidden" name="content" id="content-input" value={ post.Content } required />
if errors["content"] != "" {
<p class="mt-2 text-sm text-red-600">{ errors["content"] }</p>
}
</div>
<div>
<label for="keywords" class="block text-sm font-medium text-gray-700">Keywords</label>
<input type="text" name="keywords" id="keywords" value={ post.Keywords }
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border" />
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label for="image_file" class="block text-sm font-medium text-gray-700">Update Image</label>
<input type="file" name="image_file" id="image_file" accept="image/*"
class="mt-1 block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-indigo-50 file:text-indigo-700 hover:file:bg-indigo-100" />
if post.Image != "" {
<div class="mt-2">
<p class="text-xs text-gray-500 mb-1">Current Preview:</p>
<img src={ post.Image } class="h-20 w-auto rounded border shadow-sm" />
</div>
}
</div>
<div>
<label for="image" class="block text-sm font-medium text-gray-700">Current Image Path</label>
<input type="text" name="image" id="image" value={ post.Image }
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border" />
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Categories</label>
<div class="grid grid-cols-2 gap-2">
for _, cat := range categories {
<label class="flex items-center">
<input type="checkbox" name="category_ids" value={ fmt.Sprintf("%d", cat.ID) }
if isCategorySelected(post.Categories, cat.ID) {
checked
}
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" />
<span class="ml-2 text-sm text-gray-700">{ cat.Title }</span>
</label>
}
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Tags</label>
<div class="grid grid-cols-3 gap-2">
for _, tag := range tags {
<label class="flex items-center">
<input type="checkbox" name="tag_ids" value={ fmt.Sprintf("%d", tag.ID) }
if isTagSelected(post.Tags, tag.ID) {
checked
}
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" />
<span class="ml-2 text-sm text-gray-700">{ tag.Tag }</span>
</label>
}
</div>
</div>
<div class="flex items-center space-x-4">
<label class="flex items-center">
<input type="checkbox" name="is_active"
if post.IsActive {
checked
}
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" />
<span class="ml-2 text-sm text-gray-700">Active</span>
</label>
<label class="flex items-center">
<input type="checkbox" name="is_front"
if post.IsFront {
checked
}
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" />
<span class="ml-2 text-sm text-gray-700">Show on Front Page</span>
</label>
</div>
<div class="flex justify-end gap-3">
<a href="/admin/blog" class="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50">Cancel</a>
<button type="submit" class="bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700">Save Changes</button>
</div>
</form>
<script>
autoSlug('title', 'slug');
function initQuillEdit() {
if (typeof Quill === 'undefined') {
setTimeout(initQuillEdit, 100);
return;
}
const editorEl = document.querySelector('#editor');
if (!editorEl || editorEl.classList.contains('ql-container')) return;
const quill = new Quill('#editor', {
theme: 'snow',
modules: {
toolbar: [
[{ 'header': [1, 2, 3, false] }],
['bold', 'italic', 'underline', 'strike'],
['link', 'image', 'video'],
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
['clean']
]
}
});
const form = editorEl.closest('form');
if (form) {
const syncContent = () => {
const contentInput = document.querySelector('#content-input');
if (contentInput) contentInput.value = quill.root.innerHTML;
};
form.addEventListener('submit', syncContent);
form.addEventListener('htmx:configRequest', (event) => {
syncContent();
if (event.detail.parameters) {
event.detail.parameters['content'] = quill.root.innerHTML;
}
});
}
}
initQuillEdit();
</script>
</div>
</div>
}
}
func isCategorySelected(categories []*models.Category, id uint64) bool {
for _, cat := range categories {
if cat.ID == id {
return true
}
}
return false
}
func isTagSelected(tags []*models.Tag, id uint64) bool {
for _, tag := range tags {
if tag.ID == id {
return true
}
}
return false
}