add simple admin service
Build and deploy / deploy (push) Successful in 16s

This commit is contained in:
2026-05-18 02:48:47 +03:00
parent 10a83d39a2
commit 3a3b2bcfc0
84 changed files with 2134 additions and 5 deletions
+38
View File
@@ -0,0 +1,38 @@
import { bindMarkdownToolbar } from '../features/markdown-editor/formatting.js';
import { estimateReadingMinutes } from '../shared/lib/markdown.js';
import { slugify } from '../shared/lib/slug.js';
export function bindEvents(context) {
context.form.addEventListener('input', () => {
context.setDirty(true);
context.renderPreview();
});
context.searchInput.addEventListener('input', context.renderArticles);
context.newButton.addEventListener('click', context.createNewArticle);
context.duplicateButton.addEventListener('click', context.duplicateArticle);
context.saveButton.addEventListener('click', context.saveArticle);
context.deleteButton.addEventListener('click', context.deleteArticle);
context.coverUploadButton.addEventListener('click', () => context.coverUploadInput.click());
context.coverUploadInput.addEventListener('change', context.uploadCover);
context.assetUploadButton.addEventListener('click', () => context.assetUploadInput.click());
context.assetUploadInput.addEventListener('change', context.uploadMarkdownAssets);
bindMarkdownToolbar({
field: context.fields.contentMarkdown,
onChange() {
context.setDirty(true);
context.renderPreview();
},
});
context.slugButton.addEventListener('click', () => {
context.fields.slug.value = slugify(context.fields.title.value);
context.setDirty(true);
});
context.minutesButton.addEventListener('click', () => {
context.fields.readingMinutes.value = estimateReadingMinutes(context.fields.contentMarkdown.value);
context.setDirty(true);
});
}
+15
View File
@@ -0,0 +1,15 @@
import { getArticles } from '../entities/article/api.js';
export async function bootstrap(context) {
context.setStatus('Загрузка данных...');
try {
const [articles] = await Promise.all([getArticles(), context.loadAssets()]);
context.state.articles = articles;
context.renderArticles();
context.selectArticle(context.state.articles[0]?.slug || null);
context.setStatus(`Загружено ${context.state.articles.length} статей`);
} catch (error) {
context.setStatus(error.message);
}
}
@@ -0,0 +1,50 @@
import { getAssets } from '../entities/asset/api.js';
import { deleteArticle } from '../features/article-actions/deleteArticle.js';
import { duplicateArticle } from '../features/article-actions/duplicateArticle.js';
import { createNewArticle } from '../features/article-actions/createNewArticle.js';
import { reloadArticles } from '../features/article-actions/reloadArticles.js';
import { renderArticles } from '../features/article-actions/renderArticles.js';
import { saveArticle } from '../features/article-actions/saveArticle.js';
import { selectArticle } from '../features/article-actions/selectArticle.js';
import { updateArticleButtons } from '../features/article-actions/buttons.js';
import { mergeAssetPaths } from '../features/asset-picker/mergeAssets.js';
import { renderAssetOptions } from '../features/asset-picker/renderAssetOptions.js';
import { uploadCover } from '../features/asset-upload/uploadCover.js';
import { uploadMarkdownAssets } from '../features/asset-upload/uploadMarkdownAssets.js';
import { renderPreview } from '../features/preview-panel/renderPreview.js';
import { confirmDiscard, setDirty } from '../features/session-state/dirtyState.js';
import { setStatus } from '../features/session-state/status.js';
import { createAdminState } from './model/createAdminState.js';
export function createAdminContext(dom) {
const context = {
...dom,
state: createAdminState(),
};
context.confirmDiscard = () => confirmDiscard(context.state);
context.deleteArticle = () => deleteArticle(context);
context.duplicateArticle = () => duplicateArticle(context);
context.createNewArticle = () => createNewArticle(context);
context.loadAssets = async () => {
context.state.assets = await getAssets();
context.renderAssetOptions();
};
context.mergeUploadedAssets = (uploaded) => {
context.state.assets = mergeAssetPaths(context.state.assets, uploaded);
context.renderAssetOptions();
};
context.reloadArticles = (selectedSlug) => reloadArticles(context, selectedSlug);
context.renderArticles = () => renderArticles(context, context.selectArticle);
context.renderAssetOptions = () => renderAssetOptions(context.assetOptions, context.state.assets);
context.renderPreview = () => renderPreview(context);
context.saveArticle = () => saveArticle(context);
context.selectArticle = (slug, options) => selectArticle(slug, context, options);
context.setDirty = (dirty) => setDirty({ dirty, saveButton: context.saveButton, state: context.state });
context.setStatus = (message) => setStatus(context.statusText, message);
context.updateButtons = () => updateArticleButtons(context);
context.uploadCover = () => uploadCover(context);
context.uploadMarkdownAssets = () => uploadMarkdownAssets(context);
return context;
}
+39
View File
@@ -0,0 +1,39 @@
export function getAdminDom() {
const form = document.querySelector('#article-form');
return {
articleList: document.querySelector('#article-list'),
assetOptions: document.querySelector('#asset-options'),
assetUploadButton: document.querySelector('#asset-upload-button'),
assetUploadInput: document.querySelector('#asset-upload-input'),
contentPreview: document.querySelector('#content-preview'),
coverPreview: document.querySelector('#cover-preview'),
coverUploadButton: document.querySelector('#cover-upload-button'),
coverUploadInput: document.querySelector('#cover-upload-input'),
deleteButton: document.querySelector('#delete-button'),
duplicateButton: document.querySelector('#duplicate-button'),
form,
fields: getArticleFields(form),
minutesButton: document.querySelector('#minutes-button'),
newButton: document.querySelector('#new-button'),
openArticleLink: document.querySelector('#open-article-link'),
saveButton: document.querySelector('#save-button'),
searchInput: document.querySelector('#search-input'),
slugButton: document.querySelector('#slug-button'),
statusText: document.querySelector('#status-text'),
};
}
function getArticleFields(form) {
return {
author: form.elements.author,
categories: form.elements.categories,
contentMarkdown: form.elements.contentMarkdown,
cover: form.elements.cover,
date: form.elements.date,
excerpt: form.elements.excerpt,
readingMinutes: form.elements.readingMinutes,
slug: form.elements.slug,
title: form.elements.title,
};
}
+11
View File
@@ -0,0 +1,11 @@
import { protectUnsavedChanges } from '../features/session-state/dirtyState.js';
import { bootstrap } from './bootstrap.js';
import { bindEvents } from './bindEvents.js';
import { createAdminContext } from './createAdminContext.js';
import { getAdminDom } from './dom.js';
const context = createAdminContext(getAdminDom());
bindEvents(context);
protectUnsavedChanges(context.state);
await bootstrap(context);
@@ -0,0 +1,9 @@
export function createAdminState() {
return {
articles: [],
assets: [],
current: null,
dirty: false,
selectedSlug: null,
};
}