From 88b716e2c62b91a755f1513ddb06da64dbdcbc1a Mon Sep 17 00:00:00 2001 From: arsam Date: Fri, 18 Sep 2026 23:24:40 +0330 Subject: [PATCH] Import source: app/src/lib/engagement-api Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality. --- app/src/lib/engagement-api.js | 242 ++++++++++++++++++++++++++++++++++ 1 file changed, 242 insertions(+) create mode 100644 app/src/lib/engagement-api.js diff --git a/app/src/lib/engagement-api.js b/app/src/lib/engagement-api.js new file mode 100644 index 0000000..ae4affa --- /dev/null +++ b/app/src/lib/engagement-api.js @@ -0,0 +1,242 @@ +import { api } from './api.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}` : ''; +} + +export const engagementApi = { + listComments(slug, params) { + return api.get(`/api/articles/${encodeURIComponent(slug)}/comments${toQuery(params)}`); + }, + postComment(slug, body, replyToId) { + return api.post(`/api/articles/${encodeURIComponent(slug)}/comments`, { body, replyToId }); + }, + flagComment(id) { + return api.post(`/api/comments/${encodeURIComponent(id)}/flag`, {}); + }, + likeComment(id) { + return api.post(`/api/comments/${encodeURIComponent(id)}/like`, {}); + }, + upvoteComment(id) { + return api.post(`/api/comments/${encodeURIComponent(id)}/upvote`, {}); + }, + recordHistory(articleId, progress = 0) { + return api.post('/api/auth/history', { articleId, progress }); + }, + getStreaks() { + return api.get('/api/auth/streaks'); + }, + listHistory(params) { + return api.get(`/api/auth/history${toQuery(params)}`); + }, + clearHistory() { + return api.del('/api/auth/history'); + }, + listNotifications(params) { + return api.get(`/api/auth/notifications${toQuery(params)}`); + }, + markNotificationsRead() { + return api.post('/api/auth/notifications/read-all', {}); + }, + markNotificationRead(id) { + return api.patch(`/api/auth/notifications/${encodeURIComponent(id)}/read`, {}); + }, + deleteNotification(id) { + return api.del(`/api/auth/notifications/${encodeURIComponent(id)}`); + }, + followWriter(username) { + return api.post(`/api/writers/${encodeURIComponent(username.replace(/^@/, ''))}/follow`, {}); + }, + unfollowWriter(username) { + return api.del(`/api/writers/${encodeURIComponent(username.replace(/^@/, ''))}/follow`); + }, + writerFollowers(username, params) { + return api.get(`/api/writers/${encodeURIComponent(username.replace(/^@/, ''))}/followers${toQuery(params)}`); + }, + followTopic(slug) { + return api.post(`/api/topics/${encodeURIComponent(slug)}/follow`, {}); + }, + unfollowTopic(slug) { + return api.del(`/api/topics/${encodeURIComponent(slug)}/follow`); + }, + topicFollowers(slug, params) { + return api.get(`/api/topics/${encodeURIComponent(slug)}/followers${toQuery(params)}`); + }, + following() { + return api.get('/api/auth/following'); + }, + listReadingLists() { + return api.get('/api/auth/lists'); + }, + createReadingList(name) { + return api.post('/api/auth/lists', { name }); + }, + deleteReadingList(id) { + return api.del(`/api/auth/lists/${encodeURIComponent(id)}`); + }, + addToReadingList(listId, articleId) { + return api.post(`/api/auth/lists/${encodeURIComponent(listId)}/items`, { articleId }); + }, + removeFromReadingList(listId, itemId) { + return api.del(`/api/auth/lists/${encodeURIComponent(listId)}/items/${encodeURIComponent(itemId)}`); + }, + reorderReadingList(listId, itemIds) { + return api.put(`/api/auth/lists/${encodeURIComponent(listId)}/reorder`, { itemIds }); + }, + listQa(slug) { + return api.get(`/api/articles/${encodeURIComponent(slug)}/qa`); + }, + postQuestion(slug, body) { + return api.post(`/api/articles/${encodeURIComponent(slug)}/qa`, { body }); + }, + postAnswer(questionId, body) { + return api.post(`/api/qa/questions/${encodeURIComponent(questionId)}/answers`, { body }); + }, + writerAnalyticsOverview() { + return api.get('/api/auth/analytics/overview'); + }, + writerAnalyticsArticles() { + return api.get('/api/auth/analytics/articles'); + }, + writerAnalyticsArticle(slug) { + return api.get(`/api/auth/analytics/article/${encodeURIComponent(slug)}`); + }, + listBadges() { + return api.get('/api/auth/badges'); + }, +}; + +function urlBase64ToUint8Array(base64String) { + const padding = '='.repeat((4 - (base64String.length % 4)) % 4); + const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/'); + const rawData = atob(base64); + return Uint8Array.from([...rawData].map((c) => c.charCodeAt(0))); +} + +function getCsrfToken() { + return document.cookie.match(/tek_csrf=([^;]+)/)?.[1] ?? ''; +} + +export const notificationPreferencesApi = { + get() { + return api.get('/api/auth/notifications/preferences'); + }, + update(preferences) { + return api.put('/api/auth/notifications/preferences', preferences); + }, +}; + +export const revisionsApi = { + list(articleId, page = 1) { + return api.get(`/api/admin/articles/${encodeURIComponent(articleId)}/revisions?page=${page}`); + }, + get(articleId, revisionId) { + return api.get(`/api/admin/articles/${encodeURIComponent(articleId)}/revisions/${encodeURIComponent(revisionId)}`); + }, + restore(articleId, revisionId) { + return api.post(`/api/admin/articles/${encodeURIComponent(articleId)}/revisions/${encodeURIComponent(revisionId)}/restore`, {}); + }, +}; + +export const presenceApi = { + heartbeat(articleId) { + return api.post(`/api/admin/articles/${encodeURIComponent(articleId)}/heartbeat`, {}); + }, + getEditors(articleId) { + return api.get(`/api/admin/articles/${encodeURIComponent(articleId)}/presence`); + }, +}; + +export const pushApi = { + async getVapidKey() { + const r = await fetch('/api/v1/auth/push/vapid-key', { credentials: 'include' }); + if (!r.ok) throw new Error('push_not_configured'); + const { publicKey } = await r.json(); + return publicKey; + }, + async isSubscribed() { + if (!('PushManager' in window) || !('serviceWorker' in navigator)) return false; + const reg = await navigator.serviceWorker.ready; + const sub = await reg.pushManager.getSubscription(); + return !!sub; + }, + async subscribe() { + const publicKey = await this.getVapidKey(); + const reg = await navigator.serviceWorker.ready; + const sub = await reg.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: urlBase64ToUint8Array(publicKey), + }); + const json = sub.toJSON(); + await fetch('/api/v1/auth/push/subscribe', { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': getCsrfToken() }, + body: JSON.stringify({ + endpoint: json.endpoint, + auth: json.keys.auth, + p256dh: json.keys.p256dh, + userAgent: navigator.userAgent, + }), + }); + return sub; + }, + async unsubscribe() { + if (!('serviceWorker' in navigator)) return; + const reg = await navigator.serviceWorker.ready; + const sub = await reg.pushManager.getSubscription(); + if (!sub) return; + await fetch('/api/v1/auth/push/unsubscribe', { + method: 'DELETE', + credentials: 'include', + headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': getCsrfToken() }, + body: JSON.stringify({ endpoint: sub.endpoint }), + }); + await sub.unsubscribe(); + }, + async getGuestVapidKey() { + const r = await fetch('/api/v1/push/vapid-key'); + if (!r.ok) throw new Error('push_not_configured'); + const { publicKey } = await r.json(); + return publicKey; + }, + async subscribeAsGuest(topics) { + const publicKey = await this.getGuestVapidKey(); + const reg = await navigator.serviceWorker.ready; + const sub = await reg.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: urlBase64ToUint8Array(publicKey), + }); + const json = sub.toJSON(); + const r = await fetch('/api/v1/push/guest-subscribe', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + endpoint: json.endpoint, + auth: json.keys.auth, + p256dh: json.keys.p256dh, + topics, + userAgent: navigator.userAgent, + }), + }); + if (!r.ok) throw new Error('subscribe_failed'); + return sub; + }, + async unsubscribeGuest() { + if (!('serviceWorker' in navigator)) return; + const reg = await navigator.serviceWorker.ready; + const sub = await reg.pushManager.getSubscription(); + if (!sub) return; + await fetch('/api/v1/push/guest-unsubscribe', { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ endpoint: sub.endpoint }), + }); + await sub.unsubscribe(); + }, +};