posts are done

This commit is contained in:
2026-05-19 00:08:02 +02:00
parent 202ce22102
commit 2e9e3ed41b
35 changed files with 1528 additions and 272 deletions

View File

@@ -0,0 +1,9 @@
/**
* Generated by orval v8.8.0 🍺
* Do not edit manually.
* OpenAPI spec version: 0.0.0
*/
export type ApiSocialPostsSaveCreate200 = {
saved?: boolean;
};

View File

@@ -0,0 +1,23 @@
/**
* Generated by orval v8.8.0 🍺
* Do not edit manually.
* OpenAPI spec version: 0.0.0
*/
export type ApiSocialPostsSavedListParams = {
author?: number;
hub?: number;
/**
* Which field to use when ordering the results.
*/
ordering?: string;
/**
* A page number within the paginated result set.
*/
page?: number;
reply_to?: number;
/**
* A search term.
*/
search?: string;
};

View File

@@ -29,6 +29,8 @@ export * from "./apiSocialMessagesListParams";
export * from "./apiSocialPostsFeedListParams";
export * from "./apiSocialPostsListParams";
export * from "./apiSocialPostsMediaCreateBody";
export * from "./apiSocialPostsSaveCreate200";
export * from "./apiSocialPostsSavedListParams";
export * from "./apiZasilkovnaShipmentsListParams";
export * from "./authorMinimal";
export * from "./callback";

View File

@@ -23,4 +23,6 @@ export interface PatchedPost {
readonly vote_score?: string;
readonly user_vote?: string;
readonly reply_count?: number;
readonly is_saved?: string;
readonly save_count?: string;
}

View File

@@ -23,4 +23,6 @@ export interface Post {
readonly vote_score: string;
readonly user_vote: string;
readonly reply_count: number;
readonly is_saved: string;
readonly save_count: string;
}

View File

@@ -23,6 +23,8 @@ import type {
ApiSocialPostsFeedListParams,
ApiSocialPostsListParams,
ApiSocialPostsMediaCreateBody,
ApiSocialPostsSaveCreate200,
ApiSocialPostsSavedListParams,
PaginatedPostList,
PatchedPost,
Post,
@@ -806,6 +808,94 @@ export const useApiSocialPostsMediaCreate = <
queryClient,
);
};
/**
* Saves the post for the current user, or unsaves it if already saved. Returns `{saved: true/false}`.
* @summary Toggle save on a post
*/
export const apiSocialPostsSaveCreate = (
id: number,
post: NonReadonly<Post>,
signal?: AbortSignal,
) => {
return privateMutator<ApiSocialPostsSaveCreate200>({
url: `/api/social/posts/${id}/save/`,
method: "POST",
headers: { "Content-Type": "application/json" },
data: post,
signal,
});
};
export const getApiSocialPostsSaveCreateMutationOptions = <
TError = unknown,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof apiSocialPostsSaveCreate>>,
TError,
{ id: number; data: NonReadonly<Post> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof apiSocialPostsSaveCreate>>,
TError,
{ id: number; data: NonReadonly<Post> },
TContext
> => {
const mutationKey = ["apiSocialPostsSaveCreate"];
const { mutation: mutationOptions } = options
? options.mutation &&
"mutationKey" in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof apiSocialPostsSaveCreate>>,
{ id: number; data: NonReadonly<Post> }
> = (props) => {
const { id, data } = props ?? {};
return apiSocialPostsSaveCreate(id, data);
};
return { mutationFn, ...mutationOptions };
};
export type ApiSocialPostsSaveCreateMutationResult = NonNullable<
Awaited<ReturnType<typeof apiSocialPostsSaveCreate>>
>;
export type ApiSocialPostsSaveCreateMutationBody = NonReadonly<Post>;
export type ApiSocialPostsSaveCreateMutationError = unknown;
/**
* @summary Toggle save on a post
*/
export const useApiSocialPostsSaveCreate = <
TError = unknown,
TContext = unknown,
>(
options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof apiSocialPostsSaveCreate>>,
TError,
{ id: number; data: NonReadonly<Post> },
TContext
>;
},
queryClient?: QueryClient,
): UseMutationResult<
Awaited<ReturnType<typeof apiSocialPostsSaveCreate>>,
TError,
{ id: number; data: NonReadonly<Post> },
TContext
> => {
return useMutation(
getApiSocialPostsSaveCreateMutationOptions(options),
queryClient,
);
};
/**
* Attaches an existing hub tag to the post. The tag must belong to the same hub as the post. Any authenticated hub member can attach tags.
* @summary Attach a tag to a post
@@ -1229,3 +1319,162 @@ export function useApiSocialPostsFeedList<
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary List posts saved by the current user
*/
export const apiSocialPostsSavedList = (
params?: ApiSocialPostsSavedListParams,
signal?: AbortSignal,
) => {
return privateMutator<PaginatedPostList>({
url: `/api/social/posts/saved/`,
method: "GET",
params,
signal,
});
};
export const getApiSocialPostsSavedListQueryKey = (
params?: ApiSocialPostsSavedListParams,
) => {
return [`/api/social/posts/saved/`, ...(params ? [params] : [])] as const;
};
export const getApiSocialPostsSavedListQueryOptions = <
TData = Awaited<ReturnType<typeof apiSocialPostsSavedList>>,
TError = unknown,
>(
params?: ApiSocialPostsSavedListParams,
options?: {
query?: Partial<
UseQueryOptions<
Awaited<ReturnType<typeof apiSocialPostsSavedList>>,
TError,
TData
>
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getApiSocialPostsSavedListQueryKey(params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof apiSocialPostsSavedList>>
> = ({ signal }) => apiSocialPostsSavedList(params, signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof apiSocialPostsSavedList>>,
TError,
TData
> & { queryKey: DataTag<QueryKey, TData, TError> };
};
export type ApiSocialPostsSavedListQueryResult = NonNullable<
Awaited<ReturnType<typeof apiSocialPostsSavedList>>
>;
export type ApiSocialPostsSavedListQueryError = unknown;
export function useApiSocialPostsSavedList<
TData = Awaited<ReturnType<typeof apiSocialPostsSavedList>>,
TError = unknown,
>(
params: undefined | ApiSocialPostsSavedListParams,
options: {
query: Partial<
UseQueryOptions<
Awaited<ReturnType<typeof apiSocialPostsSavedList>>,
TError,
TData
>
> &
Pick<
DefinedInitialDataOptions<
Awaited<ReturnType<typeof apiSocialPostsSavedList>>,
TError,
Awaited<ReturnType<typeof apiSocialPostsSavedList>>
>,
"initialData"
>;
},
queryClient?: QueryClient,
): DefinedUseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
export function useApiSocialPostsSavedList<
TData = Awaited<ReturnType<typeof apiSocialPostsSavedList>>,
TError = unknown,
>(
params?: ApiSocialPostsSavedListParams,
options?: {
query?: Partial<
UseQueryOptions<
Awaited<ReturnType<typeof apiSocialPostsSavedList>>,
TError,
TData
>
> &
Pick<
UndefinedInitialDataOptions<
Awaited<ReturnType<typeof apiSocialPostsSavedList>>,
TError,
Awaited<ReturnType<typeof apiSocialPostsSavedList>>
>,
"initialData"
>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
export function useApiSocialPostsSavedList<
TData = Awaited<ReturnType<typeof apiSocialPostsSavedList>>,
TError = unknown,
>(
params?: ApiSocialPostsSavedListParams,
options?: {
query?: Partial<
UseQueryOptions<
Awaited<ReturnType<typeof apiSocialPostsSavedList>>,
TError,
TData
>
>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
/**
* @summary List posts saved by the current user
*/
export function useApiSocialPostsSavedList<
TData = Awaited<ReturnType<typeof apiSocialPostsSavedList>>,
TError = unknown,
>(
params?: ApiSocialPostsSavedListParams,
options?: {
query?: Partial<
UseQueryOptions<
Awaited<ReturnType<typeof apiSocialPostsSavedList>>,
TError,
TData
>
>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
} {
const queryOptions = getApiSocialPostsSavedListQueryOptions(params, options);
const query = useQuery(queryOptions, queryClient) as UseQueryResult<
TData,
TError
> & { queryKey: DataTag<QueryKey, TData, TError> };
return { ...query, queryKey: queryOptions.queryKey };
}

View File

@@ -23,4 +23,6 @@ export interface PatchedPost {
readonly vote_score?: string;
readonly user_vote?: string;
readonly reply_count?: number;
readonly is_saved?: string;
readonly save_count?: string;
}

View File

@@ -23,4 +23,6 @@ export interface Post {
readonly vote_score: string;
readonly user_vote: string;
readonly reply_count: number;
readonly is_saved: string;
readonly save_count: string;
}

View File

@@ -1,31 +1,89 @@
import axios, { type AxiosRequestConfig } from "axios";
import { AUTH_FLAG } from "@/context/AuthContext";
// použij tohle pro API vyžadující autentizaci
export const privateApi = axios.create({
withCredentials: true, // potřebuje HttpOnly cookies
withCredentials: true,
baseURL: '',
});
// Set baseURL at runtime (using Function to hide from orval's esbuild)
try {
const getEnv = new Function('return import.meta.env.VITE_BACKEND_URL');
privateApi.defaults.baseURL = getEnv() || "http://localhost:8000";
} catch {
privateApi.defaults.baseURL = "http://localhost:8000";
}
let isRefreshing = false;
let failedQueue: Array<{
resolve: (value?: unknown) => void;
reject: (reason?: any) => void;
}> = [];
const processQueue = (error: any = null) => {
failedQueue.forEach((promise) => {
if (error) {
promise.reject(error);
} else {
promise.resolve();
}
});
failedQueue = [];
};
privateApi.interceptors.response.use(
(res) => res,
async (error) => {
const original = error.config;
if (error.response?.status === 400 && error.response?.data) {
const data = error.response.data;
if (typeof data === 'object' && !Array.isArray(data)) {
const firstKey = Object.keys(data)[0];
if (firstKey && Array.isArray(data[firstKey]) && data[firstKey].length > 0) {
error.message = data[firstKey][0];
}
}
}
if (error.response?.status === 401 && !original._retry) {
if (original.url?.includes("/api/account/logout/")) {
return Promise.reject(error);
}
if (error.response?.data?.code === "user_not_found") {
processQueue(error);
isRefreshing = false;
localStorage.removeItem(AUTH_FLAG);
window.location.href = "/social/login";
return Promise.reject(error);
}
if (original.url?.includes("/api/account/token/refresh/")) {
processQueue(error);
isRefreshing = false;
localStorage.removeItem(AUTH_FLAG);
try {
await privateApi.post("/api/account/logout/");
} catch {
// cookies may already be expired
}
window.location.href = "/social/login";
return Promise.reject(error);
}
if (isRefreshing) {
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
})
.then(() => privateApi(original))
.catch((err) => Promise.reject(err));
}
original._retry = true;
isRefreshing = true;
try {
await privateApi.post("/api/account/token/refresh/");
processQueue();
return privateApi(original);
} catch {
// optional: logout
} catch (refreshError) {
processQueue(refreshError);
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
}
@@ -33,15 +91,10 @@ privateApi.interceptors.response.use(
}
);
export const privateMutator = async <T>(
config: AxiosRequestConfig
): Promise<T> => {
// If sending FormData, remove Content-Type header to let axios set it with boundary
export const privateMutator = async <T>(config: AxiosRequestConfig): Promise<T> => {
if (config.data instanceof FormData) {
delete config.headers?.['Content-Type'];
}
const response = await privateApi.request<T>(config);
return response.data;
};
};

View File

@@ -1,23 +1,46 @@
import axios, { type AxiosRequestConfig } from "axios";
import { AUTH_FLAG } from "@/context/AuthContext";
// použij tohle pro veřejné API nevyžadující autentizaci
export const publicApi = axios.create({
withCredentials: false, // veřejné API NEPOSÍLÁ cookies
withCredentials: true,
baseURL: '',
});
// Set baseURL at runtime (using Function to hide from orval's esbuild)
try {
const getEnv = new Function('return import.meta.env.VITE_BACKEND_URL');
publicApi.defaults.baseURL = getEnv() || "http://localhost:8000";
} catch {
publicApi.defaults.baseURL = "http://localhost:8000";
}
publicApi.interceptors.response.use(
(res) => res,
(error) => {
const url = error.config?.url ?? '';
if (
error.response?.status === 401 &&
error.response?.data?.code === "user_not_found" &&
!url.includes("/api/account/logout/")
) {
localStorage.removeItem(AUTH_FLAG);
window.location.href = "/social/login";
}
return Promise.reject(error);
}
);
const pendingRequests = new Map();
// ⬇⬇⬇ TOHLE JE TEN MUTATOR ⬇⬇⬇
export const publicMutator = async <T>(
config: AxiosRequestConfig
): Promise<T> => {
const response = await publicApi.request<T>(config);
return response.data;
};
export const publicMutator = async <T>(config: AxiosRequestConfig): Promise<T> => {
const requestKey = `${config.method}_${config.url}_${JSON.stringify(config.data || '')}`;
if (pendingRequests.has(requestKey)) {
return pendingRequests.get(requestKey);
}
const requestPromise = (async () => {
try {
const response = await publicApi.request<T>(config);
return response.data;
} finally {
pendingRequests.delete(requestKey);
}
})();
pendingRequests.set(requestKey, requestPromise);
return requestPromise;
};