The edit page loaders were calling adminListVideos/adminListArticles with the old pre-pagination signatures and filtering by ID client-side, which broke after pagination limited results to 50. Now fetches the single item by ID directly via new adminGetVideo and adminGetArticle backend queries. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1779 lines
40 KiB
TypeScript
1779 lines
40 KiB
TypeScript
import { gql, GraphQLClient } from "graphql-request";
|
|
import { apiUrl, getGraphQLClient } from "$lib/api";
|
|
import type {
|
|
Analytics,
|
|
Article,
|
|
CurrentUser,
|
|
Model,
|
|
Recording,
|
|
Stats,
|
|
User,
|
|
Video,
|
|
VideoLikeStatus,
|
|
VideoLikeResponse,
|
|
VideoPlayResponse,
|
|
} from "$lib/types";
|
|
import { logger } from "$lib/logger";
|
|
|
|
// Helper to log API calls
|
|
async function loggedApiCall<T>(
|
|
operationName: string,
|
|
operation: () => Promise<T>,
|
|
context?: Record<string, unknown>,
|
|
): Promise<T> {
|
|
const startTime = Date.now();
|
|
|
|
try {
|
|
logger.debug(`🔄 API: ${operationName}`, { context });
|
|
const result = await operation();
|
|
const duration = Date.now() - startTime;
|
|
logger.info(`✅ API: ${operationName} succeeded`, { duration, context });
|
|
return result;
|
|
} catch (error) {
|
|
const duration = Date.now() - startTime;
|
|
logger.error(`❌ API: ${operationName} failed`, {
|
|
duration,
|
|
context,
|
|
error: error instanceof Error ? error : new Error(String(error)),
|
|
});
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// For server-side auth checks: forward cookie header manually
|
|
function getAuthClient(token: string, fetchFn?: typeof globalThis.fetch) {
|
|
return new GraphQLClient(`${apiUrl}/graphql`, {
|
|
fetch: fetchFn || globalThis.fetch,
|
|
headers: { cookie: `session_token=${token}` },
|
|
});
|
|
}
|
|
|
|
// ─── Auth ────────────────────────────────────────────────────────────────────
|
|
|
|
const ME_QUERY = gql`
|
|
query Me {
|
|
me {
|
|
id
|
|
email
|
|
first_name
|
|
last_name
|
|
artist_name
|
|
slug
|
|
description
|
|
tags
|
|
role
|
|
is_admin
|
|
avatar
|
|
banner
|
|
email_verified
|
|
date_created
|
|
}
|
|
}
|
|
`;
|
|
|
|
export async function isAuthenticated(token: string, fetchFn?: typeof globalThis.fetch) {
|
|
return loggedApiCall(
|
|
"isAuthenticated",
|
|
async () => {
|
|
try {
|
|
const client = getAuthClient(token, fetchFn);
|
|
const data = await client.request<{ me: CurrentUser | null }>(ME_QUERY);
|
|
if (data.me) {
|
|
return { authenticated: true, user: data.me };
|
|
}
|
|
return { authenticated: false };
|
|
} catch {
|
|
return { authenticated: false };
|
|
}
|
|
},
|
|
{ hasToken: !!token },
|
|
);
|
|
}
|
|
|
|
const LOGIN_MUTATION = gql`
|
|
mutation Login($email: String!, $password: String!) {
|
|
login(email: $email, password: $password) {
|
|
id
|
|
email
|
|
first_name
|
|
last_name
|
|
artist_name
|
|
slug
|
|
description
|
|
tags
|
|
role
|
|
avatar
|
|
banner
|
|
email_verified
|
|
date_created
|
|
}
|
|
}
|
|
`;
|
|
|
|
export async function login(email: string, password: string) {
|
|
return loggedApiCall(
|
|
"login",
|
|
async () => {
|
|
const data = await getGraphQLClient().request<{ login: CurrentUser }>(LOGIN_MUTATION, {
|
|
email,
|
|
password,
|
|
});
|
|
return data.login;
|
|
},
|
|
{ email },
|
|
);
|
|
}
|
|
|
|
const LOGOUT_MUTATION = gql`
|
|
mutation Logout {
|
|
logout
|
|
}
|
|
`;
|
|
|
|
export async function logout() {
|
|
return loggedApiCall("logout", async () => {
|
|
await getGraphQLClient().request(LOGOUT_MUTATION);
|
|
});
|
|
}
|
|
|
|
const REGISTER_MUTATION = gql`
|
|
mutation Register($email: String!, $password: String!, $firstName: String!, $lastName: String!) {
|
|
register(email: $email, password: $password, firstName: $firstName, lastName: $lastName)
|
|
}
|
|
`;
|
|
|
|
export async function register(
|
|
email: string,
|
|
password: string,
|
|
firstName: string,
|
|
lastName: string,
|
|
) {
|
|
return loggedApiCall(
|
|
"register",
|
|
async () => {
|
|
await getGraphQLClient().request(REGISTER_MUTATION, {
|
|
email,
|
|
password,
|
|
firstName,
|
|
lastName,
|
|
});
|
|
},
|
|
{ email, firstName, lastName },
|
|
);
|
|
}
|
|
|
|
const VERIFY_EMAIL_MUTATION = gql`
|
|
mutation VerifyEmail($token: String!) {
|
|
verifyEmail(token: $token)
|
|
}
|
|
`;
|
|
|
|
export async function verify(token: string, fetchFn?: typeof globalThis.fetch) {
|
|
return loggedApiCall(
|
|
"verify",
|
|
async () => {
|
|
await getGraphQLClient(fetchFn).request(VERIFY_EMAIL_MUTATION, { token });
|
|
},
|
|
{ hasToken: !!token },
|
|
);
|
|
}
|
|
|
|
const REQUEST_PASSWORD_MUTATION = gql`
|
|
mutation RequestPasswordReset($email: String!) {
|
|
requestPasswordReset(email: $email)
|
|
}
|
|
`;
|
|
|
|
export async function requestPassword(email: string) {
|
|
return loggedApiCall(
|
|
"requestPassword",
|
|
async () => {
|
|
await getGraphQLClient().request(REQUEST_PASSWORD_MUTATION, { email });
|
|
},
|
|
{ email },
|
|
);
|
|
}
|
|
|
|
const RESET_PASSWORD_MUTATION = gql`
|
|
mutation ResetPassword($token: String!, $newPassword: String!) {
|
|
resetPassword(token: $token, newPassword: $newPassword)
|
|
}
|
|
`;
|
|
|
|
export async function resetPassword(token: string, password: string) {
|
|
return loggedApiCall(
|
|
"resetPassword",
|
|
async () => {
|
|
await getGraphQLClient().request(RESET_PASSWORD_MUTATION, {
|
|
token,
|
|
newPassword: password,
|
|
});
|
|
},
|
|
{ hasToken: !!token },
|
|
);
|
|
}
|
|
|
|
// ─── Articles ────────────────────────────────────────────────────────────────
|
|
|
|
const ARTICLES_QUERY = gql`
|
|
query GetArticles(
|
|
$search: String
|
|
$category: String
|
|
$sortBy: String
|
|
$offset: Int
|
|
$limit: Int
|
|
$featured: Boolean
|
|
$tag: String
|
|
) {
|
|
articles(
|
|
search: $search
|
|
category: $category
|
|
sortBy: $sortBy
|
|
offset: $offset
|
|
limit: $limit
|
|
featured: $featured
|
|
tag: $tag
|
|
) {
|
|
items {
|
|
id
|
|
slug
|
|
title
|
|
excerpt
|
|
content
|
|
image
|
|
tags
|
|
publish_date
|
|
category
|
|
featured
|
|
author {
|
|
id
|
|
artist_name
|
|
slug
|
|
avatar
|
|
}
|
|
}
|
|
total
|
|
}
|
|
}
|
|
`;
|
|
|
|
export async function getArticles(
|
|
params: {
|
|
search?: string;
|
|
category?: string;
|
|
sortBy?: string;
|
|
offset?: number;
|
|
limit?: number;
|
|
featured?: boolean;
|
|
tag?: string;
|
|
} = {},
|
|
fetchFn?: typeof globalThis.fetch,
|
|
): Promise<{ items: Article[]; total: number }> {
|
|
return loggedApiCall("getArticles", async () => {
|
|
const data = await getGraphQLClient(fetchFn).request<{
|
|
articles: { items: Article[]; total: number };
|
|
}>(ARTICLES_QUERY, params);
|
|
return data.articles;
|
|
});
|
|
}
|
|
|
|
const ARTICLE_BY_SLUG_QUERY = gql`
|
|
query GetArticleBySlug($slug: String!) {
|
|
article(slug: $slug) {
|
|
id
|
|
slug
|
|
title
|
|
excerpt
|
|
content
|
|
image
|
|
tags
|
|
publish_date
|
|
category
|
|
featured
|
|
author {
|
|
id
|
|
artist_name
|
|
slug
|
|
avatar
|
|
}
|
|
}
|
|
}
|
|
`;
|
|
|
|
export async function getArticleBySlug(slug: string, fetchFn?: typeof globalThis.fetch) {
|
|
return loggedApiCall(
|
|
"getArticleBySlug",
|
|
async () => {
|
|
const data = await getGraphQLClient(fetchFn).request<{ article: Article | null }>(
|
|
ARTICLE_BY_SLUG_QUERY,
|
|
{ slug },
|
|
);
|
|
if (!data.article) throw new Error("Article not found");
|
|
return data.article;
|
|
},
|
|
{ slug },
|
|
);
|
|
}
|
|
|
|
// ─── Videos ──────────────────────────────────────────────────────────────────
|
|
|
|
const VIDEOS_QUERY = gql`
|
|
query GetVideos(
|
|
$modelId: String
|
|
$featured: Boolean
|
|
$limit: Int
|
|
$search: String
|
|
$offset: Int
|
|
$sortBy: String
|
|
$duration: String
|
|
$tag: String
|
|
) {
|
|
videos(
|
|
modelId: $modelId
|
|
featured: $featured
|
|
limit: $limit
|
|
search: $search
|
|
offset: $offset
|
|
sortBy: $sortBy
|
|
duration: $duration
|
|
tag: $tag
|
|
) {
|
|
items {
|
|
id
|
|
slug
|
|
title
|
|
description
|
|
image
|
|
movie
|
|
tags
|
|
upload_date
|
|
premium
|
|
featured
|
|
likes_count
|
|
plays_count
|
|
models {
|
|
id
|
|
artist_name
|
|
slug
|
|
avatar
|
|
}
|
|
movie_file {
|
|
id
|
|
filename
|
|
mime_type
|
|
duration
|
|
}
|
|
}
|
|
total
|
|
}
|
|
}
|
|
`;
|
|
|
|
export async function getVideos(
|
|
params: {
|
|
search?: string;
|
|
sortBy?: string;
|
|
duration?: string;
|
|
offset?: number;
|
|
limit?: number;
|
|
tag?: string;
|
|
} = {},
|
|
fetchFn?: typeof globalThis.fetch,
|
|
): Promise<{ items: Video[]; total: number }> {
|
|
return loggedApiCall("getVideos", async () => {
|
|
const data = await getGraphQLClient(fetchFn).request<{
|
|
videos: { items: Video[]; total: number };
|
|
}>(VIDEOS_QUERY, params);
|
|
return data.videos;
|
|
});
|
|
}
|
|
|
|
export async function getVideosForModel(id: string, fetchFn?: typeof globalThis.fetch) {
|
|
return loggedApiCall(
|
|
"getVideosForModel",
|
|
async () => {
|
|
const data = await getGraphQLClient(fetchFn).request<{
|
|
videos: { items: Video[]; total: number };
|
|
}>(VIDEOS_QUERY, { modelId: id, limit: 10000 });
|
|
return data.videos.items;
|
|
},
|
|
{ modelId: id },
|
|
);
|
|
}
|
|
|
|
export async function getFeaturedVideos(
|
|
limit: number,
|
|
fetchFn: typeof globalThis.fetch = globalThis.fetch,
|
|
) {
|
|
return loggedApiCall(
|
|
"getFeaturedVideos",
|
|
async () => {
|
|
const data = await getGraphQLClient(fetchFn).request<{
|
|
videos: { items: Video[]; total: number };
|
|
}>(VIDEOS_QUERY, { featured: true, limit });
|
|
return data.videos.items;
|
|
},
|
|
{ limit },
|
|
);
|
|
}
|
|
|
|
const VIDEO_BY_SLUG_QUERY = gql`
|
|
query GetVideoBySlug($slug: String!) {
|
|
video(slug: $slug) {
|
|
id
|
|
slug
|
|
title
|
|
description
|
|
image
|
|
movie
|
|
tags
|
|
upload_date
|
|
premium
|
|
featured
|
|
likes_count
|
|
plays_count
|
|
models {
|
|
id
|
|
artist_name
|
|
slug
|
|
avatar
|
|
}
|
|
movie_file {
|
|
id
|
|
filename
|
|
mime_type
|
|
duration
|
|
}
|
|
}
|
|
}
|
|
`;
|
|
|
|
export async function getVideoBySlug(slug: string, fetchFn?: typeof globalThis.fetch) {
|
|
return loggedApiCall(
|
|
"getVideoBySlug",
|
|
async () => {
|
|
const data = await getGraphQLClient(fetchFn).request<{ video: Video | null }>(
|
|
VIDEO_BY_SLUG_QUERY,
|
|
{ slug },
|
|
);
|
|
if (!data.video) throw new Error("Video not found");
|
|
return data.video;
|
|
},
|
|
{ slug },
|
|
);
|
|
}
|
|
|
|
// ─── Models ──────────────────────────────────────────────────────────────────
|
|
|
|
const MODELS_QUERY = gql`
|
|
query GetModels(
|
|
$featured: Boolean
|
|
$limit: Int
|
|
$search: String
|
|
$offset: Int
|
|
$sortBy: String
|
|
$tag: String
|
|
) {
|
|
models(
|
|
featured: $featured
|
|
limit: $limit
|
|
search: $search
|
|
offset: $offset
|
|
sortBy: $sortBy
|
|
tag: $tag
|
|
) {
|
|
items {
|
|
id
|
|
slug
|
|
artist_name
|
|
description
|
|
avatar
|
|
banner
|
|
tags
|
|
date_created
|
|
photos {
|
|
id
|
|
filename
|
|
}
|
|
}
|
|
total
|
|
}
|
|
}
|
|
`;
|
|
|
|
export async function getModels(
|
|
params: { search?: string; sortBy?: string; offset?: number; limit?: number; tag?: string } = {},
|
|
fetchFn?: typeof globalThis.fetch,
|
|
): Promise<{ items: Model[]; total: number }> {
|
|
return loggedApiCall("getModels", async () => {
|
|
const data = await getGraphQLClient(fetchFn).request<{
|
|
models: { items: Model[]; total: number };
|
|
}>(MODELS_QUERY, params);
|
|
return data.models;
|
|
});
|
|
}
|
|
|
|
export async function getFeaturedModels(
|
|
limit = 3,
|
|
fetchFn: typeof globalThis.fetch = globalThis.fetch,
|
|
) {
|
|
return loggedApiCall(
|
|
"getFeaturedModels",
|
|
async () => {
|
|
const data = await getGraphQLClient(fetchFn).request<{
|
|
models: { items: Model[]; total: number };
|
|
}>(MODELS_QUERY, { featured: true, limit });
|
|
return data.models.items;
|
|
},
|
|
{ limit },
|
|
);
|
|
}
|
|
|
|
const MODEL_BY_SLUG_QUERY = gql`
|
|
query GetModelBySlug($slug: String!) {
|
|
model(slug: $slug) {
|
|
id
|
|
slug
|
|
artist_name
|
|
description
|
|
avatar
|
|
banner
|
|
tags
|
|
date_created
|
|
photos {
|
|
id
|
|
filename
|
|
}
|
|
}
|
|
}
|
|
`;
|
|
|
|
export async function getModelBySlug(slug: string, fetchFn?: typeof globalThis.fetch) {
|
|
return loggedApiCall(
|
|
"getModelBySlug",
|
|
async () => {
|
|
const data = await getGraphQLClient(fetchFn).request<{ model: Model | null }>(
|
|
MODEL_BY_SLUG_QUERY,
|
|
{ slug },
|
|
);
|
|
if (!data.model) throw new Error("Model not found");
|
|
return data.model;
|
|
},
|
|
{ slug },
|
|
);
|
|
}
|
|
|
|
// ─── Profile ─────────────────────────────────────────────────────────────────
|
|
|
|
const UPDATE_PROFILE_MUTATION = gql`
|
|
mutation UpdateProfile(
|
|
$firstName: String
|
|
$lastName: String
|
|
$artistName: String
|
|
$description: String
|
|
$tags: [String!]
|
|
) {
|
|
updateProfile(
|
|
firstName: $firstName
|
|
lastName: $lastName
|
|
artistName: $artistName
|
|
description: $description
|
|
tags: $tags
|
|
) {
|
|
id
|
|
email
|
|
first_name
|
|
last_name
|
|
artist_name
|
|
slug
|
|
description
|
|
tags
|
|
role
|
|
avatar
|
|
banner
|
|
date_created
|
|
}
|
|
}
|
|
`;
|
|
|
|
export async function updateProfile(user: Partial<User> & { password?: string }) {
|
|
return loggedApiCall(
|
|
"updateProfile",
|
|
async () => {
|
|
const data = await getGraphQLClient().request<{ updateProfile: User }>(
|
|
UPDATE_PROFILE_MUTATION,
|
|
{
|
|
firstName: user.first_name,
|
|
lastName: user.last_name,
|
|
artistName: user.artist_name,
|
|
description: user.description,
|
|
tags: user.tags,
|
|
},
|
|
);
|
|
return data.updateProfile;
|
|
},
|
|
{ userId: user.id },
|
|
);
|
|
}
|
|
|
|
// ─── Stats ───────────────────────────────────────────────────────────────────
|
|
|
|
const STATS_QUERY = gql`
|
|
query GetStats {
|
|
stats {
|
|
videos_count
|
|
models_count
|
|
viewers_count
|
|
}
|
|
}
|
|
`;
|
|
|
|
export async function getStats(fetchFn?: typeof globalThis.fetch) {
|
|
return loggedApiCall("getStats", async () => {
|
|
const data = await getGraphQLClient(fetchFn).request<{ stats: Stats }>(STATS_QUERY);
|
|
return data.stats;
|
|
});
|
|
}
|
|
|
|
// Stub — Directus folder concept dropped
|
|
export async function getFolders(_fetchFn?: typeof globalThis.fetch) {
|
|
return loggedApiCall("getFolders", async () => [] as { id: string; name: string }[]);
|
|
}
|
|
|
|
// ─── Files ───────────────────────────────────────────────────────────────────
|
|
|
|
export async function removeFile(id: string) {
|
|
return loggedApiCall(
|
|
"removeFile",
|
|
async () => {
|
|
// File deletion via REST DELETE /assets/:id (backend handles it)
|
|
const response = await fetch(`${apiUrl}/assets/${id}`, {
|
|
method: "DELETE",
|
|
credentials: "include",
|
|
});
|
|
if (!response.ok) throw new Error(`Failed to delete file: ${response.statusText}`);
|
|
},
|
|
{ fileId: id },
|
|
);
|
|
}
|
|
|
|
export async function uploadFile(data: FormData) {
|
|
return loggedApiCall("uploadFile", async () => {
|
|
const response = await fetch(`${apiUrl}/upload`, {
|
|
method: "POST",
|
|
body: data,
|
|
credentials: "include",
|
|
});
|
|
if (!response.ok) throw new Error(`Upload failed: ${response.statusText}`);
|
|
return response.json();
|
|
});
|
|
}
|
|
|
|
// ─── Comments ────────────────────────────────────────────────────────────────
|
|
|
|
const COMMENTS_FOR_VIDEO_QUERY = gql`
|
|
query CommentsForVideo($videoId: String!) {
|
|
commentsForVideo(videoId: $videoId) {
|
|
id
|
|
comment
|
|
item_id
|
|
user_id
|
|
date_created
|
|
user {
|
|
id
|
|
first_name
|
|
last_name
|
|
avatar
|
|
}
|
|
}
|
|
}
|
|
`;
|
|
|
|
export async function getCommentsForVideo(item: string, fetchFn?: typeof globalThis.fetch) {
|
|
return loggedApiCall(
|
|
"getCommentsForVideo",
|
|
async () => {
|
|
const data = await getGraphQLClient(fetchFn).request<{
|
|
commentsForVideo: {
|
|
id: number;
|
|
comment: string;
|
|
item_id: string;
|
|
user_id: string;
|
|
date_created: string;
|
|
user: {
|
|
id: string;
|
|
first_name: string | null;
|
|
last_name: string | null;
|
|
artist_name: string | null;
|
|
avatar: string | null;
|
|
} | null;
|
|
}[];
|
|
}>(COMMENTS_FOR_VIDEO_QUERY, { videoId: item });
|
|
return data.commentsForVideo;
|
|
},
|
|
{ videoId: item },
|
|
);
|
|
}
|
|
|
|
const CREATE_COMMENT_MUTATION = gql`
|
|
mutation CreateCommentForVideo($videoId: String!, $comment: String!) {
|
|
createCommentForVideo(videoId: $videoId, comment: $comment) {
|
|
id
|
|
comment
|
|
item_id
|
|
user_id
|
|
date_created
|
|
}
|
|
}
|
|
`;
|
|
|
|
export async function createCommentForVideo(item: string, comment: string) {
|
|
return loggedApiCall(
|
|
"createCommentForVideo",
|
|
async () => {
|
|
const data = await getGraphQLClient().request(CREATE_COMMENT_MUTATION, {
|
|
videoId: item,
|
|
comment,
|
|
});
|
|
return data;
|
|
},
|
|
{ videoId: item, commentLength: comment.length },
|
|
);
|
|
}
|
|
|
|
export async function countCommentsForModel(
|
|
_user_created: string,
|
|
_fetchFn?: typeof globalThis.fetch,
|
|
) {
|
|
// Not directly available in new API, return 0
|
|
return 0;
|
|
}
|
|
|
|
// ─── Tags ────────────────────────────────────────────────────────────────────
|
|
|
|
export async function getItemsByTag(
|
|
category: "video" | "article" | "model",
|
|
tag: string,
|
|
fetchFn?: typeof globalThis.fetch,
|
|
) {
|
|
return loggedApiCall(
|
|
"getItemsByTag",
|
|
async () => {
|
|
switch (category) {
|
|
case "video":
|
|
return getVideos({ tag, limit: 1000 }, fetchFn).then((r) => r.items);
|
|
case "model":
|
|
return getModels({ tag, limit: 1000 }, fetchFn).then((r) => r.items);
|
|
case "article":
|
|
return getArticles({ tag, limit: 1000 }, fetchFn).then((r) => r.items);
|
|
}
|
|
},
|
|
{ category, tag },
|
|
);
|
|
}
|
|
|
|
// ─── Recordings ──────────────────────────────────────────────────────────────
|
|
|
|
const RECORDINGS_QUERY = gql`
|
|
query GetRecordings(
|
|
$status: String
|
|
$tags: String
|
|
$linkedVideoId: String
|
|
$limit: Int
|
|
$page: Int
|
|
) {
|
|
recordings(
|
|
status: $status
|
|
tags: $tags
|
|
linkedVideoId: $linkedVideoId
|
|
limit: $limit
|
|
page: $page
|
|
) {
|
|
id
|
|
title
|
|
description
|
|
slug
|
|
duration
|
|
events
|
|
device_info
|
|
user_id
|
|
status
|
|
tags
|
|
linked_video
|
|
featured
|
|
public
|
|
date_created
|
|
date_updated
|
|
}
|
|
}
|
|
`;
|
|
|
|
export async function getRecordings(fetchFn?: typeof globalThis.fetch) {
|
|
return loggedApiCall(
|
|
"getRecordings",
|
|
async () => {
|
|
const data = await getGraphQLClient(fetchFn).request<{ recordings: Recording[] }>(
|
|
RECORDINGS_QUERY,
|
|
);
|
|
return data.recordings;
|
|
},
|
|
{},
|
|
);
|
|
}
|
|
|
|
const CREATE_RECORDING_MUTATION = gql`
|
|
mutation CreateRecording(
|
|
$title: String!
|
|
$description: String
|
|
$duration: Int!
|
|
$events: JSON!
|
|
$deviceInfo: JSON!
|
|
$tags: [String!]
|
|
$status: String
|
|
$linkedVideoId: String
|
|
) {
|
|
createRecording(
|
|
title: $title
|
|
description: $description
|
|
duration: $duration
|
|
events: $events
|
|
deviceInfo: $deviceInfo
|
|
tags: $tags
|
|
status: $status
|
|
linkedVideoId: $linkedVideoId
|
|
) {
|
|
id
|
|
title
|
|
description
|
|
slug
|
|
duration
|
|
events
|
|
device_info
|
|
user_id
|
|
status
|
|
tags
|
|
linked_video
|
|
featured
|
|
public
|
|
date_created
|
|
date_updated
|
|
}
|
|
}
|
|
`;
|
|
|
|
export async function createRecording(
|
|
recording: {
|
|
title: string;
|
|
description?: string;
|
|
duration: number;
|
|
events: unknown[];
|
|
device_info: unknown[];
|
|
tags?: string[];
|
|
status?: string;
|
|
},
|
|
fetchFn?: typeof globalThis.fetch,
|
|
) {
|
|
return loggedApiCall(
|
|
"createRecording",
|
|
async () => {
|
|
const data = await getGraphQLClient(fetchFn).request<{ createRecording: Recording }>(
|
|
CREATE_RECORDING_MUTATION,
|
|
{
|
|
title: recording.title,
|
|
description: recording.description,
|
|
duration: recording.duration,
|
|
events: recording.events,
|
|
deviceInfo: recording.device_info,
|
|
tags: recording.tags,
|
|
status: recording.status,
|
|
},
|
|
);
|
|
return data.createRecording;
|
|
},
|
|
{ title: recording.title, eventCount: recording.events.length },
|
|
);
|
|
}
|
|
|
|
const DELETE_RECORDING_MUTATION = gql`
|
|
mutation DeleteRecording($id: String!) {
|
|
deleteRecording(id: $id)
|
|
}
|
|
`;
|
|
|
|
export async function deleteRecording(id: string) {
|
|
return loggedApiCall(
|
|
"deleteRecording",
|
|
async () => {
|
|
await getGraphQLClient().request(DELETE_RECORDING_MUTATION, { id });
|
|
},
|
|
{ id },
|
|
);
|
|
}
|
|
|
|
const RECORDING_QUERY = gql`
|
|
query GetRecording($id: String!) {
|
|
recording(id: $id) {
|
|
id
|
|
title
|
|
description
|
|
slug
|
|
duration
|
|
events
|
|
device_info
|
|
user_id
|
|
status
|
|
tags
|
|
linked_video
|
|
featured
|
|
public
|
|
date_created
|
|
date_updated
|
|
}
|
|
}
|
|
`;
|
|
|
|
export async function getRecording(id: string, fetchFn?: typeof globalThis.fetch) {
|
|
return loggedApiCall(
|
|
"getRecording",
|
|
async () => {
|
|
const data = await getGraphQLClient(fetchFn).request<{ recording: Recording | null }>(
|
|
RECORDING_QUERY,
|
|
{ id },
|
|
);
|
|
return data.recording;
|
|
},
|
|
{ id },
|
|
);
|
|
}
|
|
|
|
// ─── Video likes & plays ─────────────────────────────────────────────────────
|
|
|
|
const LIKE_VIDEO_MUTATION = gql`
|
|
mutation LikeVideo($videoId: String!) {
|
|
likeVideo(videoId: $videoId) {
|
|
liked
|
|
likes_count
|
|
}
|
|
}
|
|
`;
|
|
|
|
export async function likeVideo(videoId: string) {
|
|
return loggedApiCall(
|
|
"likeVideo",
|
|
async () => {
|
|
const data = await getGraphQLClient().request<{ likeVideo: VideoLikeResponse }>(
|
|
LIKE_VIDEO_MUTATION,
|
|
{ videoId },
|
|
);
|
|
return data.likeVideo;
|
|
},
|
|
{ videoId },
|
|
);
|
|
}
|
|
|
|
const UNLIKE_VIDEO_MUTATION = gql`
|
|
mutation UnlikeVideo($videoId: String!) {
|
|
unlikeVideo(videoId: $videoId) {
|
|
liked
|
|
likes_count
|
|
}
|
|
}
|
|
`;
|
|
|
|
export async function unlikeVideo(videoId: string) {
|
|
return loggedApiCall(
|
|
"unlikeVideo",
|
|
async () => {
|
|
const data = await getGraphQLClient().request<{ unlikeVideo: VideoLikeResponse }>(
|
|
UNLIKE_VIDEO_MUTATION,
|
|
{ videoId },
|
|
);
|
|
return data.unlikeVideo;
|
|
},
|
|
{ videoId },
|
|
);
|
|
}
|
|
|
|
const VIDEO_LIKE_STATUS_QUERY = gql`
|
|
query VideoLikeStatus($videoId: String!) {
|
|
videoLikeStatus(videoId: $videoId) {
|
|
liked
|
|
}
|
|
}
|
|
`;
|
|
|
|
export async function getVideoLikeStatus(videoId: string, fetchFn?: typeof globalThis.fetch) {
|
|
return loggedApiCall(
|
|
"getVideoLikeStatus",
|
|
async () => {
|
|
const data = await getGraphQLClient(fetchFn).request<{ videoLikeStatus: VideoLikeStatus }>(
|
|
VIDEO_LIKE_STATUS_QUERY,
|
|
{ videoId },
|
|
);
|
|
return data.videoLikeStatus;
|
|
},
|
|
{ videoId },
|
|
);
|
|
}
|
|
|
|
const RECORD_VIDEO_PLAY_MUTATION = gql`
|
|
mutation RecordVideoPlay($videoId: String!, $sessionId: String) {
|
|
recordVideoPlay(videoId: $videoId, sessionId: $sessionId) {
|
|
success
|
|
play_id
|
|
plays_count
|
|
}
|
|
}
|
|
`;
|
|
|
|
export async function recordVideoPlay(videoId: string, sessionId?: string) {
|
|
return loggedApiCall(
|
|
"recordVideoPlay",
|
|
async () => {
|
|
const data = await getGraphQLClient().request<{ recordVideoPlay: VideoPlayResponse }>(
|
|
RECORD_VIDEO_PLAY_MUTATION,
|
|
{ videoId, sessionId },
|
|
);
|
|
return data.recordVideoPlay;
|
|
},
|
|
{ videoId },
|
|
);
|
|
}
|
|
|
|
const UPDATE_VIDEO_PLAY_MUTATION = gql`
|
|
mutation UpdateVideoPlay(
|
|
$videoId: String!
|
|
$playId: String!
|
|
$durationWatched: Int!
|
|
$completed: Boolean!
|
|
) {
|
|
updateVideoPlay(
|
|
videoId: $videoId
|
|
playId: $playId
|
|
durationWatched: $durationWatched
|
|
completed: $completed
|
|
)
|
|
}
|
|
`;
|
|
|
|
export async function updateVideoPlay(
|
|
videoId: string,
|
|
playId: string,
|
|
durationWatched: number,
|
|
completed: boolean,
|
|
) {
|
|
return loggedApiCall(
|
|
"updateVideoPlay",
|
|
async () => {
|
|
await getGraphQLClient().request(UPDATE_VIDEO_PLAY_MUTATION, {
|
|
videoId,
|
|
playId,
|
|
durationWatched,
|
|
completed,
|
|
});
|
|
},
|
|
{ videoId, playId, durationWatched, completed },
|
|
);
|
|
}
|
|
|
|
// ─── Delete comment ──────────────────────────────────────────────────────────
|
|
|
|
const DELETE_COMMENT_MUTATION = gql`
|
|
mutation DeleteComment($id: Int!) {
|
|
deleteComment(id: $id)
|
|
}
|
|
`;
|
|
|
|
export async function deleteComment(id: number) {
|
|
return loggedApiCall(
|
|
"deleteComment",
|
|
async () => {
|
|
await getGraphQLClient().request(DELETE_COMMENT_MUTATION, { id });
|
|
},
|
|
{ id },
|
|
);
|
|
}
|
|
|
|
// ─── Admin: Users ─────────────────────────────────────────────────────────────
|
|
|
|
const ADMIN_LIST_USERS_QUERY = gql`
|
|
query AdminListUsers($role: String, $search: String, $limit: Int, $offset: Int) {
|
|
adminListUsers(role: $role, search: $search, limit: $limit, offset: $offset) {
|
|
total
|
|
items {
|
|
id
|
|
email
|
|
first_name
|
|
last_name
|
|
artist_name
|
|
slug
|
|
role
|
|
is_admin
|
|
avatar
|
|
email_verified
|
|
date_created
|
|
}
|
|
}
|
|
}
|
|
`;
|
|
|
|
export async function adminListUsers(
|
|
opts: { role?: string; search?: string; limit?: number; offset?: number } = {},
|
|
fetchFn?: typeof globalThis.fetch,
|
|
token?: string,
|
|
) {
|
|
return loggedApiCall(
|
|
"adminListUsers",
|
|
async () => {
|
|
const client = token ? getAuthClient(token) : getGraphQLClient(fetchFn);
|
|
const data = await client.request<{
|
|
adminListUsers: { total: number; items: User[] };
|
|
}>(ADMIN_LIST_USERS_QUERY, opts);
|
|
return data.adminListUsers;
|
|
},
|
|
opts,
|
|
);
|
|
}
|
|
|
|
const ADMIN_UPDATE_USER_MUTATION = gql`
|
|
mutation AdminUpdateUser(
|
|
$userId: String!
|
|
$role: String
|
|
$isAdmin: Boolean
|
|
$firstName: String
|
|
$lastName: String
|
|
$artistName: String
|
|
$avatarId: String
|
|
$bannerId: String
|
|
) {
|
|
adminUpdateUser(
|
|
userId: $userId
|
|
role: $role
|
|
isAdmin: $isAdmin
|
|
firstName: $firstName
|
|
lastName: $lastName
|
|
artistName: $artistName
|
|
avatarId: $avatarId
|
|
bannerId: $bannerId
|
|
) {
|
|
id
|
|
email
|
|
first_name
|
|
last_name
|
|
artist_name
|
|
role
|
|
is_admin
|
|
avatar
|
|
banner
|
|
date_created
|
|
}
|
|
}
|
|
`;
|
|
|
|
export async function adminUpdateUser(input: {
|
|
userId: string;
|
|
role?: string;
|
|
isAdmin?: boolean;
|
|
firstName?: string;
|
|
lastName?: string;
|
|
artistName?: string;
|
|
avatarId?: string;
|
|
bannerId?: string;
|
|
}) {
|
|
return loggedApiCall(
|
|
"adminUpdateUser",
|
|
async () => {
|
|
const data = await getGraphQLClient().request<{ adminUpdateUser: User | null }>(
|
|
ADMIN_UPDATE_USER_MUTATION,
|
|
input,
|
|
);
|
|
return data.adminUpdateUser;
|
|
},
|
|
{ userId: input.userId },
|
|
);
|
|
}
|
|
|
|
const ADMIN_DELETE_USER_MUTATION = gql`
|
|
mutation AdminDeleteUser($userId: String!) {
|
|
adminDeleteUser(userId: $userId)
|
|
}
|
|
`;
|
|
|
|
export async function adminDeleteUser(userId: string) {
|
|
return loggedApiCall(
|
|
"adminDeleteUser",
|
|
async () => {
|
|
await getGraphQLClient().request(ADMIN_DELETE_USER_MUTATION, { userId });
|
|
},
|
|
{ userId },
|
|
);
|
|
}
|
|
|
|
const ADMIN_GET_USER_QUERY = gql`
|
|
query AdminGetUser($userId: String!) {
|
|
adminGetUser(userId: $userId) {
|
|
id
|
|
email
|
|
first_name
|
|
last_name
|
|
artist_name
|
|
slug
|
|
role
|
|
is_admin
|
|
avatar
|
|
banner
|
|
description
|
|
tags
|
|
email_verified
|
|
date_created
|
|
photos {
|
|
id
|
|
filename
|
|
}
|
|
}
|
|
}
|
|
`;
|
|
|
|
export async function adminGetUser(userId: string, token?: string) {
|
|
return loggedApiCall(
|
|
"adminGetUser",
|
|
async () => {
|
|
const client = token ? getAuthClient(token) : getGraphQLClient();
|
|
const data = await client.request<{ adminGetUser: any }>(ADMIN_GET_USER_QUERY, { userId });
|
|
return data.adminGetUser;
|
|
},
|
|
{ userId },
|
|
);
|
|
}
|
|
|
|
const ADMIN_ADD_USER_PHOTO_MUTATION = gql`
|
|
mutation AdminAddUserPhoto($userId: String!, $fileId: String!) {
|
|
adminAddUserPhoto(userId: $userId, fileId: $fileId)
|
|
}
|
|
`;
|
|
|
|
export async function adminAddUserPhoto(userId: string, fileId: string) {
|
|
return loggedApiCall("adminAddUserPhoto", async () => {
|
|
await getGraphQLClient().request(ADMIN_ADD_USER_PHOTO_MUTATION, { userId, fileId });
|
|
});
|
|
}
|
|
|
|
const ADMIN_REMOVE_USER_PHOTO_MUTATION = gql`
|
|
mutation AdminRemoveUserPhoto($userId: String!, $fileId: String!) {
|
|
adminRemoveUserPhoto(userId: $userId, fileId: $fileId)
|
|
}
|
|
`;
|
|
|
|
export async function adminRemoveUserPhoto(userId: string, fileId: string) {
|
|
return loggedApiCall("adminRemoveUserPhoto", async () => {
|
|
await getGraphQLClient().request(ADMIN_REMOVE_USER_PHOTO_MUTATION, { userId, fileId });
|
|
});
|
|
}
|
|
|
|
// ─── Admin: Videos ────────────────────────────────────────────────────────────
|
|
|
|
const ADMIN_LIST_VIDEOS_QUERY = gql`
|
|
query AdminListVideos(
|
|
$search: String
|
|
$premium: Boolean
|
|
$featured: Boolean
|
|
$limit: Int
|
|
$offset: Int
|
|
) {
|
|
adminListVideos(
|
|
search: $search
|
|
premium: $premium
|
|
featured: $featured
|
|
limit: $limit
|
|
offset: $offset
|
|
) {
|
|
items {
|
|
id
|
|
slug
|
|
title
|
|
description
|
|
image
|
|
movie
|
|
tags
|
|
upload_date
|
|
premium
|
|
featured
|
|
likes_count
|
|
plays_count
|
|
models {
|
|
id
|
|
artist_name
|
|
slug
|
|
avatar
|
|
}
|
|
movie_file {
|
|
id
|
|
filename
|
|
mime_type
|
|
duration
|
|
}
|
|
}
|
|
total
|
|
}
|
|
}
|
|
`;
|
|
|
|
export async function adminListVideos(
|
|
opts: {
|
|
search?: string;
|
|
premium?: boolean;
|
|
featured?: boolean;
|
|
limit?: number;
|
|
offset?: number;
|
|
} = {},
|
|
fetchFn?: typeof globalThis.fetch,
|
|
token?: string,
|
|
): Promise<{ items: Video[]; total: number }> {
|
|
return loggedApiCall("adminListVideos", async () => {
|
|
const client = token ? getAuthClient(token) : getGraphQLClient(fetchFn);
|
|
const data = await client.request<{ adminListVideos: { items: Video[]; total: number } }>(
|
|
ADMIN_LIST_VIDEOS_QUERY,
|
|
opts,
|
|
);
|
|
return data.adminListVideos;
|
|
});
|
|
}
|
|
|
|
const ADMIN_GET_VIDEO_QUERY = gql`
|
|
query AdminGetVideo($id: String!) {
|
|
adminGetVideo(id: $id) {
|
|
id
|
|
slug
|
|
title
|
|
description
|
|
image
|
|
movie
|
|
tags
|
|
upload_date
|
|
premium
|
|
featured
|
|
likes_count
|
|
plays_count
|
|
models {
|
|
id
|
|
artist_name
|
|
slug
|
|
avatar
|
|
}
|
|
movie_file {
|
|
id
|
|
filename
|
|
mime_type
|
|
duration
|
|
}
|
|
}
|
|
}
|
|
`;
|
|
|
|
export async function adminGetVideo(
|
|
id: string,
|
|
fetchFn?: typeof globalThis.fetch,
|
|
token?: string,
|
|
): Promise<Video | null> {
|
|
return loggedApiCall("adminGetVideo", async () => {
|
|
const client = token ? getAuthClient(token) : getGraphQLClient(fetchFn);
|
|
const data = await client.request<{ adminGetVideo: Video | null }>(ADMIN_GET_VIDEO_QUERY, {
|
|
id,
|
|
});
|
|
return data.adminGetVideo;
|
|
});
|
|
}
|
|
|
|
const CREATE_VIDEO_MUTATION = gql`
|
|
mutation CreateVideo(
|
|
$title: String!
|
|
$slug: String!
|
|
$description: String
|
|
$imageId: String
|
|
$movieId: String
|
|
$tags: [String!]
|
|
$premium: Boolean
|
|
$featured: Boolean
|
|
$uploadDate: String
|
|
) {
|
|
createVideo(
|
|
title: $title
|
|
slug: $slug
|
|
description: $description
|
|
imageId: $imageId
|
|
movieId: $movieId
|
|
tags: $tags
|
|
premium: $premium
|
|
featured: $featured
|
|
uploadDate: $uploadDate
|
|
) {
|
|
id
|
|
slug
|
|
title
|
|
}
|
|
}
|
|
`;
|
|
|
|
export async function createVideo(input: {
|
|
title: string;
|
|
slug: string;
|
|
description?: string;
|
|
imageId?: string;
|
|
movieId?: string;
|
|
tags?: string[];
|
|
premium?: boolean;
|
|
featured?: boolean;
|
|
uploadDate?: string;
|
|
}) {
|
|
return loggedApiCall(
|
|
"createVideo",
|
|
async () => {
|
|
const data = await getGraphQLClient().request<{ createVideo: Video }>(
|
|
CREATE_VIDEO_MUTATION,
|
|
input,
|
|
);
|
|
return data.createVideo;
|
|
},
|
|
{ title: input.title },
|
|
);
|
|
}
|
|
|
|
const UPDATE_VIDEO_MUTATION = gql`
|
|
mutation UpdateVideo(
|
|
$id: String!
|
|
$title: String
|
|
$slug: String
|
|
$description: String
|
|
$imageId: String
|
|
$movieId: String
|
|
$tags: [String!]
|
|
$premium: Boolean
|
|
$featured: Boolean
|
|
$uploadDate: String
|
|
) {
|
|
updateVideo(
|
|
id: $id
|
|
title: $title
|
|
slug: $slug
|
|
description: $description
|
|
imageId: $imageId
|
|
movieId: $movieId
|
|
tags: $tags
|
|
premium: $premium
|
|
featured: $featured
|
|
uploadDate: $uploadDate
|
|
) {
|
|
id
|
|
slug
|
|
title
|
|
}
|
|
}
|
|
`;
|
|
|
|
export async function updateVideo(input: {
|
|
id: string;
|
|
title?: string;
|
|
slug?: string;
|
|
description?: string;
|
|
imageId?: string;
|
|
movieId?: string;
|
|
tags?: string[];
|
|
premium?: boolean;
|
|
featured?: boolean;
|
|
uploadDate?: string;
|
|
}) {
|
|
return loggedApiCall(
|
|
"updateVideo",
|
|
async () => {
|
|
const data = await getGraphQLClient().request<{ updateVideo: Video | null }>(
|
|
UPDATE_VIDEO_MUTATION,
|
|
input,
|
|
);
|
|
return data.updateVideo;
|
|
},
|
|
{ id: input.id },
|
|
);
|
|
}
|
|
|
|
const DELETE_VIDEO_MUTATION = gql`
|
|
mutation DeleteVideo($id: String!) {
|
|
deleteVideo(id: $id)
|
|
}
|
|
`;
|
|
|
|
export async function deleteVideo(id: string) {
|
|
return loggedApiCall(
|
|
"deleteVideo",
|
|
async () => {
|
|
await getGraphQLClient().request(DELETE_VIDEO_MUTATION, { id });
|
|
},
|
|
{ id },
|
|
);
|
|
}
|
|
|
|
const SET_VIDEO_MODELS_MUTATION = gql`
|
|
mutation SetVideoModels($videoId: String!, $userIds: [String!]!) {
|
|
setVideoModels(videoId: $videoId, userIds: $userIds)
|
|
}
|
|
`;
|
|
|
|
export async function setVideoModels(videoId: string, userIds: string[]) {
|
|
return loggedApiCall(
|
|
"setVideoModels",
|
|
async () => {
|
|
await getGraphQLClient().request(SET_VIDEO_MODELS_MUTATION, { videoId, userIds });
|
|
},
|
|
{ videoId, count: userIds.length },
|
|
);
|
|
}
|
|
|
|
// ─── Admin: Articles ──────────────────────────────────────────────────────────
|
|
|
|
const ADMIN_LIST_ARTICLES_QUERY = gql`
|
|
query AdminListArticles(
|
|
$search: String
|
|
$category: String
|
|
$featured: Boolean
|
|
$limit: Int
|
|
$offset: Int
|
|
) {
|
|
adminListArticles(
|
|
search: $search
|
|
category: $category
|
|
featured: $featured
|
|
limit: $limit
|
|
offset: $offset
|
|
) {
|
|
items {
|
|
id
|
|
slug
|
|
title
|
|
excerpt
|
|
image
|
|
tags
|
|
publish_date
|
|
category
|
|
featured
|
|
content
|
|
author {
|
|
id
|
|
artist_name
|
|
slug
|
|
avatar
|
|
}
|
|
}
|
|
total
|
|
}
|
|
}
|
|
`;
|
|
|
|
export async function adminListArticles(
|
|
opts: {
|
|
search?: string;
|
|
category?: string;
|
|
featured?: boolean;
|
|
limit?: number;
|
|
offset?: number;
|
|
} = {},
|
|
fetchFn?: typeof globalThis.fetch,
|
|
token?: string,
|
|
): Promise<{ items: Article[]; total: number }> {
|
|
return loggedApiCall("adminListArticles", async () => {
|
|
const client = token ? getAuthClient(token) : getGraphQLClient(fetchFn);
|
|
const data = await client.request<{ adminListArticles: { items: Article[]; total: number } }>(
|
|
ADMIN_LIST_ARTICLES_QUERY,
|
|
opts,
|
|
);
|
|
return data.adminListArticles;
|
|
});
|
|
}
|
|
|
|
const ADMIN_GET_ARTICLE_QUERY = gql`
|
|
query AdminGetArticle($id: String!) {
|
|
adminGetArticle(id: $id) {
|
|
id
|
|
slug
|
|
title
|
|
excerpt
|
|
content
|
|
image
|
|
tags
|
|
publish_date
|
|
category
|
|
featured
|
|
author {
|
|
id
|
|
artist_name
|
|
slug
|
|
avatar
|
|
}
|
|
}
|
|
}
|
|
`;
|
|
|
|
export async function adminGetArticle(
|
|
id: string,
|
|
fetchFn?: typeof globalThis.fetch,
|
|
token?: string,
|
|
): Promise<Article | null> {
|
|
return loggedApiCall("adminGetArticle", async () => {
|
|
const client = token ? getAuthClient(token) : getGraphQLClient(fetchFn);
|
|
const data = await client.request<{ adminGetArticle: Article | null }>(
|
|
ADMIN_GET_ARTICLE_QUERY,
|
|
{ id },
|
|
);
|
|
return data.adminGetArticle;
|
|
});
|
|
}
|
|
|
|
const CREATE_ARTICLE_MUTATION = gql`
|
|
mutation CreateArticle(
|
|
$title: String!
|
|
$slug: String!
|
|
$excerpt: String
|
|
$content: String
|
|
$imageId: String
|
|
$tags: [String!]
|
|
$category: String
|
|
$featured: Boolean
|
|
$publishDate: String
|
|
) {
|
|
createArticle(
|
|
title: $title
|
|
slug: $slug
|
|
excerpt: $excerpt
|
|
content: $content
|
|
imageId: $imageId
|
|
tags: $tags
|
|
category: $category
|
|
featured: $featured
|
|
publishDate: $publishDate
|
|
) {
|
|
id
|
|
slug
|
|
title
|
|
}
|
|
}
|
|
`;
|
|
|
|
export async function createArticle(input: {
|
|
title: string;
|
|
slug: string;
|
|
excerpt?: string;
|
|
content?: string;
|
|
imageId?: string;
|
|
tags?: string[];
|
|
category?: string;
|
|
featured?: boolean;
|
|
publishDate?: string;
|
|
}) {
|
|
return loggedApiCall(
|
|
"createArticle",
|
|
async () => {
|
|
const data = await getGraphQLClient().request<{ createArticle: Article }>(
|
|
CREATE_ARTICLE_MUTATION,
|
|
input,
|
|
);
|
|
return data.createArticle;
|
|
},
|
|
{ title: input.title },
|
|
);
|
|
}
|
|
|
|
const UPDATE_ARTICLE_MUTATION = gql`
|
|
mutation UpdateArticle(
|
|
$id: String!
|
|
$title: String
|
|
$slug: String
|
|
$excerpt: String
|
|
$content: String
|
|
$imageId: String
|
|
$authorId: String
|
|
$tags: [String!]
|
|
$category: String
|
|
$featured: Boolean
|
|
$publishDate: String
|
|
) {
|
|
updateArticle(
|
|
id: $id
|
|
title: $title
|
|
slug: $slug
|
|
excerpt: $excerpt
|
|
content: $content
|
|
imageId: $imageId
|
|
authorId: $authorId
|
|
tags: $tags
|
|
category: $category
|
|
featured: $featured
|
|
publishDate: $publishDate
|
|
) {
|
|
id
|
|
slug
|
|
title
|
|
}
|
|
}
|
|
`;
|
|
|
|
export async function updateArticle(input: {
|
|
id: string;
|
|
title?: string;
|
|
slug?: string;
|
|
excerpt?: string;
|
|
content?: string;
|
|
imageId?: string;
|
|
authorId?: string | null;
|
|
tags?: string[];
|
|
category?: string;
|
|
featured?: boolean;
|
|
publishDate?: string;
|
|
}) {
|
|
return loggedApiCall(
|
|
"updateArticle",
|
|
async () => {
|
|
const data = await getGraphQLClient().request<{ updateArticle: Article | null }>(
|
|
UPDATE_ARTICLE_MUTATION,
|
|
input,
|
|
);
|
|
return data.updateArticle;
|
|
},
|
|
{ id: input.id },
|
|
);
|
|
}
|
|
|
|
const DELETE_ARTICLE_MUTATION = gql`
|
|
mutation DeleteArticle($id: String!) {
|
|
deleteArticle(id: $id)
|
|
}
|
|
`;
|
|
|
|
export async function deleteArticle(id: string) {
|
|
return loggedApiCall(
|
|
"deleteArticle",
|
|
async () => {
|
|
await getGraphQLClient().request(DELETE_ARTICLE_MUTATION, { id });
|
|
},
|
|
{ id },
|
|
);
|
|
}
|
|
|
|
// ─── Analytics ───────────────────────────────────────────────────────────────
|
|
|
|
const ANALYTICS_QUERY = gql`
|
|
query GetAnalytics {
|
|
analytics
|
|
}
|
|
`;
|
|
|
|
export async function getAnalytics(fetchFn?: typeof globalThis.fetch) {
|
|
return loggedApiCall(
|
|
"getAnalytics",
|
|
async () => {
|
|
const data = await getGraphQLClient(fetchFn).request<{ analytics: Analytics | null }>(
|
|
ANALYTICS_QUERY,
|
|
);
|
|
return data.analytics;
|
|
},
|
|
{},
|
|
);
|
|
}
|