Import source: app/src/lib/content-api
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
@@ -0,0 +1,115 @@
|
|||||||
|
import { api } from './api.js';
|
||||||
|
import { TeknavData } from '../../teknav-data.js';
|
||||||
|
|
||||||
|
function toQuery(params = {}) {
|
||||||
|
const query = new URLSearchParams();
|
||||||
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
|
if (value !== undefined && value !== null && value !== '') query.set(key, String(value));
|
||||||
|
});
|
||||||
|
const s = query.toString();
|
||||||
|
return s ? `?${s}` : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const articleCache = new Map();
|
||||||
|
|
||||||
|
export const contentApi = {
|
||||||
|
async listArticles(params = {}) {
|
||||||
|
const res = await api.get(`/api/articles${toQuery(params)}`);
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
|
||||||
|
async getArticle(slug) {
|
||||||
|
if (articleCache.has(slug)) {
|
||||||
|
return articleCache.get(slug);
|
||||||
|
}
|
||||||
|
const res = await api.get(`/api/articles/${encodeURIComponent(slug)}`);
|
||||||
|
articleCache.set(slug, res);
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
|
||||||
|
async getRelated(slug) {
|
||||||
|
return api.get(`/api/articles/${encodeURIComponent(slug)}/related`);
|
||||||
|
},
|
||||||
|
|
||||||
|
async listSeries() {
|
||||||
|
const res = await api.get('/api/series');
|
||||||
|
return res?.items ?? [];
|
||||||
|
},
|
||||||
|
|
||||||
|
async getSeries(slug) {
|
||||||
|
const res = await api.get(`/api/series/${encodeURIComponent(slug)}`);
|
||||||
|
return res?.series;
|
||||||
|
},
|
||||||
|
|
||||||
|
async listNewsletterArchive() {
|
||||||
|
const res = await api.get('/api/newsletter/archive');
|
||||||
|
return res?.items ?? [];
|
||||||
|
},
|
||||||
|
|
||||||
|
async getNewsletterIssue(slug) {
|
||||||
|
const res = await api.get(`/api/newsletter/archive/${encodeURIComponent(slug)}`);
|
||||||
|
return res?.campaign;
|
||||||
|
},
|
||||||
|
|
||||||
|
async search(params = {}) {
|
||||||
|
return api.get(`/api/search${toQuery(params)}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
async listCategories() {
|
||||||
|
const res = await api.get('/api/categories');
|
||||||
|
return res?.items ?? [];
|
||||||
|
},
|
||||||
|
|
||||||
|
async getCategory(slug) {
|
||||||
|
const res = await api.get(`/api/categories/${encodeURIComponent(slug)}`);
|
||||||
|
return res?.category;
|
||||||
|
},
|
||||||
|
|
||||||
|
async listAuthors() {
|
||||||
|
const res = await api.get('/api/authors');
|
||||||
|
return res?.items ?? [];
|
||||||
|
},
|
||||||
|
|
||||||
|
async getAuthor(slug) {
|
||||||
|
const res = await api.get(`/api/authors/${encodeURIComponent(slug)}`);
|
||||||
|
return res?.author;
|
||||||
|
},
|
||||||
|
|
||||||
|
async listJobs() {
|
||||||
|
const res = await api.get('/api/jobs');
|
||||||
|
return res?.items ?? [];
|
||||||
|
},
|
||||||
|
|
||||||
|
submitJob(payload) {
|
||||||
|
return api.post('/api/jobs', payload);
|
||||||
|
},
|
||||||
|
|
||||||
|
async listCourses() {
|
||||||
|
const res = await api.get('/api/courses');
|
||||||
|
return res?.items ?? [];
|
||||||
|
},
|
||||||
|
|
||||||
|
subscribeNewsletter(email, captchaId, userSolution) {
|
||||||
|
return api.post('/api/newsletter', { email, captchaId, userSolution });
|
||||||
|
},
|
||||||
|
|
||||||
|
trackEvent(payload) {
|
||||||
|
return api.post('/api/analytics/events', payload);
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleReaction(articleId, type) {
|
||||||
|
return api.post(`/api/articles/${encodeURIComponent(articleId)}/reactions`, { type });
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleSaved(articleId) {
|
||||||
|
return api.post(`/api/articles/${encodeURIComponent(articleId)}/save`, {});
|
||||||
|
},
|
||||||
|
|
||||||
|
fallbackArticles() {
|
||||||
|
return TeknavData.articles ?? [];
|
||||||
|
},
|
||||||
|
|
||||||
|
fallbackAuthors() {
|
||||||
|
return TeknavData.authors ?? [];
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
|
||||||
|
vi.mock('./api.js', () => ({
|
||||||
|
api: {
|
||||||
|
get: vi.fn(),
|
||||||
|
post: vi.fn(),
|
||||||
|
},
|
||||||
|
ApiError: class extends Error {
|
||||||
|
constructor(s, b) { super(); this.status = s; this.body = b; }
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { contentApi } from './content-api.js';
|
||||||
|
import { api } from './api.js';
|
||||||
|
|
||||||
|
describe('contentApi', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('listArticles calls GET /api/articles', async () => {
|
||||||
|
api.get.mockResolvedValue({ items: [{ slug: 'test' }], total: 1 });
|
||||||
|
const res = await contentApi.listArticles({ limit: 10 });
|
||||||
|
expect(api.get).toHaveBeenCalledWith('/api/articles?limit=10');
|
||||||
|
expect(res.items).toEqual([{ slug: 'test' }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getArticle calls GET /api/articles/:slug', async () => {
|
||||||
|
api.get.mockResolvedValue({ article: { slug: 'test', title: 'Test' } });
|
||||||
|
const res = await contentApi.getArticle('test-unique-slug-1');
|
||||||
|
expect(api.get).toHaveBeenCalledWith('/api/articles/test-unique-slug-1');
|
||||||
|
expect(res.article.slug).toBe('test');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getArticle caches results', async () => {
|
||||||
|
api.get.mockClear();
|
||||||
|
api.get.mockResolvedValue({ article: { slug: 'cache-test-slug-999' } });
|
||||||
|
await contentApi.getArticle('cache-test-slug-999');
|
||||||
|
const callCount = api.get.mock.calls.length;
|
||||||
|
await contentApi.getArticle('cache-test-slug-999');
|
||||||
|
expect(api.get.mock.calls.length).toBe(callCount);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('search calls GET /api/search with query', async () => {
|
||||||
|
api.get.mockResolvedValue({ items: [] });
|
||||||
|
await contentApi.search({ q: 'هوش مصنوعی' });
|
||||||
|
expect(api.get).toHaveBeenCalledWith('/api/search?q=%D9%87%D9%88%D8%B4+%D9%85%D8%B5%D9%86%D9%88%D8%B9%DB%8C');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('listCategories calls GET /api/categories', async () => {
|
||||||
|
api.get.mockResolvedValue({ items: [{ slug: 'ai', name: 'هوش مصنوعی' }] });
|
||||||
|
const res = await contentApi.listCategories();
|
||||||
|
expect(res[0].slug).toBe('ai');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('toggleReaction calls POST with article ID and type', async () => {
|
||||||
|
api.post.mockResolvedValue({ ok: true });
|
||||||
|
await contentApi.toggleReaction('art123', 'insightful');
|
||||||
|
expect(api.post).toHaveBeenCalledWith('/api/articles/art123/reactions', { type: 'insightful' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('subscribeNewsletter sends email and captcha data', async () => {
|
||||||
|
api.post.mockResolvedValue({ ok: true });
|
||||||
|
await contentApi.subscribeNewsletter('test@example.com', 'cap-id', 'ABC');
|
||||||
|
expect(api.post).toHaveBeenCalledWith('/api/newsletter', {
|
||||||
|
email: 'test@example.com',
|
||||||
|
captchaId: 'cap-id',
|
||||||
|
userSolution: 'ABC',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fallbackArticles returns array from TeknavData', () => {
|
||||||
|
const result = contentApi.fallbackArticles();
|
||||||
|
expect(Array.isArray(result)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fallbackAuthors returns array from TeknavData', () => {
|
||||||
|
const result = contentApi.fallbackAuthors();
|
||||||
|
expect(Array.isArray(result)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user