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

This commit is contained in:
2026-05-14 17:14:27 +02:00
parent 7110f30e16
commit 44385adb6b
17 changed files with 203 additions and 286 deletions

View File

@@ -33,7 +33,7 @@ export default function LoginPage() {
const form = useForm<z.infer<typeof LoginSchema>>({
resolver: zodResolver(LoginSchema),
defaultValues: { username: "", password: "" },
defaultValues: { email: "", password: "" },
});
async function onSubmit(values: z.infer<typeof LoginSchema>) {
@@ -43,7 +43,7 @@ export default function LoginPage() {
setToken(token);
router.push("/"); // Redirect to homepage on successful login
} catch {
setError("Invalid username or password.");
setError("Invalid email or password.");
}
}
@@ -61,12 +61,12 @@ export default function LoginPage() {
{/* ... Form fields for username and password ... */}
<FormField
control={form.control}
name="username"
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="frutiger" {...field} />
<Input type="email" placeholder="you@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>

View File

@@ -3,6 +3,7 @@ import {
getFeed,
getFriends,
getMe,
getTopFriends,
getUserProfile,
Me,
User,
@@ -60,7 +61,7 @@ async function FeedPage({
const { items: allThoughts, totalPages } = feedData!;
const thoughtThreads = buildThoughtThreads(allThoughts);
const authors = [...new Set(allThoughts.map((t) => t.authorUsername))];
const authors = [...new Set(allThoughts.map((t) => t.author.username))];
const userProfiles = await Promise.all(
authors.map((username) => getUserProfile(username, token).catch(() => null))
);
@@ -72,10 +73,10 @@ async function FeedPage({
);
const friends = (await getFriends(token)).users.map((user) => user.username);
const shouldDisplayTopFriends =
token && me?.topFriends && me.topFriends.length > 8;
console.log("Should display top friends:", shouldDisplayTopFriends);
const topFriendsData = me
? await getTopFriends(me.username, token).catch(() => ({ topFriends: [] }))
: { topFriends: [] };
const shouldDisplayTopFriends = topFriendsData.topFriends.length > 0;
return (
<div className="container mx-auto max-w-6xl p-4 sm:p-6">
@@ -96,7 +97,7 @@ async function FeedPage({
<div className="block lg:hidden space-y-6">
<PopularTags />
{shouldDisplayTopFriends && (
<TopFriends mode="top-friends" usernames={me.topFriends} />
<TopFriends mode="top-friends" usernames={topFriendsData.topFriends} />
)}
{!shouldDisplayTopFriends && token && friends.length > 0 && (
<TopFriends mode="friends" usernames={friends || []} />
@@ -141,7 +142,7 @@ async function FeedPage({
<div className="sticky top-20 space-y-6">
<PopularTags />
{shouldDisplayTopFriends && (
<TopFriends mode="top-friends" usernames={me.topFriends} />
<TopFriends mode="top-friends" usernames={topFriendsData.topFriends} />
)}
{!shouldDisplayTopFriends && token && friends.length > 0 && (
<TopFriends mode="friends" usernames={friends || []} />

View File

@@ -30,7 +30,7 @@ export default async function SearchPage({ searchParams }: SearchPageProps) {
const authorDetails = new Map<string, { avatarUrl?: string | null }>();
if (results) {
results.users.users.forEach((user: User) => {
results.users.forEach((user: User) => {
authorDetails.set(user.username, { avatarUrl: user.avatarUrl });
});
}
@@ -48,21 +48,21 @@ export default async function SearchPage({ searchParams }: SearchPageProps) {
<Tabs defaultValue="thoughts" className="w-full">
<TabsList>
<TabsTrigger value="thoughts">
Thoughts ({results.thoughts.thoughts.length})
Thoughts ({results.thoughts.length})
</TabsTrigger>
<TabsTrigger value="users">
Users ({results.users.users.length})
Users ({results.users.length})
</TabsTrigger>
</TabsList>
<TabsContent value="thoughts">
<ThoughtList
thoughts={results.thoughts.thoughts}
thoughts={results.thoughts}
authorDetails={authorDetails}
currentUser={me}
/>
</TabsContent>
<TabsContent value="users">
<UserListCard users={results.users.users} />
<UserListCard users={results.users} />
</TabsContent>
</Tabs>
) : (

View File

@@ -10,7 +10,7 @@ export default async function ApiKeysPage() {
}
const initialApiKeys = await getApiKeys(token).catch(() => ({
apiKeys: [],
keys: [],
}));
return (
@@ -21,7 +21,7 @@ export default async function ApiKeysPage() {
Manage API keys for third-party applications.
</p>
</div>
<ApiKeyList initialApiKeys={initialApiKeys.apiKeys} />
<ApiKeyList initialApiKeys={initialApiKeys.keys} />
</div>
);
}

View File

@@ -23,11 +23,11 @@ export default async function TagPage({ params }: TagPageProps) {
notFound();
}
const allThoughts = thoughtsResult.value.thoughts;
const allThoughts = thoughtsResult.value.items;
const thoughtThreads = buildThoughtThreads(allThoughts);
const me = meResult.status === "fulfilled" ? (meResult.value as Me) : null;
const authors = [...new Set(allThoughts.map((t) => t.authorUsername))];
const authors = [...new Set(allThoughts.map((t) => t.author.username))];
const userProfiles = await Promise.all(
authors.map((username) => getUserProfile(username, token).catch(() => null))
);

View File

@@ -15,7 +15,7 @@ interface ThoughtPageProps {
}
function collectAuthors(thread: ThoughtThreadType): string[] {
const authors = new Set<string>([thread.authorUsername]);
const authors = new Set<string>([thread.author.username]);
for (const reply of thread.replies) {
collectAuthors(reply).forEach((author) => authors.add(author));
}

View File

@@ -26,7 +26,7 @@ export default async function FollowersPage({ params }: FollowersPageProps) {
<p className="text-muted-foreground">Users following @{username}.</p>
</header>
<main>
<UserListCard users={followersData.users} />
<UserListCard users={followersData.items} />
</main>
</div>
);

View File

@@ -26,7 +26,7 @@ export default async function FollowingPage({ params }: FollowingPageProps) {
<p className="text-muted-foreground">Users that @{username} follows.</p>
</header>
<main>
<UserListCard users={followingData.users} />
<UserListCard users={followingData.items} />
</main>
</div>
);

View File

@@ -3,6 +3,7 @@ import {
getFollowingList,
getFriends,
getMe,
getTopFriends,
getUserProfile,
getUserThoughts,
Me,
@@ -55,33 +56,31 @@ export default async function ProfilePage({ params }: ProfilePageProps) {
const me = meResult.status === "fulfilled" ? (meResult.value as Me) : null;
const thoughts =
thoughtsResult.status === "fulfilled" ? thoughtsResult.value.thoughts : [];
thoughtsResult.status === "fulfilled" ? thoughtsResult.value.items : [];
const thoughtThreads = buildThoughtThreads(thoughts);
const followersCount =
followersResult.status === "fulfilled"
? followersResult.value.users.length
? followersResult.value.total
: 0;
const followingCount =
followingResult.status === "fulfilled"
? followingResult.value.users.length
? followingResult.value.total
: 0;
const isOwnProfile = me?.username === user.username;
const isFollowing =
me?.following?.some(
(followedUser) => followedUser.username === user.username
) || false;
const isFollowing = user.isFollowedByViewer;
const authorDetails = new Map<string, { avatarUrl?: string | null }>();
authorDetails.set(user.username, { avatarUrl: user.avatarUrl });
const friends =
typeof token === "string"
? (await getFriends(token)).users.map((user) => user.username)
? (await getFriends(token)).users.map((u) => u.username)
: [];
const shouldDisplayTopFriends = token && friends.length > 8;
const topFriendsData = await getTopFriends(username, token).catch(() => ({ topFriends: [] }));
const shouldDisplayTopFriends = topFriendsData.topFriends.length > 0;
return (
<div id={`profile-page-${user.username}`}>
@@ -195,7 +194,7 @@ export default async function ProfilePage({ params }: ProfilePageProps) {
</Card>
{shouldDisplayTopFriends && (
<TopFriends mode="top-friends" usernames={user.topFriends} />
<TopFriends mode="top-friends" usernames={topFriendsData.topFriends} />
)}
{token && <TopFriends mode="friends" usernames={friends || []} />}
</div>

View File

@@ -64,7 +64,7 @@ export function ApiKeyList({ initialApiKeys }: ApiKeyListProps) {
try {
const newKeyResponse = await createApiKey(values, token);
setKeys((prev) => [...prev, newKeyResponse]);
setNewKey(newKeyResponse.plaintextKey ?? null);
setNewKey(newKeyResponse.key ?? null);
form.reset();
toast.success("API Key created successfully.");
} catch {
@@ -113,7 +113,7 @@ export function ApiKeyList({ initialApiKeys }: ApiKeyListProps) {
{`Created on ${format(key.createdAt, "PPP")}`}
</p>
<p className="text-xs font-mono text-muted-foreground mt-1">
{`${key.keyPrefix}...`}
{key.id}
</p>
</div>
</div>

View File

@@ -16,11 +16,9 @@ import {
FormLabel,
FormControl,
FormMessage,
FormDescription,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { TopFriendsCombobox } from "@/components/top-friends-combobox";
interface EditProfileFormProps {
currentUser: Me;
@@ -38,7 +36,6 @@ export function EditProfileForm({ currentUser }: EditProfileFormProps) {
avatarUrl: currentUser.avatarUrl ?? undefined,
headerUrl: currentUser.headerUrl ?? undefined,
customCss: currentUser.customCss ?? undefined,
topFriends: currentUser.topFriends ?? [],
},
});
@@ -135,25 +132,6 @@ export function EditProfileForm({ currentUser }: EditProfileFormProps) {
</FormItem>
)}
/>
<FormField
name="topFriends"
control={form.control}
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>Top Friends</FormLabel>
<FormControl>
<TopFriendsCombobox
value={field.value || []}
onChange={field.onChange}
/>
</FormControl>
<FormDescription>
Select up to 8 of your friends to display on your profile.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</CardContent>
<CardFooter className="border-t px-6 py-4">
<Button type="submit" disabled={form.formState.isSubmitting}>

View File

@@ -35,7 +35,7 @@ export function PostThoughtForm() {
const form = useForm<z.infer<typeof CreateThoughtSchema>>({
resolver: zodResolver(CreateThoughtSchema),
defaultValues: { content: "", visibility: "Public" },
defaultValues: { content: "", visibility: "public" },
});
async function onSubmit(values: z.infer<typeof CreateThoughtSchema>) {
@@ -93,19 +93,24 @@ export function PostThoughtForm() {
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="Public">
<SelectItem value="public">
<div className="flex items-center gap-2">
<Globe className="h-4 w-4" /> Public
</div>
</SelectItem>
<SelectItem value="FriendsOnly">
<SelectItem value="followers">
<div className="flex items-center gap-2">
<Users className="h-4 w-4" /> Friends Only
<Users className="h-4 w-4" /> Followers
</div>
</SelectItem>
<SelectItem value="Private">
<SelectItem value="unlisted">
<div className="flex items-center gap-2">
<Lock className="h-4 w-4" /> Private
<Lock className="h-4 w-4" /> Unlisted
</div>
</SelectItem>
<SelectItem value="direct">
<div className="flex items-center gap-2">
<Lock className="h-4 w-4" /> Direct
</div>
</SelectItem>
</SelectContent>

View File

@@ -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",
},
});

View File

@@ -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;

View File

@@ -27,9 +27,9 @@ export function ThoughtList({
<div className="space-y-6 p-4">
{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 (
<ThoughtCard

View File

@@ -15,9 +15,9 @@ export function ThoughtThread({
isReply = false,
}: ThoughtThreadProps) {
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 (

View File

@@ -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,
});
@@ -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 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 createThought = (data: z.infer<typeof CreateThoughtSchema>, token: string) =>
apiFetch("/thoughts", { method: "POST", body: JSON.stringify(data) }, ThoughtSchema, token);
export const deleteThought = (thoughtId: string, token: string) =>
apiFetch(
`/thoughts/${thoughtId}`,
{ method: "DELETE" },
z.null(), // Expect a 204 No Content response
token
);
apiFetch(`/thoughts/${thoughtId}`, { method: "DELETE" }, z.null(), 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 getThoughtById = (thoughtId: string, token: string | null) =>
apiFetch(`/thoughts/${thoughtId}`, {}, ThoughtSchema, token);
export const getThoughtThread = (thoughtId: string, token: string | null) =>
apiFetch(`/thoughts/${thoughtId}/thread`, {}, ThoughtThreadSchema, token);
// ── Tags ──────────────────────────────────────────────────────────────────
export const getThoughtsByTag = (tagName: string, token: string | null) =>
apiFetch(
`/tags/${tagName}`,
{},
z.object({ thoughts: z.array(ThoughtSchema) }),
z.object({ tag: z.string(), items: z.array(ThoughtSchema), total: z.number(), page: z.number(), perPage: z.number() }),
token
);
export const getThoughtById = (thoughtId: string, token: string | null) =>
export const getPopularTags = () =>
apiFetch(
`/thoughts/${thoughtId}`,
"/tags/popular",
{},
ThoughtSchema, // Expect a single thought object
token
z.object({ tags: z.array(z.object({ name: z.string(), thoughtCount: z.number() })) })
.transform((d) => d.tags.map((t) => t.name))
);
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
);
// ── Search ────────────────────────────────────────────────────────────────
export const search = (query: string, token: string | null) =>
apiFetch(
`/search?q=${encodeURIComponent(query)}`,
{},
SearchResultsSchema,
token
);
apiFetch(`/search?q=${encodeURIComponent(query)}`, {}, SearchResultsSchema, token);
// ── API Keys ──────────────────────────────────────────────────────────────
export const getApiKeys = (token: string) =>
apiFetch(`/users/me/api-keys`, {}, ApiKeyListSchema, token);
apiFetch("/api-keys", {}, z.object({ keys: z.array(ApiKeySchema) }), 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 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(
`/users/me/api-keys/${keyId}`,
{ method: "DELETE" },
z.null(),
token
);
apiFetch(`/api-keys/${keyId}`, { method: "DELETE" }, z.null(), token);
export const getThoughtThread = (thoughtId: string, token: string | null) =>
apiFetch(`/thoughts/${thoughtId}/thread`, {}, ThoughtThreadSchema, token);
// ── Legacy alias used by top-friends-combobox ─────────────────────────────
export const getAllUsers = (page: number = 1, pageSize: number = 20) =>
apiFetch(
`/users/all?page=${page}&page_size=${pageSize}`,
{},
z.object({
items: z.array(UserSchema),
page: z.number(),
pageSize: z.number(),
totalPages: z.number(),
totalItems: z.number(),
})
);
export const getAllUsersCount = () =>
apiFetch(
`/users/count`,
{},
z.object({
count: z.number(),
})
);
export const getFriends = (token: string) =>
getMeFollowingList(token).then((r) => ({ users: r.items }));