feat: update frontend to work with v2 backend — camelCase, new endpoints, nested author
Some checks failed
lint / lint (push) Has been cancelled
test / unit (push) Has been cancelled
test / integration (push) Has been cancelled
lint / lint (pull_request) Failing after 9m38s
test / unit (pull_request) Successful in 16m2s
test / integration (pull_request) Failing after 17m2s
Some checks failed
lint / lint (push) Has been cancelled
test / unit (push) Has been cancelled
test / integration (push) Has been cancelled
lint / lint (pull_request) Failing after 9m38s
test / unit (pull_request) Successful in 16m2s
test / integration (pull_request) Failing after 17m2s
This commit is contained in:
@@ -1,84 +1,80 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const UserSchema = z.object({
|
||||
id: z.uuid(),
|
||||
id: z.string().uuid(),
|
||||
username: z.string(),
|
||||
displayName: z.string().nullable(),
|
||||
bio: z.string().nullable(),
|
||||
avatarUrl: z.url().nullable(),
|
||||
headerUrl: z.url().nullable(),
|
||||
avatarUrl: z.string().nullable(),
|
||||
headerUrl: z.string().nullable(),
|
||||
customCss: z.string().nullable(),
|
||||
topFriends: z.array(z.string()),
|
||||
local: z.boolean(),
|
||||
isFollowedByViewer: z.boolean(),
|
||||
joinedAt: z.coerce.date(),
|
||||
});
|
||||
|
||||
export const MeSchema = z.object({
|
||||
id: z.uuid(),
|
||||
username: z.string(),
|
||||
displayName: z.string().nullable(),
|
||||
bio: z.string().nullable(),
|
||||
avatarUrl: z.url().nullable(),
|
||||
headerUrl: z.url().nullable(),
|
||||
customCss: z.string().nullable(),
|
||||
topFriends: z.array(z.string()),
|
||||
joinedAt: z.coerce.date(),
|
||||
following: z.array(UserSchema),
|
||||
});
|
||||
export const MeSchema = UserSchema;
|
||||
|
||||
export const ThoughtSchema = z.object({
|
||||
id: z.uuid(),
|
||||
authorUsername: z.string(),
|
||||
authorDisplayName: z.string().nullable(),
|
||||
id: z.string().uuid(),
|
||||
content: z.string(),
|
||||
visibility: z.enum(["Public", "FriendsOnly", "Private"]),
|
||||
replyToId: z.uuid().nullable(),
|
||||
author: UserSchema,
|
||||
replyToId: z.string().uuid().nullable(),
|
||||
visibility: z.string(),
|
||||
contentWarning: z.string().nullable(),
|
||||
sensitive: z.boolean(),
|
||||
likeCount: z.number(),
|
||||
boostCount: z.number(),
|
||||
replyCount: z.number(),
|
||||
likedByViewer: z.boolean(),
|
||||
boostedByViewer: z.boolean(),
|
||||
createdAt: z.coerce.date(),
|
||||
updatedAt: z.coerce.date().nullable(),
|
||||
});
|
||||
|
||||
export const RegisterSchema = z.object({
|
||||
username: z.string().min(3),
|
||||
email: z.email(),
|
||||
email: z.string().email(),
|
||||
password: z.string().min(6),
|
||||
});
|
||||
|
||||
export const LoginSchema = z.object({
|
||||
username: z.string().min(3),
|
||||
email: z.string().email(),
|
||||
password: z.string().min(6),
|
||||
});
|
||||
|
||||
export const CreateThoughtSchema = z.object({
|
||||
content: z.string().min(1).max(128),
|
||||
visibility: z.enum(["Public", "FriendsOnly", "Private"]).optional(),
|
||||
replyToId: z.string().uuid().optional(),
|
||||
visibility: z.enum(["public", "followers", "unlisted", "direct"]).optional(),
|
||||
inReplyToId: z.string().uuid().optional(),
|
||||
});
|
||||
|
||||
export const UpdateProfileSchema = z.object({
|
||||
displayName: z.string().max(50).optional(),
|
||||
bio: z.string().max(4000).optional(),
|
||||
avatarUrl: z.url().or(z.literal("")).optional(),
|
||||
headerUrl: z.url().or(z.literal("")).optional(),
|
||||
avatarUrl: z.string().optional(),
|
||||
headerUrl: z.string().optional(),
|
||||
customCss: z.string().optional(),
|
||||
topFriends: z.array(z.string()).max(8).optional(),
|
||||
});
|
||||
|
||||
export const SearchResultsSchema = z.object({
|
||||
users: z.object({ users: z.array(UserSchema) }),
|
||||
thoughts: z.object({ thoughts: z.array(ThoughtSchema) }),
|
||||
query: z.string(),
|
||||
thoughts: z.array(ThoughtSchema),
|
||||
users: z.array(UserSchema),
|
||||
});
|
||||
|
||||
export const ApiKeySchema = z.object({
|
||||
id: z.uuid(),
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
keyPrefix: z.string(),
|
||||
createdAt: z.coerce.date(),
|
||||
});
|
||||
|
||||
export const ApiKeyResponseSchema = ApiKeySchema.extend({
|
||||
plaintextKey: z.string().optional(),
|
||||
key: z.string().optional(),
|
||||
});
|
||||
|
||||
export const ApiKeyListSchema = z.object({
|
||||
apiKeys: z.array(ApiKeySchema),
|
||||
keys: z.array(ApiKeySchema),
|
||||
});
|
||||
|
||||
export const CreateApiKeySchema = z.object({
|
||||
@@ -87,37 +83,51 @@ export const CreateApiKeySchema = z.object({
|
||||
|
||||
export const ThoughtThreadSchema: z.ZodType<{
|
||||
id: string;
|
||||
authorUsername: string;
|
||||
authorDisplayName: string | null;
|
||||
content: string;
|
||||
visibility: "Public" | "FriendsOnly" | "Private";
|
||||
author: z.infer<typeof UserSchema>;
|
||||
replyToId: string | null;
|
||||
visibility: string;
|
||||
contentWarning: string | null;
|
||||
sensitive: boolean;
|
||||
likeCount: number;
|
||||
boostCount: number;
|
||||
replyCount: number;
|
||||
likedByViewer: boolean;
|
||||
boostedByViewer: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date | null;
|
||||
replies: ThoughtThread[];
|
||||
}> = z.object({
|
||||
id: z.uuid(),
|
||||
authorUsername: z.string(),
|
||||
authorDisplayName: z.string().nullable(),
|
||||
id: z.string().uuid(),
|
||||
content: z.string(),
|
||||
visibility: z.enum(["Public", "FriendsOnly", "Private"]),
|
||||
replyToId: z.uuid().nullable(),
|
||||
author: UserSchema,
|
||||
replyToId: z.string().uuid().nullable(),
|
||||
visibility: z.string(),
|
||||
contentWarning: z.string().nullable(),
|
||||
sensitive: z.boolean(),
|
||||
likeCount: z.number(),
|
||||
boostCount: z.number(),
|
||||
replyCount: z.number(),
|
||||
likedByViewer: z.boolean(),
|
||||
boostedByViewer: z.boolean(),
|
||||
createdAt: z.coerce.date(),
|
||||
updatedAt: z.coerce.date().nullable(),
|
||||
replies: z.lazy(() => z.array(ThoughtThreadSchema)),
|
||||
});
|
||||
|
||||
export type User = z.infer<typeof UserSchema>;
|
||||
export type Me = z.infer<typeof MeSchema>;
|
||||
export type Thought = z.infer<typeof ThoughtSchema>;
|
||||
export type ThoughtThread = z.infer<typeof ThoughtThreadSchema>;
|
||||
export type Register = z.infer<typeof RegisterSchema>;
|
||||
export type Login = z.infer<typeof LoginSchema>;
|
||||
export type ApiKey = z.infer<typeof ApiKeySchema>;
|
||||
export type ApiKeyResponse = z.infer<typeof ApiKeyResponseSchema>;
|
||||
export type ThoughtThread = z.infer<typeof ThoughtThreadSchema>;
|
||||
|
||||
const API_BASE_URL =
|
||||
typeof window === "undefined"
|
||||
? process.env.NEXT_PUBLIC_SERVER_SIDE_API_URL // Server-side
|
||||
: process.env.NEXT_PUBLIC_API_URL; // Client-side
|
||||
? process.env.NEXT_PUBLIC_SERVER_SIDE_API_URL
|
||||
: process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
async function apiFetch<T>(
|
||||
endpoint: string,
|
||||
@@ -138,8 +148,7 @@ async function apiFetch<T>(
|
||||
headers["Authorization"] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const fullUrl = `${API_BASE_URL}${endpoint}`;
|
||||
const response = await fetch(fullUrl, {
|
||||
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
||||
...options,
|
||||
headers,
|
||||
});
|
||||
@@ -147,7 +156,7 @@ async function apiFetch<T>(
|
||||
if (!response.ok) {
|
||||
throw new Error(`API request failed with status ${response.status}`);
|
||||
}
|
||||
|
||||
|
||||
if (response.status === 204) {
|
||||
return null as T;
|
||||
}
|
||||
@@ -156,197 +165,122 @@ async function apiFetch<T>(
|
||||
return schema.parse(data);
|
||||
}
|
||||
|
||||
// ── Auth ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export const registerUser = (data: z.infer<typeof RegisterSchema>) =>
|
||||
apiFetch("/auth/register", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
}, UserSchema);
|
||||
apiFetch("/auth/register", { method: "POST", body: JSON.stringify(data) }, UserSchema);
|
||||
|
||||
export const loginUser = (data: z.infer<typeof LoginSchema>) =>
|
||||
apiFetch("/auth/login", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
}, z.object({ token: z.string() }));
|
||||
apiFetch("/auth/login", { method: "POST", body: JSON.stringify(data) }, z.object({ token: z.string() }));
|
||||
|
||||
export const getFeed = (token: string, page: number = 1, pageSize: number = 20) =>
|
||||
apiFetch(
|
||||
`/feed?page=${page}&page_size=${pageSize}`,
|
||||
{},
|
||||
z.object({ items: z.array(ThoughtSchema), totalPages: z.number() }),
|
||||
token
|
||||
);
|
||||
// ── Current user ──────────────────────────────────────────────────────────
|
||||
|
||||
export const getMe = (token: string) =>
|
||||
apiFetch("/users/me", {}, MeSchema, token);
|
||||
|
||||
export const updateProfile = (data: z.infer<typeof UpdateProfileSchema>, token: string) =>
|
||||
apiFetch("/users/me", { method: "PATCH", body: JSON.stringify(data) }, UserSchema, token);
|
||||
|
||||
export const getMeFollowingList = (token: string) =>
|
||||
apiFetch("/users/me/following-list", {}, z.object({ total: z.number(), items: z.array(UserSchema) }), token);
|
||||
|
||||
// ── Users ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export const getUserProfile = (username: string, token: string | null) =>
|
||||
apiFetch(`/users/${username}`, {}, UserSchema, token);
|
||||
|
||||
export const getFollowersList = (username: string, token: string | null) =>
|
||||
apiFetch(`/users/${username}/follower-list`, {}, z.object({ total: z.number(), items: z.array(UserSchema) }), token);
|
||||
|
||||
export const getFollowingList = (username: string, token: string | null) =>
|
||||
apiFetch(`/users/${username}/following-list`, {}, z.object({ total: z.number(), items: z.array(UserSchema) }), token);
|
||||
|
||||
export const getTopFriends = (username: string, token: string | null) =>
|
||||
apiFetch(`/users/${username}/top-friends`, {}, z.object({ topFriends: z.array(z.string()) }), token);
|
||||
|
||||
export const followUser = (username: string, token: string) =>
|
||||
apiFetch(`/users/${username}/follow`, { method: "POST" }, z.null(), token);
|
||||
|
||||
export const unfollowUser = (username: string, token: string) =>
|
||||
apiFetch(`/users/${username}/follow`, { method: "DELETE" }, z.null(), token);
|
||||
|
||||
export const getAllUsers = (page: number = 1, pageSize: number = 20) =>
|
||||
apiFetch(
|
||||
`/users?page=${page}&per_page=${pageSize}`,
|
||||
{},
|
||||
z.object({ items: z.array(UserSchema), total: z.number(), page: z.number(), perPage: z.number() })
|
||||
.transform((d) => ({ ...d, totalPages: Math.ceil(d.total / d.perPage) }))
|
||||
);
|
||||
|
||||
export const getAllUsersCount = () =>
|
||||
apiFetch("/users/count", {}, z.object({ count: z.number() }));
|
||||
|
||||
// ── Thoughts ──────────────────────────────────────────────────────────────
|
||||
|
||||
export const getFeed = (token: string, page: number = 1, pageSize: number = 20) =>
|
||||
apiFetch(
|
||||
`/feed?page=${page}&per_page=${pageSize}`,
|
||||
{},
|
||||
z.object({ items: z.array(ThoughtSchema), total: z.number(), page: z.number(), perPage: z.number() })
|
||||
.transform((d) => ({ ...d, totalPages: Math.ceil(d.total / d.perPage) })),
|
||||
token
|
||||
);
|
||||
|
||||
export const getUserThoughts = (username: string, token: string | null) =>
|
||||
apiFetch(
|
||||
`/users/${username}/thoughts`,
|
||||
{},
|
||||
z.object({ thoughts: z.array(ThoughtSchema) }),
|
||||
z.object({ items: z.array(ThoughtSchema), total: z.number(), page: z.number(), perPage: z.number() }),
|
||||
token
|
||||
);
|
||||
|
||||
export const createThought = (
|
||||
data: z.infer<typeof CreateThoughtSchema>,
|
||||
token: string
|
||||
) =>
|
||||
apiFetch(
|
||||
"/thoughts",
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
},
|
||||
ThoughtSchema,
|
||||
token
|
||||
);
|
||||
export const createThought = (data: z.infer<typeof CreateThoughtSchema>, token: string) =>
|
||||
apiFetch("/thoughts", { method: "POST", body: JSON.stringify(data) }, ThoughtSchema, token);
|
||||
|
||||
export const followUser = (username: string, token: string) =>
|
||||
apiFetch(
|
||||
`/users/${username}/follow`,
|
||||
{ method: "POST" },
|
||||
z.null(), // Expect a 204 No Content response, which we treat as null
|
||||
token
|
||||
);
|
||||
|
||||
export const unfollowUser = (username: string, token: string) =>
|
||||
apiFetch(
|
||||
`/users/${username}/follow`,
|
||||
{ method: "DELETE" },
|
||||
z.null(), // Expect a 204 No Content response
|
||||
token
|
||||
);
|
||||
|
||||
export const getMe = (token: string) =>
|
||||
apiFetch("/users/me", {}, MeSchema, token);
|
||||
|
||||
export const getPopularTags = () =>
|
||||
apiFetch(
|
||||
"/tags/popular",
|
||||
{},
|
||||
z.array(z.string()) // Expect an array of strings
|
||||
);
|
||||
|
||||
export const deleteThought = (thoughtId: string, token: string) =>
|
||||
apiFetch(
|
||||
`/thoughts/${thoughtId}`,
|
||||
{ method: "DELETE" },
|
||||
z.null(), // Expect a 204 No Content response
|
||||
token
|
||||
);
|
||||
|
||||
export const updateProfile = (
|
||||
data: z.infer<typeof UpdateProfileSchema>,
|
||||
token: string
|
||||
) =>
|
||||
apiFetch(
|
||||
"/users/me",
|
||||
{
|
||||
method: "PUT",
|
||||
body: JSON.stringify(data),
|
||||
},
|
||||
UserSchema, // Expect the updated user object back
|
||||
token
|
||||
);
|
||||
|
||||
export const getThoughtsByTag = (tagName: string, token: string | null) =>
|
||||
apiFetch(
|
||||
`/tags/${tagName}`,
|
||||
{},
|
||||
z.object({ thoughts: z.array(ThoughtSchema) }),
|
||||
token
|
||||
);
|
||||
export const deleteThought = (thoughtId: string, token: string) =>
|
||||
apiFetch(`/thoughts/${thoughtId}`, { method: "DELETE" }, z.null(), token);
|
||||
|
||||
export const getThoughtById = (thoughtId: string, token: string | null) =>
|
||||
apiFetch(
|
||||
`/thoughts/${thoughtId}`,
|
||||
{},
|
||||
ThoughtSchema, // Expect a single thought object
|
||||
token
|
||||
);
|
||||
|
||||
export const getFollowingList = (username: string, token: string | null) =>
|
||||
apiFetch(
|
||||
`/users/${username}/following`,
|
||||
{},
|
||||
z.object({ users: z.array(UserSchema) }),
|
||||
token
|
||||
);
|
||||
|
||||
export const getFollowersList = (username: string, token: string | null) =>
|
||||
apiFetch(
|
||||
`/users/${username}/followers`,
|
||||
{},
|
||||
z.object({ users: z.array(UserSchema) }),
|
||||
token
|
||||
);
|
||||
|
||||
export const getFriends = (token: string) =>
|
||||
apiFetch(
|
||||
"/friends",
|
||||
{},
|
||||
z.object({ users: z.array(UserSchema) }),
|
||||
token
|
||||
);
|
||||
|
||||
|
||||
|
||||
export const search = (query: string, token: string | null) =>
|
||||
apiFetch(
|
||||
`/search?q=${encodeURIComponent(query)}`,
|
||||
{},
|
||||
SearchResultsSchema,
|
||||
token
|
||||
);
|
||||
|
||||
|
||||
export const getApiKeys = (token: string) =>
|
||||
apiFetch(`/users/me/api-keys`, {}, ApiKeyListSchema, token);
|
||||
|
||||
export const createApiKey = (
|
||||
data: z.infer<typeof CreateApiKeySchema>,
|
||||
token: string
|
||||
) =>
|
||||
apiFetch(
|
||||
`/users/me/api-keys`,
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
},
|
||||
ApiKeyResponseSchema,
|
||||
token
|
||||
);
|
||||
|
||||
export const deleteApiKey = (keyId: string, token: string) =>
|
||||
apiFetch(
|
||||
`/users/me/api-keys/${keyId}`,
|
||||
{ method: "DELETE" },
|
||||
z.null(),
|
||||
token
|
||||
);
|
||||
apiFetch(`/thoughts/${thoughtId}`, {}, ThoughtSchema, token);
|
||||
|
||||
export const getThoughtThread = (thoughtId: string, token: string | null) =>
|
||||
apiFetch(`/thoughts/${thoughtId}/thread`, {}, ThoughtThreadSchema, token);
|
||||
|
||||
// ── Tags ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export const getAllUsers = (page: number = 1, pageSize: number = 20) =>
|
||||
export const getThoughtsByTag = (tagName: string, token: string | null) =>
|
||||
apiFetch(
|
||||
`/users/all?page=${page}&page_size=${pageSize}`,
|
||||
`/tags/${tagName}`,
|
||||
{},
|
||||
z.object({
|
||||
items: z.array(UserSchema),
|
||||
page: z.number(),
|
||||
pageSize: z.number(),
|
||||
totalPages: z.number(),
|
||||
totalItems: z.number(),
|
||||
})
|
||||
z.object({ tag: z.string(), items: z.array(ThoughtSchema), total: z.number(), page: z.number(), perPage: z.number() }),
|
||||
token
|
||||
);
|
||||
|
||||
export const getAllUsersCount = () =>
|
||||
export const getPopularTags = () =>
|
||||
apiFetch(
|
||||
`/users/count`,
|
||||
"/tags/popular",
|
||||
{},
|
||||
z.object({
|
||||
count: z.number(),
|
||||
})
|
||||
);
|
||||
z.object({ tags: z.array(z.object({ name: z.string(), thoughtCount: z.number() })) })
|
||||
.transform((d) => d.tags.map((t) => t.name))
|
||||
);
|
||||
|
||||
// ── Search ────────────────────────────────────────────────────────────────
|
||||
|
||||
export const search = (query: string, token: string | null) =>
|
||||
apiFetch(`/search?q=${encodeURIComponent(query)}`, {}, SearchResultsSchema, token);
|
||||
|
||||
// ── API Keys ──────────────────────────────────────────────────────────────
|
||||
|
||||
export const getApiKeys = (token: string) =>
|
||||
apiFetch("/api-keys", {}, z.object({ keys: z.array(ApiKeySchema) }), token);
|
||||
|
||||
export const createApiKey = (data: z.infer<typeof CreateApiKeySchema>, token: string) =>
|
||||
apiFetch("/api-keys", { method: "POST", body: JSON.stringify(data) }, ApiKeyResponseSchema, token);
|
||||
|
||||
export const deleteApiKey = (keyId: string, token: string) =>
|
||||
apiFetch(`/api-keys/${keyId}`, { method: "DELETE" }, z.null(), token);
|
||||
|
||||
// ── Legacy alias used by top-friends-combobox ─────────────────────────────
|
||||
|
||||
export const getFriends = (token: string) =>
|
||||
getMeFollowingList(token).then((r) => ({ users: r.items }));
|
||||
|
||||
Reference in New Issue
Block a user