) {
@@ -93,19 +93,24 @@ export function PostThoughtForm() {
-
+
Public
-
+
- Friends Only
+ Followers
-
+
- Private
+ Unlisted
+
+
+
+
+ Direct
diff --git a/thoughts-frontend/components/reply-form.tsx b/thoughts-frontend/components/reply-form.tsx
index 6192b86..12076c7 100644
--- a/thoughts-frontend/components/reply-form.tsx
+++ b/thoughts-frontend/components/reply-form.tsx
@@ -33,8 +33,8 @@ export function ReplyForm({ parentThoughtId, onReplySuccess }: ReplyFormProps) {
resolver: zodResolver(CreateThoughtSchema),
defaultValues: {
content: "",
- replyToId: parentThoughtId,
- visibility: "Public", // Replies default to Public
+ inReplyToId: parentThoughtId,
+ visibility: "public",
},
});
diff --git a/thoughts-frontend/components/thought-card.tsx b/thoughts-frontend/components/thought-card.tsx
index fd2255d..0648af6 100644
--- a/thoughts-frontend/components/thought-card.tsx
+++ b/thoughts-frontend/components/thought-card.tsx
@@ -65,7 +65,7 @@ export function ThoughtCard({
addSuffix: true,
});
- const isAuthor = currentUser?.username === thought.authorUsername;
+ const isAuthor = currentUser?.username === thought.author.username;
const handleDelete = async () => {
if (!token) return;
diff --git a/thoughts-frontend/components/thought-list.tsx b/thoughts-frontend/components/thought-list.tsx
index d7c8aa7..6609f0a 100644
--- a/thoughts-frontend/components/thought-list.tsx
+++ b/thoughts-frontend/components/thought-list.tsx
@@ -27,9 +27,9 @@ export function ThoughtList({
{thoughts.map((thought) => {
const author = {
- username: thought.authorUsername,
- displayName: thought.authorDisplayName,
- ...authorDetails.get(thought.authorUsername),
+ username: thought.author.username,
+ displayName: thought.author.displayName,
+ ...authorDetails.get(thought.author.username),
};
return (
;
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;
export type Me = z.infer;
export type Thought = z.infer;
+export type ThoughtThread = z.infer;
export type Register = z.infer;
export type Login = z.infer;
export type ApiKey = z.infer;
export type ApiKeyResponse = z.infer;
-export type ThoughtThread = z.infer;
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(
endpoint: string,
@@ -138,8 +148,7 @@ async function apiFetch(
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(
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(
return schema.parse(data);
}
+// ── Auth ──────────────────────────────────────────────────────────────────
+
export const registerUser = (data: z.infer) =>
- 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) =>
- 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, 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,
- token: string
-) =>
- apiFetch(
- "/thoughts",
- {
- method: "POST",
- body: JSON.stringify(data),
- },
- ThoughtSchema,
- token
- );
+export const createThought = (data: z.infer, 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,
- 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,
- 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(),
- })
- );
\ No newline at end of file
+ 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, 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 }));