mirror of
https://github.com/PeterMaquiran/tvone.git
synced 2026-04-23 12:35:51 +00:00
This commit is contained in:
@@ -10,7 +10,7 @@ import {
|
|||||||
// Importe o componente que criámos (ajuste o caminho se necessário)
|
// Importe o componente que criámos (ajuste o caminho se necessário)
|
||||||
import MultiAspectEditor from '../../../components/MultiAspectEditor';
|
import MultiAspectEditor from '../../../components/MultiAspectEditor';
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { getFlat, type Category } from "@/lib/categories.api";
|
import { getTree, type Category } from "@/lib/categories.api";
|
||||||
// import { keycloak } from '@/app/feature/auth/keycloak-config';
|
// import { keycloak } from '@/app/feature/auth/keycloak-config';
|
||||||
const Editor = dynamic(
|
const Editor = dynamic(
|
||||||
() => import("@tinymce/tinymce-react").then((mod) => mod.Editor),
|
() => import("@tinymce/tinymce-react").then((mod) => mod.Editor),
|
||||||
@@ -73,6 +73,8 @@ export const CreateNewsPage = () => {
|
|||||||
const [categories, setCategories] = useState<Category[]>([]);
|
const [categories, setCategories] = useState<Category[]>([]);
|
||||||
const [categoriesLoading, setCategoriesLoading] = useState(true);
|
const [categoriesLoading, setCategoriesLoading] = useState(true);
|
||||||
const [categoryId, setCategoryId] = useState<string>("");
|
const [categoryId, setCategoryId] = useState<string>("");
|
||||||
|
const [subCategoryId, setSubCategoryId] = useState<string>("");
|
||||||
|
const [showSubCategoryError, setShowSubCategoryError] = useState(false);
|
||||||
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
@@ -81,7 +83,7 @@ export const CreateNewsPage = () => {
|
|||||||
(async () => {
|
(async () => {
|
||||||
setCategoriesLoading(true);
|
setCategoriesLoading(true);
|
||||||
try {
|
try {
|
||||||
const list = await getFlat();
|
const list = await getTree();
|
||||||
if (!cancelled) setCategories(list);
|
if (!cancelled) setCategories(list);
|
||||||
} catch {
|
} catch {
|
||||||
if (!cancelled) setCategories([]);
|
if (!cancelled) setCategories([]);
|
||||||
@@ -94,6 +96,26 @@ export const CreateNewsPage = () => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const parentCategories = categories.filter((c) => !c.parentId);
|
||||||
|
const selectedParentCategory = parentCategories.find((c) => c.id === categoryId);
|
||||||
|
const subCategories = selectedParentCategory?.children ?? [];
|
||||||
|
const isSubCategoryRequired = Boolean(categoryId) && subCategories.length > 0;
|
||||||
|
|
||||||
|
const handleCategoryChange = (value: string) => {
|
||||||
|
setCategoryId(value);
|
||||||
|
setSubCategoryId("");
|
||||||
|
setShowSubCategoryError(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePublish = () => {
|
||||||
|
if (isSubCategoryRequired && !subCategoryId) {
|
||||||
|
setShowSubCategoryError(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setShowSubCategoryError(false);
|
||||||
|
// TODO: ligar ao endpoint de publicar quando o fluxo de submissão estiver pronto.
|
||||||
|
};
|
||||||
|
|
||||||
// 2. Lógica de Upload
|
// 2. Lógica de Upload
|
||||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
if (e.target.files && e.target.files.length > 0) {
|
if (e.target.files && e.target.files.length > 0) {
|
||||||
@@ -157,7 +179,10 @@ export const CreateNewsPage = () => {
|
|||||||
<button className="px-5 py-2 rounded-lg border border-slate-200 bg-white text-sm font-medium hover:bg-slate-50 transition-all flex items-center gap-2">
|
<button className="px-5 py-2 rounded-lg border border-slate-200 bg-white text-sm font-medium hover:bg-slate-50 transition-all flex items-center gap-2">
|
||||||
<Save size={16}/> Salvar Rascunho
|
<Save size={16}/> Salvar Rascunho
|
||||||
</button>
|
</button>
|
||||||
<button className="px-6 py-2 rounded-lg bg-blue-600 text-white text-sm font-medium hover:bg-blue-700 transition-all shadow-lg shadow-blue-200 flex items-center gap-2">
|
<button
|
||||||
|
onClick={handlePublish}
|
||||||
|
className="px-6 py-2 rounded-lg bg-blue-600 text-white text-sm font-medium hover:bg-blue-700 transition-all shadow-lg shadow-blue-200 flex items-center gap-2"
|
||||||
|
>
|
||||||
<Send size={16}/> Publicar Artigo
|
<Send size={16}/> Publicar Artigo
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -216,19 +241,67 @@ export const CreateNewsPage = () => {
|
|||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={categoryId}
|
value={categoryId}
|
||||||
onChange={(e) => setCategoryId(e.target.value)}
|
onChange={(e) => handleCategoryChange(e.target.value)}
|
||||||
disabled={categoriesLoading}
|
disabled={categoriesLoading}
|
||||||
className="w-full bg-slate-50 border border-slate-100 rounded-lg px-3 py-2 text-sm outline-none disabled:opacity-60"
|
className="w-full bg-slate-50 border border-slate-100 rounded-lg px-3 py-2 text-sm outline-none disabled:opacity-60"
|
||||||
>
|
>
|
||||||
<option value="">
|
<option value="">
|
||||||
{categoriesLoading ? "A carregar categorias…" : "Selecione uma categoria"}
|
{categoriesLoading ? "A carregar categorias…" : "Selecione uma categoria"}
|
||||||
</option>
|
</option>
|
||||||
{categories.map((c) => (
|
{parentCategories.map((c) => (
|
||||||
<option key={c.id} value={c.id}>
|
<option key={c.id} value={c.id}>
|
||||||
{c.name}
|
{c.name}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
<p className="mt-2 text-[11px] text-slate-400">
|
||||||
|
Primeiro escolha a categoria principal.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="flex items-center gap-2 text-xs font-bold uppercase text-slate-400 mb-3">
|
||||||
|
<Tag size={14}/> Subcategoria
|
||||||
|
{isSubCategoryRequired && (
|
||||||
|
<span className="text-[9px] text-red-500 font-bold tracking-wide">obrigatório</span>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={subCategoryId}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSubCategoryId(e.target.value);
|
||||||
|
setShowSubCategoryError(false);
|
||||||
|
}}
|
||||||
|
disabled={!categoryId || subCategories.length === 0}
|
||||||
|
className={`w-full bg-slate-50 border rounded-lg px-3 py-2 text-sm outline-none disabled:opacity-60 ${
|
||||||
|
showSubCategoryError ? "border-red-300 bg-red-50/40" : "border-slate-100"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{!categoryId ? (
|
||||||
|
<option value="">Selecione primeiro uma categoria</option>
|
||||||
|
) : subCategories.length === 0 ? (
|
||||||
|
<option value="">Sem subcategorias para esta categoria</option>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<option value="">Selecione uma subcategoria</option>
|
||||||
|
{subCategories.map((c) => (
|
||||||
|
<option key={c.id} value={c.id}>
|
||||||
|
{c.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</select>
|
||||||
|
{showSubCategoryError && (
|
||||||
|
<p className="mt-2 text-[11px] text-red-600">
|
||||||
|
Esta categoria possui subcategorias. Selecione uma subcategoria para publicar.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{!showSubCategoryError && categoryId && subCategories.length > 0 && (
|
||||||
|
<p className="mt-2 text-[11px] text-slate-400">
|
||||||
|
Esta categoria tem {subCategories.length} subcategoria(s) disponivel(is).
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* --- INPUT DE UPLOAD ATUALIZADO --- */}
|
{/* --- INPUT DE UPLOAD ATUALIZADO --- */}
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
export type ApiMethod = "GET" | "POST" | "PATCH" | "DELETE";
|
||||||
|
|
||||||
|
export interface ApiClientConfig {
|
||||||
|
baseUrl: string;
|
||||||
|
getAccessToken?: () => string | Promise<string | undefined> | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ApiError extends Error {
|
||||||
|
status: number;
|
||||||
|
body: unknown;
|
||||||
|
|
||||||
|
constructor(status: number, message: string, body: unknown) {
|
||||||
|
super(message);
|
||||||
|
this.status = status;
|
||||||
|
this.body = body;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ApiClient {
|
||||||
|
private baseUrl: string;
|
||||||
|
private getAccessToken?: ApiClientConfig["getAccessToken"];
|
||||||
|
|
||||||
|
constructor(config: ApiClientConfig) {
|
||||||
|
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
||||||
|
this.getAccessToken = config.getAccessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async buildHeaders(extra?: HeadersInit): Promise<HeadersInit> {
|
||||||
|
const token = this.getAccessToken ? await this.getAccessToken() : undefined;
|
||||||
|
return {
|
||||||
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||||
|
...extra,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildUrl(path: string, query?: Record<string, string | number | boolean | undefined>) {
|
||||||
|
const url = new URL(`${this.baseUrl}${path}`);
|
||||||
|
if (query) {
|
||||||
|
for (const [k, v] of Object.entries(query)) {
|
||||||
|
if (v !== undefined && v !== null) url.searchParams.set(k, String(v));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
async request<TResponse, TBody = undefined>(params: {
|
||||||
|
method: ApiMethod;
|
||||||
|
path: string;
|
||||||
|
query?: Record<string, string | number | boolean | undefined>;
|
||||||
|
body?: TBody;
|
||||||
|
headers?: HeadersInit;
|
||||||
|
isFormData?: boolean;
|
||||||
|
}): Promise<TResponse> {
|
||||||
|
const { method, path, query, body, headers, isFormData } = params;
|
||||||
|
const finalHeaders = await this.buildHeaders(headers);
|
||||||
|
|
||||||
|
const response = await fetch(this.buildUrl(path, query), {
|
||||||
|
method,
|
||||||
|
headers: isFormData ? finalHeaders : { "Content-Type": "application/json", ...finalHeaders },
|
||||||
|
body: body
|
||||||
|
? isFormData
|
||||||
|
? (body as unknown as FormData)
|
||||||
|
: JSON.stringify(body)
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const text = await response.text();
|
||||||
|
const data = text ? safeJsonParse(text) : null;
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new ApiError(response.status, response.statusText, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data as TResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
get<TResponse>(path: string, query?: Record<string, string | number | boolean | undefined>) {
|
||||||
|
return this.request<TResponse>({ method: "GET", path, query });
|
||||||
|
}
|
||||||
|
|
||||||
|
post<TResponse, TBody = undefined>(
|
||||||
|
path: string,
|
||||||
|
body?: TBody,
|
||||||
|
options?: { headers?: HeadersInit; isFormData?: boolean }
|
||||||
|
) {
|
||||||
|
return this.request<TResponse, TBody>({
|
||||||
|
method: "POST",
|
||||||
|
path,
|
||||||
|
body,
|
||||||
|
headers: options?.headers,
|
||||||
|
isFormData: options?.isFormData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
patch<TResponse, TBody = undefined>(path: string, body?: TBody) {
|
||||||
|
return this.request<TResponse, TBody>({ method: "PATCH", path, body });
|
||||||
|
}
|
||||||
|
|
||||||
|
delete<TResponse>(path: string) {
|
||||||
|
return this.request<TResponse>({ method: "DELETE", path });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeJsonParse(text: string) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(text);
|
||||||
|
} catch {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
+215
@@ -0,0 +1,215 @@
|
|||||||
|
export type UserRole = "ADMIN" | "EDITOR" | "AUTHOR" | "READER";
|
||||||
|
export type ArticleStatus = "DRAFT" | "PUBLISHED" | "ARCHIVED";
|
||||||
|
|
||||||
|
export interface DeleteResponseDto {
|
||||||
|
deleted: true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserOutputDto {
|
||||||
|
id: string;
|
||||||
|
keycloakId: string;
|
||||||
|
email: string;
|
||||||
|
displayName: string | null;
|
||||||
|
avatarKey: string | null;
|
||||||
|
role: UserRole;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CategoryOutputDto {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
parentId: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CategoryTreeOutputDto extends CategoryOutputDto {
|
||||||
|
children: CategoryTreeOutputDto[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateCategoryInputDto {
|
||||||
|
name: string;
|
||||||
|
slug?: string;
|
||||||
|
parentId?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateCategoryInputDto {
|
||||||
|
name?: string;
|
||||||
|
slug?: string;
|
||||||
|
parentId?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TagOutputDto {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateTagInputDto {
|
||||||
|
name: string;
|
||||||
|
slug?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateTagInputDto {
|
||||||
|
name?: string;
|
||||||
|
slug?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ImageOutputDto {
|
||||||
|
id: string;
|
||||||
|
fileKey: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UploadedImageOutputDto extends ImageOutputDto {
|
||||||
|
urls: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ArticleAuthorOutputDto {
|
||||||
|
id: string;
|
||||||
|
displayName: string | null;
|
||||||
|
email?: string;
|
||||||
|
avatarKey: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ArticleCategoryLinkOutputDto {
|
||||||
|
articleId: string;
|
||||||
|
categoryId: string;
|
||||||
|
category: CategoryOutputDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ArticleTagLinkOutputDto {
|
||||||
|
articleId: string;
|
||||||
|
tagId: string;
|
||||||
|
tag: TagOutputDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ArticleImageLinkOutputDto {
|
||||||
|
articleId: string;
|
||||||
|
imageId: string;
|
||||||
|
sortOrder: number;
|
||||||
|
image: ImageOutputDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ArticleOutputDto {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
slug: string;
|
||||||
|
content: string;
|
||||||
|
excerpt: string | null;
|
||||||
|
status: ArticleStatus;
|
||||||
|
publishedAt: string | null;
|
||||||
|
authorId: string;
|
||||||
|
author: ArticleAuthorOutputDto;
|
||||||
|
categories: ArticleCategoryLinkOutputDto[];
|
||||||
|
tags: ArticleTagLinkOutputDto[];
|
||||||
|
images: ArticleImageLinkOutputDto[];
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ListArticlesInputDto {
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
search?: string;
|
||||||
|
categoryId?: string;
|
||||||
|
tagId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ManageArticlesInputDto extends ListArticlesInputDto {
|
||||||
|
status?: ArticleStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateArticleInputDto {
|
||||||
|
title: string;
|
||||||
|
slug?: string;
|
||||||
|
content: string;
|
||||||
|
excerpt?: string;
|
||||||
|
status?: ArticleStatus;
|
||||||
|
categoryIds?: string[];
|
||||||
|
tagIds?: string[];
|
||||||
|
imageIds?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateArticleInputDto {
|
||||||
|
title?: string;
|
||||||
|
slug?: string;
|
||||||
|
content?: string;
|
||||||
|
excerpt?: string;
|
||||||
|
status?: ArticleStatus;
|
||||||
|
categoryIds?: string[];
|
||||||
|
tagIds?: string[];
|
||||||
|
imageIds?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AttachImageInputDto {
|
||||||
|
imageId: string;
|
||||||
|
sortOrder?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginatedArticlesOutputDto {
|
||||||
|
items: ArticleOutputDto[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CommentUserOutputDto {
|
||||||
|
id: string;
|
||||||
|
displayName: string | null;
|
||||||
|
avatarKey: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CommentOutputDto {
|
||||||
|
id: string;
|
||||||
|
content: string;
|
||||||
|
articleId: string;
|
||||||
|
userId: string;
|
||||||
|
parentId: string | null;
|
||||||
|
user: CommentUserOutputDto;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CommentTreeOutputDto extends CommentOutputDto {
|
||||||
|
replies: CommentTreeOutputDto[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateCommentInputDto {
|
||||||
|
content: string;
|
||||||
|
parentId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BookmarkOutputDto {
|
||||||
|
userId: string;
|
||||||
|
articleId: string;
|
||||||
|
createdAt: string;
|
||||||
|
article: ArticleOutputDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginatedBookmarksOutputDto {
|
||||||
|
items: BookmarkOutputDto[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProfileOutputDto {
|
||||||
|
keycloak: {
|
||||||
|
email?: string;
|
||||||
|
name?: string;
|
||||||
|
picture?: string;
|
||||||
|
email_verified?: boolean;
|
||||||
|
roles: string[];
|
||||||
|
};
|
||||||
|
user: UserOutputDto | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateMeInputDto {
|
||||||
|
displayName?: string;
|
||||||
|
avatarKey?: string;
|
||||||
|
}
|
||||||
+101
@@ -0,0 +1,101 @@
|
|||||||
|
import { ApiClient } from "./api-client";
|
||||||
|
import type {
|
||||||
|
AttachImageInputDto,
|
||||||
|
ArticleOutputDto,
|
||||||
|
CategoryOutputDto,
|
||||||
|
CategoryTreeOutputDto,
|
||||||
|
CommentOutputDto,
|
||||||
|
CommentTreeOutputDto,
|
||||||
|
CreateArticleInputDto,
|
||||||
|
CreateCategoryInputDto,
|
||||||
|
CreateCommentInputDto,
|
||||||
|
CreateTagInputDto,
|
||||||
|
DeleteResponseDto,
|
||||||
|
ListArticlesInputDto,
|
||||||
|
ManageArticlesInputDto,
|
||||||
|
PaginatedArticlesOutputDto,
|
||||||
|
PaginatedBookmarksOutputDto,
|
||||||
|
ProfileOutputDto,
|
||||||
|
TagOutputDto,
|
||||||
|
UpdateArticleInputDto,
|
||||||
|
UpdateCategoryInputDto,
|
||||||
|
UpdateMeInputDto,
|
||||||
|
UpdateTagInputDto,
|
||||||
|
UploadedImageOutputDto,
|
||||||
|
UserOutputDto,
|
||||||
|
BookmarkOutputDto,
|
||||||
|
} from "./dtos";
|
||||||
|
|
||||||
|
type ApiQuery = Record<string, string | number | boolean | undefined>;
|
||||||
|
|
||||||
|
export function createTvOneApiServices(api: ApiClient) {
|
||||||
|
return {
|
||||||
|
users: {
|
||||||
|
me: () => api.get<UserOutputDto>("/users/me"),
|
||||||
|
updateMe: (dto: UpdateMeInputDto) => api.patch<UserOutputDto, UpdateMeInputDto>("/users/me", dto),
|
||||||
|
},
|
||||||
|
|
||||||
|
profile: {
|
||||||
|
get: () => api.get<ProfileOutputDto>("/profile"),
|
||||||
|
},
|
||||||
|
|
||||||
|
categories: {
|
||||||
|
tree: () => api.get<CategoryTreeOutputDto[]>("/categories"),
|
||||||
|
flat: () => api.get<CategoryOutputDto[]>("/categories/flat"),
|
||||||
|
create: (dto: CreateCategoryInputDto) =>
|
||||||
|
api.post<CategoryOutputDto, CreateCategoryInputDto>("/categories", dto),
|
||||||
|
update: (id: string, dto: UpdateCategoryInputDto) =>
|
||||||
|
api.patch<CategoryOutputDto, UpdateCategoryInputDto>(`/categories/${id}`, dto),
|
||||||
|
remove: (id: string) => api.delete<DeleteResponseDto>(`/categories/${id}`),
|
||||||
|
},
|
||||||
|
|
||||||
|
tags: {
|
||||||
|
list: () => api.get<TagOutputDto[]>("/tags"),
|
||||||
|
create: (dto: CreateTagInputDto) => api.post<TagOutputDto, CreateTagInputDto>("/tags", dto),
|
||||||
|
update: (id: string, dto: UpdateTagInputDto) =>
|
||||||
|
api.patch<TagOutputDto, UpdateTagInputDto>(`/tags/${id}`, dto),
|
||||||
|
remove: (id: string) => api.delete<DeleteResponseDto>(`/tags/${id}`),
|
||||||
|
},
|
||||||
|
|
||||||
|
articles: {
|
||||||
|
listPublished: (query?: ListArticlesInputDto) =>
|
||||||
|
api.get<PaginatedArticlesOutputDto>("/articles", query as ApiQuery | undefined),
|
||||||
|
findPublishedBySlug: (slug: string) => api.get<ArticleOutputDto>(`/articles/by-slug/${slug}`),
|
||||||
|
listManage: (query?: ManageArticlesInputDto) =>
|
||||||
|
api.get<PaginatedArticlesOutputDto>("/articles/manage", query as ApiQuery | undefined),
|
||||||
|
findById: (id: string) => api.get<ArticleOutputDto>(`/articles/by-id/${id}`),
|
||||||
|
create: (dto: CreateArticleInputDto) =>
|
||||||
|
api.post<ArticleOutputDto, CreateArticleInputDto>("/articles", dto),
|
||||||
|
update: (id: string, dto: UpdateArticleInputDto) =>
|
||||||
|
api.patch<ArticleOutputDto, UpdateArticleInputDto>(`/articles/${id}`, dto),
|
||||||
|
remove: (id: string) => api.delete<DeleteResponseDto>(`/articles/${id}`),
|
||||||
|
attachImage: (id: string, dto: AttachImageInputDto) =>
|
||||||
|
api.post<ArticleOutputDto, AttachImageInputDto>(`/articles/${id}/images`, dto),
|
||||||
|
},
|
||||||
|
|
||||||
|
comments: {
|
||||||
|
listForArticle: (articleId: string) =>
|
||||||
|
api.get<CommentTreeOutputDto[]>(`/comments/article/${articleId}`),
|
||||||
|
create: (articleId: string, dto: CreateCommentInputDto) =>
|
||||||
|
api.post<CommentOutputDto, CreateCommentInputDto>(`/comments/article/${articleId}`, dto),
|
||||||
|
remove: (id: string) => api.delete<DeleteResponseDto>(`/comments/${id}`),
|
||||||
|
},
|
||||||
|
|
||||||
|
bookmarks: {
|
||||||
|
listMine: (query?: { page?: number; limit?: number }) =>
|
||||||
|
api.get<PaginatedBookmarksOutputDto>("/bookmarks/me", query),
|
||||||
|
add: (articleId: string) => api.post<BookmarkOutputDto>(`/bookmarks/${articleId}`),
|
||||||
|
remove: (articleId: string) => api.delete<DeleteResponseDto>(`/bookmarks/${articleId}`),
|
||||||
|
},
|
||||||
|
|
||||||
|
images: {
|
||||||
|
upload: async (file: File) => {
|
||||||
|
const form = new FormData();
|
||||||
|
form.append("file", file);
|
||||||
|
return api.post<UploadedImageOutputDto, FormData>("/images/upload", form, {
|
||||||
|
isFormData: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user