added frontend for social + feed partiali working

This commit is contained in:
2026-05-18 02:25:47 +02:00
parent e1df55df0e
commit 202ce22102
88 changed files with 4236 additions and 737 deletions

View File

@@ -5,14 +5,18 @@
*/
export type ApiSocialMessagesListParams = {
/**
* The pagination cursor value.
*/
cursor?: string;
/**
* Which field to use when ordering the results.
*/
ordering?: string;
/**
* A page number within the paginated result set.
* Number of results to return per page.
*/
page?: number;
page_size?: number;
/**
* A search term.
*/

View File

@@ -0,0 +1,31 @@
/**
* Generated by orval v8.8.0 🍺
* Do not edit manually.
* OpenAPI spec version: 0.0.0
*/
export type ApiSocialPostsFeedListParams = {
author?: number;
/**
* Opaque pagination cursor.
*/
cursor?: string;
/**
* Algorithm key, default `recent`.
*/
feed_strategy?: string;
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

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

View File

@@ -0,0 +1,20 @@
/**
* Generated by orval v8.8.0 🍺
* Do not edit manually.
* OpenAPI spec version: 0.0.0
*/
export interface AuthorMinimal {
readonly id: number;
/**
* Požadováno. 150 znaků nebo méně. Pouze písmena, číslice a znaky @/./+/-/_.
* @maxLength 150
* @pattern ^[\w.@+-]+$
*/
username: string;
/** @maxLength 150 */
first_name?: string;
/** @maxLength 150 */
last_name?: string;
readonly avatar: string;
}

View File

@@ -42,4 +42,6 @@ export interface CustomUser {
postal_code?: string | null;
readonly gdpr: boolean;
is_active?: boolean;
/** @nullable */
avatar?: string | null;
}

View File

@@ -26,8 +26,11 @@ export * from "./apiSocialHubsModeratorsListParams";
export * from "./apiSocialHubsTagsListParams";
export * from "./apiSocialChatsListParams";
export * from "./apiSocialMessagesListParams";
export * from "./apiSocialPostsFeedListParams";
export * from "./apiSocialPostsListParams";
export * from "./apiSocialPostsMediaCreateBody";
export * from "./apiZasilkovnaShipmentsListParams";
export * from "./authorMinimal";
export * from "./callback";
export * from "./carrierRead";
export * from "./cart";

View File

@@ -6,7 +6,6 @@
import type { Message } from "./message";
export interface PaginatedMessageList {
count: number;
/** @nullable */
next?: string | null;
/** @nullable */

View File

@@ -42,4 +42,6 @@ export interface PatchedCustomUser {
postal_code?: string | null;
readonly gdpr?: boolean;
is_active?: boolean;
/** @nullable */
avatar?: string | null;
}

View File

@@ -3,6 +3,7 @@
* Do not edit manually.
* OpenAPI spec version: 0.0.0
*/
import type { AuthorMinimal } from "./authorMinimal";
import type { PostContent } from "./postContent";
import type { Tags } from "./tags";
@@ -12,10 +13,14 @@ export interface PatchedPost {
readonly created_at?: Date;
readonly updated_at?: Date;
readonly author?: number;
readonly author_detail?: AuthorMinimal;
/** @nullable */
hub?: number | null;
/** @nullable */
reply_to?: number | null;
readonly tags?: readonly Tags[];
readonly contents?: readonly PostContent[];
readonly vote_score?: string;
readonly user_vote?: string;
readonly reply_count?: number;
}

View File

@@ -3,6 +3,7 @@
* Do not edit manually.
* OpenAPI spec version: 0.0.0
*/
import type { AuthorMinimal } from "./authorMinimal";
import type { PostContent } from "./postContent";
import type { Tags } from "./tags";
@@ -12,10 +13,14 @@ export interface Post {
readonly created_at: Date;
readonly updated_at: Date;
readonly author: number;
readonly author_detail: AuthorMinimal;
/** @nullable */
hub?: number | null;
/** @nullable */
reply_to?: number | null;
readonly tags: readonly Tags[];
readonly contents: readonly PostContent[];
readonly vote_score: string;
readonly user_vote: string;
readonly reply_count: number;
}

View File

@@ -5,16 +5,22 @@
*/
export interface UserRegistration {
/**
* Užívatelské jméno
* @maxLength 150
* @pattern ^[\w.@+-]+$
*/
username?: string;
/**
* Křestní jméno uživatele
* @maxLength 150
*/
first_name: string;
first_name?: string;
/**
* Příjmení uživatele
* @maxLength 150
*/
last_name: string;
last_name?: string;
/**
* Emailová adresa uživatele
* @maxLength 254
@@ -26,7 +32,7 @@ export interface UserRegistration {
* @nullable
* @pattern ^\+?\d{9,15}$
*/
phone_number: string | null;
phone_number?: string | null;
/** Heslo musí mít alespoň 8 znaků, obsahovat velká a malá písmena a číslici. */
password: string;
/**
@@ -34,20 +40,20 @@ export interface UserRegistration {
* @maxLength 100
* @nullable
*/
city: string | null;
city?: string | null;
/**
* Ulice uživatele
* @maxLength 200
* @nullable
*/
street: string | null;
street?: string | null;
/**
* PSČ uživatele
* @maxLength 5
* @nullable
* @pattern ^\d{5}$
*/
postal_code: string | null;
postal_code?: string | null;
/** Souhlas se zpracováním osobních údajů */
gdpr: boolean;
}

View File

@@ -20,10 +20,13 @@ import type {
} from "@tanstack/react-query";
import type {
ApiSocialPostsFeedListParams,
ApiSocialPostsListParams,
ApiSocialPostsMediaCreateBody,
PaginatedPostList,
PatchedPost,
Post,
PostContent,
PostVote,
TagAttach,
} from "../models";
@@ -712,6 +715,97 @@ export const useApiSocialPostsDestroy = <TError = unknown, TContext = unknown>(
queryClient,
);
};
/**
* Attach an image or video file to a post. Only the post author can upload.
* @summary Upload media to a post
*/
export const apiSocialPostsMediaCreate = (
id: number,
apiSocialPostsMediaCreateBody: ApiSocialPostsMediaCreateBody,
signal?: AbortSignal,
) => {
const formData = new FormData();
formData.append(`file`, apiSocialPostsMediaCreateBody.file);
return privateMutator<PostContent>({
url: `/api/social/posts/${id}/media/`,
method: "POST",
data: formData,
signal,
});
};
export const getApiSocialPostsMediaCreateMutationOptions = <
TError = unknown,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof apiSocialPostsMediaCreate>>,
TError,
{ id: number; data: ApiSocialPostsMediaCreateBody },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof apiSocialPostsMediaCreate>>,
TError,
{ id: number; data: ApiSocialPostsMediaCreateBody },
TContext
> => {
const mutationKey = ["apiSocialPostsMediaCreate"];
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 apiSocialPostsMediaCreate>>,
{ id: number; data: ApiSocialPostsMediaCreateBody }
> = (props) => {
const { id, data } = props ?? {};
return apiSocialPostsMediaCreate(id, data);
};
return { mutationFn, ...mutationOptions };
};
export type ApiSocialPostsMediaCreateMutationResult = NonNullable<
Awaited<ReturnType<typeof apiSocialPostsMediaCreate>>
>;
export type ApiSocialPostsMediaCreateMutationBody =
ApiSocialPostsMediaCreateBody;
export type ApiSocialPostsMediaCreateMutationError = unknown;
/**
* @summary Upload media to a post
*/
export const useApiSocialPostsMediaCreate = <
TError = unknown,
TContext = unknown,
>(
options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof apiSocialPostsMediaCreate>>,
TError,
{ id: number; data: ApiSocialPostsMediaCreateBody },
TContext
>;
},
queryClient?: QueryClient,
): UseMutationResult<
Awaited<ReturnType<typeof apiSocialPostsMediaCreate>>,
TError,
{ id: number; data: ApiSocialPostsMediaCreateBody },
TContext
> => {
return useMutation(
getApiSocialPostsMediaCreateMutationOptions(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
@@ -976,3 +1070,162 @@ export const useApiSocialPostsVoteCreate = <
queryClient,
);
};
/**
* Returns a cursor-paginated stream of top-level posts (excluding replies) aggregated from the user's joined hubs, public hubs, and hub-less posts. Pass `feed_strategy` to switch between ranking algorithms (currently only `recent` is implemented; reserved for future custom algorithms).
* @summary Get the user's post feed
*/
export const apiSocialPostsFeedList = (
params?: ApiSocialPostsFeedListParams,
signal?: AbortSignal,
) => {
return privateMutator<PaginatedPostList>({
url: `/api/social/posts/feed/`,
method: "GET",
params,
signal,
});
};
export const getApiSocialPostsFeedListQueryKey = (
params?: ApiSocialPostsFeedListParams,
) => {
return [`/api/social/posts/feed/`, ...(params ? [params] : [])] as const;
};
export const getApiSocialPostsFeedListQueryOptions = <
TData = Awaited<ReturnType<typeof apiSocialPostsFeedList>>,
TError = unknown,
>(
params?: ApiSocialPostsFeedListParams,
options?: {
query?: Partial<
UseQueryOptions<
Awaited<ReturnType<typeof apiSocialPostsFeedList>>,
TError,
TData
>
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getApiSocialPostsFeedListQueryKey(params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof apiSocialPostsFeedList>>
> = ({ signal }) => apiSocialPostsFeedList(params, signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof apiSocialPostsFeedList>>,
TError,
TData
> & { queryKey: DataTag<QueryKey, TData, TError> };
};
export type ApiSocialPostsFeedListQueryResult = NonNullable<
Awaited<ReturnType<typeof apiSocialPostsFeedList>>
>;
export type ApiSocialPostsFeedListQueryError = unknown;
export function useApiSocialPostsFeedList<
TData = Awaited<ReturnType<typeof apiSocialPostsFeedList>>,
TError = unknown,
>(
params: undefined | ApiSocialPostsFeedListParams,
options: {
query: Partial<
UseQueryOptions<
Awaited<ReturnType<typeof apiSocialPostsFeedList>>,
TError,
TData
>
> &
Pick<
DefinedInitialDataOptions<
Awaited<ReturnType<typeof apiSocialPostsFeedList>>,
TError,
Awaited<ReturnType<typeof apiSocialPostsFeedList>>
>,
"initialData"
>;
},
queryClient?: QueryClient,
): DefinedUseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
export function useApiSocialPostsFeedList<
TData = Awaited<ReturnType<typeof apiSocialPostsFeedList>>,
TError = unknown,
>(
params?: ApiSocialPostsFeedListParams,
options?: {
query?: Partial<
UseQueryOptions<
Awaited<ReturnType<typeof apiSocialPostsFeedList>>,
TError,
TData
>
> &
Pick<
UndefinedInitialDataOptions<
Awaited<ReturnType<typeof apiSocialPostsFeedList>>,
TError,
Awaited<ReturnType<typeof apiSocialPostsFeedList>>
>,
"initialData"
>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
export function useApiSocialPostsFeedList<
TData = Awaited<ReturnType<typeof apiSocialPostsFeedList>>,
TError = unknown,
>(
params?: ApiSocialPostsFeedListParams,
options?: {
query?: Partial<
UseQueryOptions<
Awaited<ReturnType<typeof apiSocialPostsFeedList>>,
TError,
TData
>
>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
/**
* @summary Get the user's post feed
*/
export function useApiSocialPostsFeedList<
TData = Awaited<ReturnType<typeof apiSocialPostsFeedList>>,
TError = unknown,
>(
params?: ApiSocialPostsFeedListParams,
options?: {
query?: Partial<
UseQueryOptions<
Awaited<ReturnType<typeof apiSocialPostsFeedList>>,
TError,
TData
>
>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
} {
const queryOptions = getApiSocialPostsFeedListQueryOptions(params, options);
const query = useQuery(queryOptions, queryClient) as UseQueryResult<
TData,
TError
> & { queryKey: DataTag<QueryKey, TData, TError> };
return { ...query, queryKey: queryOptions.queryKey };
}

View File

@@ -0,0 +1,20 @@
/**
* Generated by orval v8.8.0 🍺
* Do not edit manually.
* OpenAPI spec version: 0.0.0
*/
export interface AuthorMinimal {
readonly id: number;
/**
* Požadováno. 150 znaků nebo méně. Pouze písmena, číslice a znaky @/./+/-/_.
* @maxLength 150
* @pattern ^[\w.@+-]+$
*/
username: string;
/** @maxLength 150 */
first_name?: string;
/** @maxLength 150 */
last_name?: string;
readonly avatar: string;
}

View File

@@ -42,4 +42,6 @@ export interface CustomUser {
postal_code?: string | null;
readonly gdpr: boolean;
is_active?: boolean;
/** @nullable */
avatar?: string | null;
}

View File

@@ -17,6 +17,7 @@ export * from "./apiDownloaderDownloadRetrieveParams";
export * from "./apiChoicesRetrieve200";
export * from "./apiChoicesRetrieve200Item";
export * from "./apiChoicesRetrieveParams";
export * from "./authorMinimal";
export * from "./callback";
export * from "./carrierRead";
export * from "./cart";

View File

@@ -6,7 +6,6 @@
import type { Message } from "./message";
export interface PaginatedMessageList {
count: number;
/** @nullable */
next?: string | null;
/** @nullable */

View File

@@ -42,4 +42,6 @@ export interface PatchedCustomUser {
postal_code?: string | null;
readonly gdpr?: boolean;
is_active?: boolean;
/** @nullable */
avatar?: string | null;
}

View File

@@ -3,6 +3,7 @@
* Do not edit manually.
* OpenAPI spec version: 0.0.0
*/
import type { AuthorMinimal } from "./authorMinimal";
import type { PostContent } from "./postContent";
import type { Tags } from "./tags";
@@ -12,10 +13,14 @@ export interface PatchedPost {
readonly created_at?: Date;
readonly updated_at?: Date;
readonly author?: number;
readonly author_detail?: AuthorMinimal;
/** @nullable */
hub?: number | null;
/** @nullable */
reply_to?: number | null;
readonly tags?: readonly Tags[];
readonly contents?: readonly PostContent[];
readonly vote_score?: string;
readonly user_vote?: string;
readonly reply_count?: number;
}

View File

@@ -3,6 +3,7 @@
* Do not edit manually.
* OpenAPI spec version: 0.0.0
*/
import type { AuthorMinimal } from "./authorMinimal";
import type { PostContent } from "./postContent";
import type { Tags } from "./tags";
@@ -12,10 +13,14 @@ export interface Post {
readonly created_at: Date;
readonly updated_at: Date;
readonly author: number;
readonly author_detail: AuthorMinimal;
/** @nullable */
hub?: number | null;
/** @nullable */
reply_to?: number | null;
readonly tags: readonly Tags[];
readonly contents: readonly PostContent[];
readonly vote_score: string;
readonly user_vote: string;
readonly reply_count: number;
}

View File

@@ -5,16 +5,22 @@
*/
export interface UserRegistration {
/**
* Užívatelské jméno
* @maxLength 150
* @pattern ^[\w.@+-]+$
*/
username?: string;
/**
* Křestní jméno uživatele
* @maxLength 150
*/
first_name: string;
first_name?: string;
/**
* Příjmení uživatele
* @maxLength 150
*/
last_name: string;
last_name?: string;
/**
* Emailová adresa uživatele
* @maxLength 254
@@ -26,7 +32,7 @@ export interface UserRegistration {
* @nullable
* @pattern ^\+?\d{9,15}$
*/
phone_number: string | null;
phone_number?: string | null;
/** Heslo musí mít alespoň 8 znaků, obsahovat velká a malá písmena a číslici. */
password: string;
/**
@@ -34,20 +40,20 @@ export interface UserRegistration {
* @maxLength 100
* @nullable
*/
city: string | null;
city?: string | null;
/**
* Ulice uživatele
* @maxLength 200
* @nullable
*/
street: string | null;
street?: string | null;
/**
* PSČ uživatele
* @maxLength 5
* @nullable
* @pattern ^\d{5}$
*/
postal_code: string | null;
postal_code?: string | null;
/** Souhlas se zpracováním osobních údajů */
gdpr: boolean;
}

View File

@@ -0,0 +1,75 @@
/**
* Hand-written wrappers for endpoints not yet picked up by orval regen.
* Run `npm run api:gen` after running the backend to migrate to the generated client.
*/
import { privateMutator } from "../privateClient";
import type { Post } from "../generated/private/models/post";
import type { Message } from "../generated/private/models/message";
export interface CursorPaginated<T> {
next: string | null;
previous: string | null;
results: T[];
}
export type FeedStrategy = "recent";
export interface FeedParams {
cursor?: string | null;
feed_strategy?: FeedStrategy;
page_size?: number;
}
export const apiSocialPostsFeed = (
params?: FeedParams,
signal?: AbortSignal,
) =>
privateMutator<CursorPaginated<Post>>({
url: `/api/social/posts/feed/`,
method: "GET",
params,
signal,
});
export const feedQueryKey = (params?: Omit<FeedParams, "cursor">) =>
["social", "posts", "feed", params ?? {}] as const;
export interface RepliesParams {
reply_to: number;
cursor?: string | null;
page_size?: number;
}
export const apiSocialPostReplies = (
params: RepliesParams,
signal?: AbortSignal,
) =>
privateMutator<CursorPaginated<Post>>({
url: `/api/social/posts/`,
method: "GET",
params,
signal,
});
export const repliesQueryKey = (postId: number) =>
["social", "posts", "replies", postId] as const;
export interface MessagesParams {
chat: number;
cursor?: string | null;
page_size?: number;
}
export const apiSocialMessagesCursor = (
params: MessagesParams,
signal?: AbortSignal,
) =>
privateMutator<CursorPaginated<Message>>({
url: `/api/social/messages/`,
method: "GET",
params,
signal,
});
export const messagesQueryKey = (chatId: number) =>
["social", "messages", chatId] as const;

View File

@@ -0,0 +1,23 @@
/**
* Derives the WebSocket base URL from env. Mirrors the BE Channels routing,
* which lives behind the same host as the REST API.
*
* Set `VITE_WS_URL` to override (e.g. wss://example.com); otherwise we flip the
* scheme of `VITE_BACKEND_URL`.
*/
export function getChatSocketUrl(chatId: number | string): string {
const explicit = import.meta.env.VITE_WS_URL as string | undefined;
if (explicit) {
return `${stripTrailing(explicit)}/ws/chat/${chatId}/`;
}
const backend = (import.meta.env.VITE_BACKEND_URL as string | undefined)
?? "http://localhost:8000";
const wsBase = backend.replace(/^http/, "ws");
// WS endpoints live at the host root (/ws/chat/<id>/), not under /api/.
const hostOnly = wsBase.replace(/\/api\/?$/, "");
return `${stripTrailing(hostOnly)}/ws/chat/${chatId}/`;
}
function stripTrailing(s: string): string {
return s.endsWith("/") ? s.slice(0, -1) : s;
}