diff --git a/frontend/src/features/follows/FollowedPage.tsx b/frontend/src/features/follows/FollowedPage.tsx index 85e6c1c..de505a8 100644 --- a/frontend/src/features/follows/FollowedPage.tsx +++ b/frontend/src/features/follows/FollowedPage.tsx @@ -1,10 +1,10 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { Eye, Plus, Star, StarOff } from "lucide-react"; -import { useState, type FormEvent } from "react"; +import { Eye, StarOff } from "lucide-react"; +import { useState } from "react"; import { Link } from "react-router-dom"; -import { Button, EmptyState, Spinner, inputClass } from "@/components/ui"; -import { api, errorMessage } from "@/lib/api"; +import { Button, EmptyState, Spinner } from "@/components/ui"; +import { api } from "@/lib/api"; import { cn } from "@/lib/cn"; import { formatDate } from "@/lib/format"; import type { @@ -32,101 +32,6 @@ function unseenLabel(count: number): string { return count > 99 ? "99+ unseen" : `${count} unseen`; } -function FollowForms() { - const queryClient = useQueryClient(); - const [tag, setTag] = useState(""); - const [pool, setPool] = useState(""); - const [error, setError] = useState(null); - - const tagMutation = useMutation({ - mutationFn: (value: string) => - api("/api/follows/tags/", { - method: "POST", - json: { tag: value }, - }), - onSuccess: () => { - setTag(""); - setError(null); - invalidateFollows(queryClient); - }, - onError: (err) => setError(errorMessage(err)), - }); - - const poolMutation = useMutation({ - mutationFn: (value: string) => - api("/api/follows/pools/", { - method: "POST", - json: { pool: value }, - }), - onSuccess: () => { - setPool(""); - setError(null); - invalidateFollows(queryClient); - }, - onError: (err) => setError(errorMessage(err)), - }); - - function submitTag(event: FormEvent) { - event.preventDefault(); - const value = tag.trim().toLowerCase(); - if (value) tagMutation.mutate(value); - } - - function submitPool(event: FormEvent) { - event.preventDefault(); - const value = pool.trim(); - if (value) poolMutation.mutate(value); - } - - return ( -
-
- - Follow a tag - - setTag(event.target.value)} - /> - -
- -
- - Follow a pool - - setPool(event.target.value)} - /> - -
- - {error ?

{error}

: null} -
- ); -} - function FollowCard({ title, meta, @@ -199,7 +104,10 @@ function FollowCard({
-

+

{title}

{metaLine ? ( @@ -244,25 +152,25 @@ function BlacklistCloudPanel() { return (
-
+

Blacklisted tags

- {cloud?.status === "building" ? ( - - building… - - ) : null} +

+ {cloud?.source === "user" + ? "From your e621 blacklist" + : "From e621's anonymous default blacklist"} + {cloud ? ` · ${cloud.blacklist_count} entries` : ""} + {cloud ? ` · ${cloud.posts} feed post(s) scanned` : ""} + {cloud?.computed_at ? ` · computed ${formatDate(cloud.computed_at)}` : ""} + {cloud?.status === "building" ? ( + + building… + + ) : null} +

-

- {cloud?.source === "user" - ? "From your e621 blacklist" - : "From e621's anonymous default blacklist"} - {cloud ? ` · ${cloud.blacklist_count} entries` : ""} - {cloud ? ` · ${cloud.posts} feed post(s) scanned` : ""} -

- {query.isPending ? (
@@ -283,12 +191,6 @@ function BlacklistCloudPanel() { No blacklisted tags in your feeds.

)} - - {cloud?.computed_at ? ( -

- computed {formatDate(cloud.computed_at)} -

- ) : null}
); } @@ -376,262 +278,250 @@ export default function FollowedPage() {

Followed

- New posts from your followed e621 tags and pools. The periodic - commands refresh these feeds and their covers. + New posts from your followed e621 tags and pools. Follow tags with + the + on any tag chip in the online or library detail views, and + pools from a pool page. The periodic commands refresh these feeds + and their covers.

- +
+

+ Tags{" "} + + {tags.length} + +

+ {tagsQuery.isPending ? ( +
+ +
+ ) : tags.length === 0 ? ( +

+ No followed tags yet — use the + on any tag chip. +

+ ) : ( +
+ {tags.map((follow) => ( + + selectSource({ + kind: "tag", + id: follow.id, + label: `tag:${follow.tag}`, + }) + } + onSeen={() => seenFollow.mutate({ kind: "tag", id: follow.id })} + onUnfollow={() => unfollow.mutate({ kind: "tag", id: follow.id })} + /> + ))} +
+ )} +
-
-
-
-

- Tags{" "} - - {tags.length} - -

- {tagsQuery.isPending ? ( -
- -
- ) : tags.length === 0 ? ( -

- No followed tags yet — follow one above. -

- ) : ( -
- {tags.map((follow) => ( - - selectSource({ - kind: "tag", - id: follow.id, - label: `tag:${follow.tag}`, - }) - } - onSeen={() => - seenFollow.mutate({ kind: "tag", id: follow.id }) - } - onUnfollow={() => - unfollow.mutate({ kind: "tag", id: follow.id }) - } - /> - ))} -
- )} -
- -
-

- Pools{" "} - - {pools.length} - -

- {poolsQuery.isPending ? ( -
- -
- ) : pools.length === 0 ? ( -

- No followed pools yet — paste a pool ID or URL above. -

- ) : ( -
- {pools.map((follow) => ( - - selectSource({ - kind: "pool", - id: follow.id, - label: follow.name || `Pool #${follow.pool_id}`, - }) - } - onSeen={() => - seenFollow.mutate({ kind: "pool", id: follow.id }) - } - onUnfollow={() => - unfollow.mutate({ kind: "pool", id: follow.id }) - } - /> - ))} -
- )} -
- -
-
-

- Feed{" "} - - {feed ? `${feed.count}` : "…"} - {feedSource.kind !== "all" - ? ` · showing ${feedSource.label}` - : ""} - -

-
- - {feedSource.kind !== "all" ? ( - - ) : null} -
-
- - {feedQuery.isPending ? ( -
- -
- ) : !feed || feed.results.length === 0 ? ( - +

+ Pools{" "} + + {pools.length} + +

+ {poolsQuery.isPending ? ( +
+ +
+ ) : pools.length === 0 ? ( +

+ No followed pools yet — open a pool and use "Follow pool". +

+ ) : ( +
+ {pools.map((follow) => ( + + selectSource({ + kind: "pool", + id: follow.id, + label: follow.name || `Pool #${follow.pool_id}`, + }) + } + onSeen={() => + seenFollow.mutate({ kind: "pool", id: follow.id }) + } + onUnfollow={() => + unfollow.mutate({ kind: "pool", id: follow.id }) } /> - ) : ( -
- {feed.results.map((entry) => ( - { - if (!entry.seen) seenPost.mutate(entry.post_id); - }} - className="group overflow-hidden rounded-lg border border-ctp-surface0 bg-ctp-base transition hover:border-ctp-surface2" - > - - {entry.data.preview?.url ? ( - {`Post - ) : ( - - No preview - - )} - {!entry.seen ? ( - - ) : null} - {entry.data.rating ? ( - - {entry.data.rating} - - ) : null} - - - - #{entry.post_id} - - {" "} - ▲ {entry.data.score?.total ?? 0} - {entry.data.fav_count - ? ` · ${entry.data.fav_count} ★` - : ""} - - - - {entry.sources.map((source) => source.label).join(", ")} - - - - ))} -
- )} + ))} +
+ )} +
- {feed && (page > 1 || feed.has_next) ? ( -
- - - Page {page} - - -
+
+
+

+ Feed{" "} + + {feed ? `${feed.count}` : "…"} + {feedSource.kind !== "all" ? ` · showing ${feedSource.label}` : ""} + +

+
+ + {feedSource.kind !== "all" ? ( + ) : null} -
+
- -
+ {feedQuery.isPending ? ( +
+ +
+ ) : !feed || feed.results.length === 0 ? ( + + ) : ( +
+ {feed.results.map((entry) => ( + { + if (!entry.seen) seenPost.mutate(entry.post_id); + }} + className="group block overflow-hidden rounded-lg border border-ctp-surface0 bg-ctp-base transition hover:border-ctp-surface2" + > + + {entry.data.preview?.url ? ( + {`Post + ) : ( + + No preview + + )} + {!entry.seen ? ( + + ) : null} + {entry.data.rating ? ( + + {entry.data.rating} + + ) : null} + + + + #{entry.post_id} + + {" "} + ▲ {entry.data.score?.total ?? 0} + {entry.data.fav_count + ? ` · ${entry.data.fav_count} ★` + : ""} + + + + {entry.sources.map((source) => source.label).join(", ")} + + + + ))} +
+ )} + + {feed && (page > 1 || feed.has_next) ? ( +
+ + + Page {page} + + +
+ ) : null} + + +
); }