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:
@@ -33,7 +33,7 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
const form = useForm<z.infer<typeof LoginSchema>>({
|
const form = useForm<z.infer<typeof LoginSchema>>({
|
||||||
resolver: zodResolver(LoginSchema),
|
resolver: zodResolver(LoginSchema),
|
||||||
defaultValues: { username: "", password: "" },
|
defaultValues: { email: "", password: "" },
|
||||||
});
|
});
|
||||||
|
|
||||||
async function onSubmit(values: z.infer<typeof LoginSchema>) {
|
async function onSubmit(values: z.infer<typeof LoginSchema>) {
|
||||||
@@ -43,7 +43,7 @@ export default function LoginPage() {
|
|||||||
setToken(token);
|
setToken(token);
|
||||||
router.push("/"); // Redirect to homepage on successful login
|
router.push("/"); // Redirect to homepage on successful login
|
||||||
} catch {
|
} 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 ... */}
|
{/* ... Form fields for username and password ... */}
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="username"
|
name="email"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Username</FormLabel>
|
<FormLabel>Email</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="frutiger" {...field} />
|
<Input type="email" placeholder="you@example.com" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
getFeed,
|
getFeed,
|
||||||
getFriends,
|
getFriends,
|
||||||
getMe,
|
getMe,
|
||||||
|
getTopFriends,
|
||||||
getUserProfile,
|
getUserProfile,
|
||||||
Me,
|
Me,
|
||||||
User,
|
User,
|
||||||
@@ -60,7 +61,7 @@ async function FeedPage({
|
|||||||
const { items: allThoughts, totalPages } = feedData!;
|
const { items: allThoughts, totalPages } = feedData!;
|
||||||
const thoughtThreads = buildThoughtThreads(allThoughts);
|
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(
|
const userProfiles = await Promise.all(
|
||||||
authors.map((username) => getUserProfile(username, token).catch(() => null))
|
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 friends = (await getFriends(token)).users.map((user) => user.username);
|
||||||
const shouldDisplayTopFriends =
|
const topFriendsData = me
|
||||||
token && me?.topFriends && me.topFriends.length > 8;
|
? await getTopFriends(me.username, token).catch(() => ({ topFriends: [] }))
|
||||||
|
: { topFriends: [] };
|
||||||
console.log("Should display top friends:", shouldDisplayTopFriends);
|
const shouldDisplayTopFriends = topFriendsData.topFriends.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto max-w-6xl p-4 sm:p-6">
|
<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">
|
<div className="block lg:hidden space-y-6">
|
||||||
<PopularTags />
|
<PopularTags />
|
||||||
{shouldDisplayTopFriends && (
|
{shouldDisplayTopFriends && (
|
||||||
<TopFriends mode="top-friends" usernames={me.topFriends} />
|
<TopFriends mode="top-friends" usernames={topFriendsData.topFriends} />
|
||||||
)}
|
)}
|
||||||
{!shouldDisplayTopFriends && token && friends.length > 0 && (
|
{!shouldDisplayTopFriends && token && friends.length > 0 && (
|
||||||
<TopFriends mode="friends" usernames={friends || []} />
|
<TopFriends mode="friends" usernames={friends || []} />
|
||||||
@@ -141,7 +142,7 @@ async function FeedPage({
|
|||||||
<div className="sticky top-20 space-y-6">
|
<div className="sticky top-20 space-y-6">
|
||||||
<PopularTags />
|
<PopularTags />
|
||||||
{shouldDisplayTopFriends && (
|
{shouldDisplayTopFriends && (
|
||||||
<TopFriends mode="top-friends" usernames={me.topFriends} />
|
<TopFriends mode="top-friends" usernames={topFriendsData.topFriends} />
|
||||||
)}
|
)}
|
||||||
{!shouldDisplayTopFriends && token && friends.length > 0 && (
|
{!shouldDisplayTopFriends && token && friends.length > 0 && (
|
||||||
<TopFriends mode="friends" usernames={friends || []} />
|
<TopFriends mode="friends" usernames={friends || []} />
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export default async function SearchPage({ searchParams }: SearchPageProps) {
|
|||||||
|
|
||||||
const authorDetails = new Map<string, { avatarUrl?: string | null }>();
|
const authorDetails = new Map<string, { avatarUrl?: string | null }>();
|
||||||
if (results) {
|
if (results) {
|
||||||
results.users.users.forEach((user: User) => {
|
results.users.forEach((user: User) => {
|
||||||
authorDetails.set(user.username, { avatarUrl: user.avatarUrl });
|
authorDetails.set(user.username, { avatarUrl: user.avatarUrl });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -48,21 +48,21 @@ export default async function SearchPage({ searchParams }: SearchPageProps) {
|
|||||||
<Tabs defaultValue="thoughts" className="w-full">
|
<Tabs defaultValue="thoughts" className="w-full">
|
||||||
<TabsList>
|
<TabsList>
|
||||||
<TabsTrigger value="thoughts">
|
<TabsTrigger value="thoughts">
|
||||||
Thoughts ({results.thoughts.thoughts.length})
|
Thoughts ({results.thoughts.length})
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="users">
|
<TabsTrigger value="users">
|
||||||
Users ({results.users.users.length})
|
Users ({results.users.length})
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent value="thoughts">
|
<TabsContent value="thoughts">
|
||||||
<ThoughtList
|
<ThoughtList
|
||||||
thoughts={results.thoughts.thoughts}
|
thoughts={results.thoughts}
|
||||||
authorDetails={authorDetails}
|
authorDetails={authorDetails}
|
||||||
currentUser={me}
|
currentUser={me}
|
||||||
/>
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="users">
|
<TabsContent value="users">
|
||||||
<UserListCard users={results.users.users} />
|
<UserListCard users={results.users} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export default async function ApiKeysPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const initialApiKeys = await getApiKeys(token).catch(() => ({
|
const initialApiKeys = await getApiKeys(token).catch(() => ({
|
||||||
apiKeys: [],
|
keys: [],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -21,7 +21,7 @@ export default async function ApiKeysPage() {
|
|||||||
Manage API keys for third-party applications.
|
Manage API keys for third-party applications.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<ApiKeyList initialApiKeys={initialApiKeys.apiKeys} />
|
<ApiKeyList initialApiKeys={initialApiKeys.keys} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,11 +23,11 @@ export default async function TagPage({ params }: TagPageProps) {
|
|||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
const allThoughts = thoughtsResult.value.thoughts;
|
const allThoughts = thoughtsResult.value.items;
|
||||||
const thoughtThreads = buildThoughtThreads(allThoughts);
|
const thoughtThreads = buildThoughtThreads(allThoughts);
|
||||||
const me = meResult.status === "fulfilled" ? (meResult.value as Me) : null;
|
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(
|
const userProfiles = await Promise.all(
|
||||||
authors.map((username) => getUserProfile(username, token).catch(() => null))
|
authors.map((username) => getUserProfile(username, token).catch(() => null))
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ interface ThoughtPageProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function collectAuthors(thread: ThoughtThreadType): string[] {
|
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) {
|
for (const reply of thread.replies) {
|
||||||
collectAuthors(reply).forEach((author) => authors.add(author));
|
collectAuthors(reply).forEach((author) => authors.add(author));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export default async function FollowersPage({ params }: FollowersPageProps) {
|
|||||||
<p className="text-muted-foreground">Users following @{username}.</p>
|
<p className="text-muted-foreground">Users following @{username}.</p>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<UserListCard users={followersData.users} />
|
<UserListCard users={followersData.items} />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export default async function FollowingPage({ params }: FollowingPageProps) {
|
|||||||
<p className="text-muted-foreground">Users that @{username} follows.</p>
|
<p className="text-muted-foreground">Users that @{username} follows.</p>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<UserListCard users={followingData.users} />
|
<UserListCard users={followingData.items} />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
getFollowingList,
|
getFollowingList,
|
||||||
getFriends,
|
getFriends,
|
||||||
getMe,
|
getMe,
|
||||||
|
getTopFriends,
|
||||||
getUserProfile,
|
getUserProfile,
|
||||||
getUserThoughts,
|
getUserThoughts,
|
||||||
Me,
|
Me,
|
||||||
@@ -55,33 +56,31 @@ export default async function ProfilePage({ params }: ProfilePageProps) {
|
|||||||
const me = meResult.status === "fulfilled" ? (meResult.value as Me) : null;
|
const me = meResult.status === "fulfilled" ? (meResult.value as Me) : null;
|
||||||
|
|
||||||
const thoughts =
|
const thoughts =
|
||||||
thoughtsResult.status === "fulfilled" ? thoughtsResult.value.thoughts : [];
|
thoughtsResult.status === "fulfilled" ? thoughtsResult.value.items : [];
|
||||||
const thoughtThreads = buildThoughtThreads(thoughts);
|
const thoughtThreads = buildThoughtThreads(thoughts);
|
||||||
|
|
||||||
const followersCount =
|
const followersCount =
|
||||||
followersResult.status === "fulfilled"
|
followersResult.status === "fulfilled"
|
||||||
? followersResult.value.users.length
|
? followersResult.value.total
|
||||||
: 0;
|
: 0;
|
||||||
const followingCount =
|
const followingCount =
|
||||||
followingResult.status === "fulfilled"
|
followingResult.status === "fulfilled"
|
||||||
? followingResult.value.users.length
|
? followingResult.value.total
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
const isOwnProfile = me?.username === user.username;
|
const isOwnProfile = me?.username === user.username;
|
||||||
const isFollowing =
|
const isFollowing = user.isFollowedByViewer;
|
||||||
me?.following?.some(
|
|
||||||
(followedUser) => followedUser.username === user.username
|
|
||||||
) || false;
|
|
||||||
|
|
||||||
const authorDetails = new Map<string, { avatarUrl?: string | null }>();
|
const authorDetails = new Map<string, { avatarUrl?: string | null }>();
|
||||||
authorDetails.set(user.username, { avatarUrl: user.avatarUrl });
|
authorDetails.set(user.username, { avatarUrl: user.avatarUrl });
|
||||||
|
|
||||||
const friends =
|
const friends =
|
||||||
typeof token === "string"
|
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 (
|
return (
|
||||||
<div id={`profile-page-${user.username}`}>
|
<div id={`profile-page-${user.username}`}>
|
||||||
@@ -195,7 +194,7 @@ export default async function ProfilePage({ params }: ProfilePageProps) {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{shouldDisplayTopFriends && (
|
{shouldDisplayTopFriends && (
|
||||||
<TopFriends mode="top-friends" usernames={user.topFriends} />
|
<TopFriends mode="top-friends" usernames={topFriendsData.topFriends} />
|
||||||
)}
|
)}
|
||||||
{token && <TopFriends mode="friends" usernames={friends || []} />}
|
{token && <TopFriends mode="friends" usernames={friends || []} />}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export function ApiKeyList({ initialApiKeys }: ApiKeyListProps) {
|
|||||||
try {
|
try {
|
||||||
const newKeyResponse = await createApiKey(values, token);
|
const newKeyResponse = await createApiKey(values, token);
|
||||||
setKeys((prev) => [...prev, newKeyResponse]);
|
setKeys((prev) => [...prev, newKeyResponse]);
|
||||||
setNewKey(newKeyResponse.plaintextKey ?? null);
|
setNewKey(newKeyResponse.key ?? null);
|
||||||
form.reset();
|
form.reset();
|
||||||
toast.success("API Key created successfully.");
|
toast.success("API Key created successfully.");
|
||||||
} catch {
|
} catch {
|
||||||
@@ -113,7 +113,7 @@ export function ApiKeyList({ initialApiKeys }: ApiKeyListProps) {
|
|||||||
{`Created on ${format(key.createdAt, "PPP")}`}
|
{`Created on ${format(key.createdAt, "PPP")}`}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs font-mono text-muted-foreground mt-1">
|
<p className="text-xs font-mono text-muted-foreground mt-1">
|
||||||
{`${key.keyPrefix}...`}
|
{key.id}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,11 +16,9 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
FormDescription,
|
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { TopFriendsCombobox } from "@/components/top-friends-combobox";
|
|
||||||
|
|
||||||
interface EditProfileFormProps {
|
interface EditProfileFormProps {
|
||||||
currentUser: Me;
|
currentUser: Me;
|
||||||
@@ -38,7 +36,6 @@ export function EditProfileForm({ currentUser }: EditProfileFormProps) {
|
|||||||
avatarUrl: currentUser.avatarUrl ?? undefined,
|
avatarUrl: currentUser.avatarUrl ?? undefined,
|
||||||
headerUrl: currentUser.headerUrl ?? undefined,
|
headerUrl: currentUser.headerUrl ?? undefined,
|
||||||
customCss: currentUser.customCss ?? undefined,
|
customCss: currentUser.customCss ?? undefined,
|
||||||
topFriends: currentUser.topFriends ?? [],
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -135,25 +132,6 @@ export function EditProfileForm({ currentUser }: EditProfileFormProps) {
|
|||||||
</FormItem>
|
</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>
|
</CardContent>
|
||||||
<CardFooter className="border-t px-6 py-4">
|
<CardFooter className="border-t px-6 py-4">
|
||||||
<Button type="submit" disabled={form.formState.isSubmitting}>
|
<Button type="submit" disabled={form.formState.isSubmitting}>
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export function PostThoughtForm() {
|
|||||||
|
|
||||||
const form = useForm<z.infer<typeof CreateThoughtSchema>>({
|
const form = useForm<z.infer<typeof CreateThoughtSchema>>({
|
||||||
resolver: zodResolver(CreateThoughtSchema),
|
resolver: zodResolver(CreateThoughtSchema),
|
||||||
defaultValues: { content: "", visibility: "Public" },
|
defaultValues: { content: "", visibility: "public" },
|
||||||
});
|
});
|
||||||
|
|
||||||
async function onSubmit(values: z.infer<typeof CreateThoughtSchema>) {
|
async function onSubmit(values: z.infer<typeof CreateThoughtSchema>) {
|
||||||
@@ -93,19 +93,24 @@ export function PostThoughtForm() {
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="Public">
|
<SelectItem value="public">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Globe className="h-4 w-4" /> Public
|
<Globe className="h-4 w-4" /> Public
|
||||||
</div>
|
</div>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="FriendsOnly">
|
<SelectItem value="followers">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Users className="h-4 w-4" /> Friends Only
|
<Users className="h-4 w-4" /> Followers
|
||||||
</div>
|
</div>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="Private">
|
<SelectItem value="unlisted">
|
||||||
<div className="flex items-center gap-2">
|
<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>
|
</div>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ export function ReplyForm({ parentThoughtId, onReplySuccess }: ReplyFormProps) {
|
|||||||
resolver: zodResolver(CreateThoughtSchema),
|
resolver: zodResolver(CreateThoughtSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
content: "",
|
content: "",
|
||||||
replyToId: parentThoughtId,
|
inReplyToId: parentThoughtId,
|
||||||
visibility: "Public", // Replies default to Public
|
visibility: "public",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export function ThoughtCard({
|
|||||||
addSuffix: true,
|
addSuffix: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const isAuthor = currentUser?.username === thought.authorUsername;
|
const isAuthor = currentUser?.username === thought.author.username;
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
|
|||||||
@@ -27,9 +27,9 @@ export function ThoughtList({
|
|||||||
<div className="space-y-6 p-4">
|
<div className="space-y-6 p-4">
|
||||||
{thoughts.map((thought) => {
|
{thoughts.map((thought) => {
|
||||||
const author = {
|
const author = {
|
||||||
username: thought.authorUsername,
|
username: thought.author.username,
|
||||||
displayName: thought.authorDisplayName,
|
displayName: thought.author.displayName,
|
||||||
...authorDetails.get(thought.authorUsername),
|
...authorDetails.get(thought.author.username),
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<ThoughtCard
|
<ThoughtCard
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ export function ThoughtThread({
|
|||||||
isReply = false,
|
isReply = false,
|
||||||
}: ThoughtThreadProps) {
|
}: ThoughtThreadProps) {
|
||||||
const author = {
|
const author = {
|
||||||
username: thought.authorUsername,
|
username: thought.author.username,
|
||||||
displayName: thought.authorDisplayName,
|
displayName: thought.author.displayName,
|
||||||
...authorDetails.get(thought.authorUsername),
|
...authorDetails.get(thought.author.username),
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,84 +1,80 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export const UserSchema = z.object({
|
export const UserSchema = z.object({
|
||||||
id: z.uuid(),
|
id: z.string().uuid(),
|
||||||
username: z.string(),
|
username: z.string(),
|
||||||
displayName: z.string().nullable(),
|
displayName: z.string().nullable(),
|
||||||
bio: z.string().nullable(),
|
bio: z.string().nullable(),
|
||||||
avatarUrl: z.url().nullable(),
|
avatarUrl: z.string().nullable(),
|
||||||
headerUrl: z.url().nullable(),
|
headerUrl: z.string().nullable(),
|
||||||
customCss: z.string().nullable(),
|
customCss: z.string().nullable(),
|
||||||
topFriends: z.array(z.string()),
|
local: z.boolean(),
|
||||||
|
isFollowedByViewer: z.boolean(),
|
||||||
joinedAt: z.coerce.date(),
|
joinedAt: z.coerce.date(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const MeSchema = z.object({
|
export const MeSchema = UserSchema;
|
||||||
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 ThoughtSchema = z.object({
|
export const ThoughtSchema = z.object({
|
||||||
id: z.uuid(),
|
id: z.string().uuid(),
|
||||||
authorUsername: z.string(),
|
|
||||||
authorDisplayName: z.string().nullable(),
|
|
||||||
content: z.string(),
|
content: z.string(),
|
||||||
visibility: z.enum(["Public", "FriendsOnly", "Private"]),
|
author: UserSchema,
|
||||||
replyToId: z.uuid().nullable(),
|
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(),
|
createdAt: z.coerce.date(),
|
||||||
|
updatedAt: z.coerce.date().nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const RegisterSchema = z.object({
|
export const RegisterSchema = z.object({
|
||||||
username: z.string().min(3),
|
username: z.string().min(3),
|
||||||
email: z.email(),
|
email: z.string().email(),
|
||||||
password: z.string().min(6),
|
password: z.string().min(6),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const LoginSchema = z.object({
|
export const LoginSchema = z.object({
|
||||||
username: z.string().min(3),
|
email: z.string().email(),
|
||||||
password: z.string().min(6),
|
password: z.string().min(6),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const CreateThoughtSchema = z.object({
|
export const CreateThoughtSchema = z.object({
|
||||||
content: z.string().min(1).max(128),
|
content: z.string().min(1).max(128),
|
||||||
visibility: z.enum(["Public", "FriendsOnly", "Private"]).optional(),
|
visibility: z.enum(["public", "followers", "unlisted", "direct"]).optional(),
|
||||||
replyToId: z.string().uuid().optional(),
|
inReplyToId: z.string().uuid().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const UpdateProfileSchema = z.object({
|
export const UpdateProfileSchema = z.object({
|
||||||
displayName: z.string().max(50).optional(),
|
displayName: z.string().max(50).optional(),
|
||||||
bio: z.string().max(4000).optional(),
|
bio: z.string().max(4000).optional(),
|
||||||
avatarUrl: z.url().or(z.literal("")).optional(),
|
avatarUrl: z.string().optional(),
|
||||||
headerUrl: z.url().or(z.literal("")).optional(),
|
headerUrl: z.string().optional(),
|
||||||
customCss: z.string().optional(),
|
customCss: z.string().optional(),
|
||||||
topFriends: z.array(z.string()).max(8).optional(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const SearchResultsSchema = z.object({
|
export const SearchResultsSchema = z.object({
|
||||||
users: z.object({ users: z.array(UserSchema) }),
|
query: z.string(),
|
||||||
thoughts: z.object({ thoughts: z.array(ThoughtSchema) }),
|
thoughts: z.array(ThoughtSchema),
|
||||||
|
users: z.array(UserSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ApiKeySchema = z.object({
|
export const ApiKeySchema = z.object({
|
||||||
id: z.uuid(),
|
id: z.string().uuid(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
keyPrefix: z.string(),
|
|
||||||
createdAt: z.coerce.date(),
|
createdAt: z.coerce.date(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ApiKeyResponseSchema = ApiKeySchema.extend({
|
export const ApiKeyResponseSchema = ApiKeySchema.extend({
|
||||||
plaintextKey: z.string().optional(),
|
key: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ApiKeyListSchema = z.object({
|
export const ApiKeyListSchema = z.object({
|
||||||
apiKeys: z.array(ApiKeySchema),
|
keys: z.array(ApiKeySchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const CreateApiKeySchema = z.object({
|
export const CreateApiKeySchema = z.object({
|
||||||
@@ -87,37 +83,51 @@ export const CreateApiKeySchema = z.object({
|
|||||||
|
|
||||||
export const ThoughtThreadSchema: z.ZodType<{
|
export const ThoughtThreadSchema: z.ZodType<{
|
||||||
id: string;
|
id: string;
|
||||||
authorUsername: string;
|
|
||||||
authorDisplayName: string | null;
|
|
||||||
content: string;
|
content: string;
|
||||||
visibility: "Public" | "FriendsOnly" | "Private";
|
author: z.infer<typeof UserSchema>;
|
||||||
replyToId: string | null;
|
replyToId: string | null;
|
||||||
|
visibility: string;
|
||||||
|
contentWarning: string | null;
|
||||||
|
sensitive: boolean;
|
||||||
|
likeCount: number;
|
||||||
|
boostCount: number;
|
||||||
|
replyCount: number;
|
||||||
|
likedByViewer: boolean;
|
||||||
|
boostedByViewer: boolean;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
|
updatedAt: Date | null;
|
||||||
replies: ThoughtThread[];
|
replies: ThoughtThread[];
|
||||||
}> = z.object({
|
}> = z.object({
|
||||||
id: z.uuid(),
|
id: z.string().uuid(),
|
||||||
authorUsername: z.string(),
|
|
||||||
authorDisplayName: z.string().nullable(),
|
|
||||||
content: z.string(),
|
content: z.string(),
|
||||||
visibility: z.enum(["Public", "FriendsOnly", "Private"]),
|
author: UserSchema,
|
||||||
replyToId: z.uuid().nullable(),
|
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(),
|
createdAt: z.coerce.date(),
|
||||||
|
updatedAt: z.coerce.date().nullable(),
|
||||||
replies: z.lazy(() => z.array(ThoughtThreadSchema)),
|
replies: z.lazy(() => z.array(ThoughtThreadSchema)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type User = z.infer<typeof UserSchema>;
|
export type User = z.infer<typeof UserSchema>;
|
||||||
export type Me = z.infer<typeof MeSchema>;
|
export type Me = z.infer<typeof MeSchema>;
|
||||||
export type Thought = z.infer<typeof ThoughtSchema>;
|
export type Thought = z.infer<typeof ThoughtSchema>;
|
||||||
|
export type ThoughtThread = z.infer<typeof ThoughtThreadSchema>;
|
||||||
export type Register = z.infer<typeof RegisterSchema>;
|
export type Register = z.infer<typeof RegisterSchema>;
|
||||||
export type Login = z.infer<typeof LoginSchema>;
|
export type Login = z.infer<typeof LoginSchema>;
|
||||||
export type ApiKey = z.infer<typeof ApiKeySchema>;
|
export type ApiKey = z.infer<typeof ApiKeySchema>;
|
||||||
export type ApiKeyResponse = z.infer<typeof ApiKeyResponseSchema>;
|
export type ApiKeyResponse = z.infer<typeof ApiKeyResponseSchema>;
|
||||||
export type ThoughtThread = z.infer<typeof ThoughtThreadSchema>;
|
|
||||||
|
|
||||||
const API_BASE_URL =
|
const API_BASE_URL =
|
||||||
typeof window === "undefined"
|
typeof window === "undefined"
|
||||||
? process.env.NEXT_PUBLIC_SERVER_SIDE_API_URL // Server-side
|
? process.env.NEXT_PUBLIC_SERVER_SIDE_API_URL
|
||||||
: process.env.NEXT_PUBLIC_API_URL; // Client-side
|
: process.env.NEXT_PUBLIC_API_URL;
|
||||||
|
|
||||||
async function apiFetch<T>(
|
async function apiFetch<T>(
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
@@ -138,8 +148,7 @@ async function apiFetch<T>(
|
|||||||
headers["Authorization"] = `Bearer ${token}`;
|
headers["Authorization"] = `Bearer ${token}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fullUrl = `${API_BASE_URL}${endpoint}`;
|
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
||||||
const response = await fetch(fullUrl, {
|
|
||||||
...options,
|
...options,
|
||||||
headers,
|
headers,
|
||||||
});
|
});
|
||||||
@@ -156,197 +165,122 @@ async function apiFetch<T>(
|
|||||||
return schema.parse(data);
|
return schema.parse(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Auth ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export const registerUser = (data: z.infer<typeof RegisterSchema>) =>
|
export const registerUser = (data: z.infer<typeof RegisterSchema>) =>
|
||||||
apiFetch("/auth/register", {
|
apiFetch("/auth/register", { method: "POST", body: JSON.stringify(data) }, UserSchema);
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
}, UserSchema);
|
|
||||||
|
|
||||||
export const loginUser = (data: z.infer<typeof LoginSchema>) =>
|
export const loginUser = (data: z.infer<typeof LoginSchema>) =>
|
||||||
apiFetch("/auth/login", {
|
apiFetch("/auth/login", { method: "POST", body: JSON.stringify(data) }, z.object({ token: z.string() }));
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
}, z.object({ token: z.string() }));
|
|
||||||
|
|
||||||
export const getFeed = (token: string, page: number = 1, pageSize: number = 20) =>
|
// ── Current user ──────────────────────────────────────────────────────────
|
||||||
apiFetch(
|
|
||||||
`/feed?page=${page}&page_size=${pageSize}`,
|
export const getMe = (token: string) =>
|
||||||
{},
|
apiFetch("/users/me", {}, MeSchema, token);
|
||||||
z.object({ items: z.array(ThoughtSchema), totalPages: z.number() }),
|
|
||||||
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) =>
|
export const getUserProfile = (username: string, token: string | null) =>
|
||||||
apiFetch(`/users/${username}`, {}, UserSchema, token);
|
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) =>
|
export const getUserThoughts = (username: string, token: string | null) =>
|
||||||
apiFetch(
|
apiFetch(
|
||||||
`/users/${username}/thoughts`,
|
`/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
|
token
|
||||||
);
|
);
|
||||||
|
|
||||||
export const createThought = (
|
export const createThought = (data: z.infer<typeof CreateThoughtSchema>, token: string) =>
|
||||||
data: z.infer<typeof CreateThoughtSchema>,
|
apiFetch("/thoughts", { method: "POST", body: JSON.stringify(data) }, ThoughtSchema, token);
|
||||||
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) =>
|
export const deleteThought = (thoughtId: string, token: string) =>
|
||||||
apiFetch(
|
apiFetch(`/thoughts/${thoughtId}`, { method: "DELETE" }, z.null(), token);
|
||||||
`/thoughts/${thoughtId}`,
|
|
||||||
{ method: "DELETE" },
|
|
||||||
z.null(), // Expect a 204 No Content response
|
|
||||||
token
|
|
||||||
);
|
|
||||||
|
|
||||||
export const updateProfile = (
|
export const getThoughtById = (thoughtId: string, token: string | null) =>
|
||||||
data: z.infer<typeof UpdateProfileSchema>,
|
apiFetch(`/thoughts/${thoughtId}`, {}, ThoughtSchema, token);
|
||||||
token: string
|
|
||||||
) =>
|
export const getThoughtThread = (thoughtId: string, token: string | null) =>
|
||||||
apiFetch(
|
apiFetch(`/thoughts/${thoughtId}/thread`, {}, ThoughtThreadSchema, token);
|
||||||
"/users/me",
|
|
||||||
{
|
// ── Tags ──────────────────────────────────────────────────────────────────
|
||||||
method: "PUT",
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
},
|
|
||||||
UserSchema, // Expect the updated user object back
|
|
||||||
token
|
|
||||||
);
|
|
||||||
|
|
||||||
export const getThoughtsByTag = (tagName: string, token: string | null) =>
|
export const getThoughtsByTag = (tagName: string, token: string | null) =>
|
||||||
apiFetch(
|
apiFetch(
|
||||||
`/tags/${tagName}`,
|
`/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
|
token
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getThoughtById = (thoughtId: string, token: string | null) =>
|
export const getPopularTags = () =>
|
||||||
apiFetch(
|
apiFetch(
|
||||||
`/thoughts/${thoughtId}`,
|
"/tags/popular",
|
||||||
{},
|
{},
|
||||||
ThoughtSchema, // Expect a single thought object
|
z.object({ tags: z.array(z.object({ name: z.string(), thoughtCount: z.number() })) })
|
||||||
token
|
.transform((d) => d.tags.map((t) => t.name))
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getFollowingList = (username: string, token: string | null) =>
|
// ── Search ────────────────────────────────────────────────────────────────
|
||||||
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) =>
|
export const search = (query: string, token: string | null) =>
|
||||||
apiFetch(
|
apiFetch(`/search?q=${encodeURIComponent(query)}`, {}, SearchResultsSchema, token);
|
||||||
`/search?q=${encodeURIComponent(query)}`,
|
|
||||||
{},
|
|
||||||
SearchResultsSchema,
|
|
||||||
token
|
|
||||||
);
|
|
||||||
|
|
||||||
|
// ── API Keys ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export const getApiKeys = (token: string) =>
|
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 = (
|
export const createApiKey = (data: z.infer<typeof CreateApiKeySchema>, token: string) =>
|
||||||
data: z.infer<typeof CreateApiKeySchema>,
|
apiFetch("/api-keys", { method: "POST", body: JSON.stringify(data) }, ApiKeyResponseSchema, token);
|
||||||
token: string
|
|
||||||
) =>
|
|
||||||
apiFetch(
|
|
||||||
`/users/me/api-keys`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
},
|
|
||||||
ApiKeyResponseSchema,
|
|
||||||
token
|
|
||||||
);
|
|
||||||
|
|
||||||
export const deleteApiKey = (keyId: string, token: string) =>
|
export const deleteApiKey = (keyId: string, token: string) =>
|
||||||
apiFetch(
|
apiFetch(`/api-keys/${keyId}`, { method: "DELETE" }, z.null(), token);
|
||||||
`/users/me/api-keys/${keyId}`,
|
|
||||||
{ method: "DELETE" },
|
|
||||||
z.null(),
|
|
||||||
token
|
|
||||||
);
|
|
||||||
|
|
||||||
export const getThoughtThread = (thoughtId: string, token: string | null) =>
|
// ── Legacy alias used by top-friends-combobox ─────────────────────────────
|
||||||
apiFetch(`/thoughts/${thoughtId}/thread`, {}, ThoughtThreadSchema, token);
|
|
||||||
|
|
||||||
|
export const getFriends = (token: string) =>
|
||||||
export const getAllUsers = (page: number = 1, pageSize: number = 20) =>
|
getMeFollowingList(token).then((r) => ({ users: r.items }));
|
||||||
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(),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|||||||
Reference in New Issue
Block a user