diff --git a/spa/src/components/person-row.tsx b/spa/src/components/person-row.tsx
index ee27a84..afa6c68 100644
--- a/spa/src/components/person-row.tsx
+++ b/spa/src/components/person-row.tsx
@@ -1,7 +1,7 @@
import { Link } from "@tanstack/react-router"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Card, CardContent } from "@/components/ui/card"
-import { posterUrl } from "@/lib/api/client"
+import { tmdbProfileUrl } from "@/lib/api/client"
type PersonRowProps = {
id: string
@@ -16,7 +16,7 @@ export function PersonRow({ id, name, subtitle, imagePath }: PersonRowProps) {
- {imagePath && }
+ {imagePath && }
{name[0]?.toUpperCase()}
diff --git a/spa/src/components/review-card.tsx b/spa/src/components/review-card.tsx
index 6cb016e..8d91a05 100644
--- a/spa/src/components/review-card.tsx
+++ b/spa/src/components/review-card.tsx
@@ -1,4 +1,5 @@
import { Link } from "@tanstack/react-router"
+import { Globe } from "lucide-react"
import { StarDisplay } from "@/components/star-display"
import { Card, CardContent } from "@/components/ui/card"
import { posterUrl } from "@/lib/api/client"
@@ -9,9 +10,10 @@ type ReviewCardProps = {
review: ReviewDto
userName?: string
userId?: string
+ isFederated?: boolean
}
-export function ReviewCard({ movie, review, userName, userId }: ReviewCardProps) {
+export function ReviewCard({ movie, review, userName, userId, isFederated }: ReviewCardProps) {
return (
@@ -28,6 +30,7 @@ export function ReviewCard({ movie, review, userName, userId }: ReviewCardProps)
) : (
{userName}
)}
+ {isFederated && }
·
{review.watched_at.slice(0, 10)}
diff --git a/spa/src/hooks/use-diary.ts b/spa/src/hooks/use-diary.ts
index 261af1a..02c5a31 100644
--- a/spa/src/hooks/use-diary.ts
+++ b/spa/src/hooks/use-diary.ts
@@ -74,6 +74,7 @@ export function useLogReview() {
mutationFn: (data: LogReviewRequest) => logReview(data),
onSuccess: () => {
qc.invalidateQueries({ queryKey: diaryKeys.all })
+ qc.invalidateQueries({ queryKey: ["activity-feed"] })
},
})
}
@@ -84,6 +85,7 @@ export function useDeleteReview() {
mutationFn: (id: string) => deleteReview(id),
onSuccess: () => {
qc.invalidateQueries({ queryKey: diaryKeys.all })
+ qc.invalidateQueries({ queryKey: ["activity-feed"] })
},
})
}
diff --git a/spa/src/hooks/use-social.ts b/spa/src/hooks/use-social.ts
index 53c4aaa..8fcaabf 100644
--- a/spa/src/hooks/use-social.ts
+++ b/spa/src/hooks/use-social.ts
@@ -83,7 +83,7 @@ export function useFollow() {
export function useUnfollow() {
const qc = useQueryClient()
return useMutation({
- mutationFn: (data: FollowRequest) => unfollow(data),
+ mutationFn: (data: ActorUrlRequest) => unfollow(data),
onSuccess: () => {
qc.invalidateQueries({ queryKey: socialKeys.following })
},
diff --git a/spa/src/lib/api/diary.ts b/spa/src/lib/api/diary.ts
index 232d475..63d3d1d 100644
--- a/spa/src/lib/api/diary.ts
+++ b/spa/src/lib/api/diary.ts
@@ -31,6 +31,7 @@ export const feedEntryDtoSchema = z.object({
user_id: z.string().uuid(),
user_email: z.string(),
user_display_name: z.string(),
+ is_federated: z.boolean(),
})
export type FeedEntryDto = z.infer
diff --git a/spa/src/lib/api/movies.ts b/spa/src/lib/api/movies.ts
index d0f66b3..eed73ee 100644
--- a/spa/src/lib/api/movies.ts
+++ b/spa/src/lib/api/movies.ts
@@ -60,6 +60,7 @@ export const keywordDtoSchema = z.object({
})
export const castMemberDtoSchema = z.object({
+ person_id: z.string(),
tmdb_person_id: z.number(),
name: z.string(),
character: z.string(),
@@ -69,6 +70,7 @@ export const castMemberDtoSchema = z.object({
export type CastMemberDto = z.infer
export const crewMemberDtoSchema = z.object({
+ person_id: z.string(),
tmdb_person_id: z.number(),
name: z.string(),
job: z.string(),
diff --git a/spa/src/lib/api/social.ts b/spa/src/lib/api/social.ts
index b030f17..85ad8f8 100644
--- a/spa/src/lib/api/social.ts
+++ b/spa/src/lib/api/social.ts
@@ -68,7 +68,7 @@ export function follow(data: FollowRequest) {
return post("/social/follow", data)
}
-export function unfollow(data: FollowRequest) {
+export function unfollow(data: ActorUrlRequest) {
return post("/social/unfollow", data)
}
diff --git a/spa/src/lib/api/users.ts b/spa/src/lib/api/users.ts
index 433ab42..07db2a2 100644
--- a/spa/src/lib/api/users.ts
+++ b/spa/src/lib/api/users.ts
@@ -1,10 +1,12 @@
import { z } from "zod"
import { diaryEntryDtoSchema, paginatedSchema } from "./common"
-import { get, put, putForm } from "./client"
+import { get, post, put, putForm } from "./client"
export const userSummaryDtoSchema = z.object({
id: z.string().uuid(),
email: z.string(),
+ username: z.string(),
+ display_name: z.string().optional(),
total_movies: z.number(),
avg_rating: z.number().optional(),
})
@@ -130,3 +132,7 @@ export function updateProfile(data: UpdateProfileData) {
export function updateProfileFields(data: UpdateProfileFieldsRequest) {
return put("/profile/fields", data)
}
+
+export function reindexSearch() {
+ return post("/admin/reindex-search")
+}
diff --git a/spa/src/lib/api/wrapup.ts b/spa/src/lib/api/wrapup.ts
index 31a7ec1..c67edf7 100644
--- a/spa/src/lib/api/wrapup.ts
+++ b/spa/src/lib/api/wrapup.ts
@@ -47,6 +47,7 @@ export function deleteWrapUp(id: string) {
}
export type MovieRef = {
+ movie_id?: string
title: string
year: number
runtime_minutes?: number
@@ -54,6 +55,7 @@ export type MovieRef = {
}
export type PersonStat = {
+ person_id?: string
name: string
count: number
avg_rating: number
@@ -91,6 +93,7 @@ export type WrapUpReport = {
first_movie_of_period?: MovieRef
last_movie_of_period?: MovieRef
poster_paths: string[]
+ top_cast_profile_paths: string[]
}
export function getWrapUpReport(id: string) {
diff --git a/spa/src/locales/en.json b/spa/src/locales/en.json
index e59cba4..00fd0b0 100644
--- a/spa/src/locales/en.json
+++ b/spa/src/locales/en.json
@@ -19,6 +19,7 @@
"continue": "Continue",
"generate": "Generate",
"generating": "Generating...",
+ "run": "Run",
"reviews": "{{count}} reviews",
"films": "{{count}} films",
"filmsAvg": "{{count}} films, avg {{avg}}"
@@ -154,7 +155,11 @@
"account": "Account",
"data": "Data",
"integrations": "Integrations",
- "socialGroup": "Social"
+ "socialGroup": "Social",
+ "admin": "Admin",
+ "rebuildSearch": "Rebuild Search Index",
+ "rebuildSearchDesc": "Re-index all movies and people",
+ "rebuildSearchDone": "Reindex queued"
},
"editProfile": {
"title": "Edit Profile",
@@ -201,6 +206,9 @@
"generateWrapUp": "Generate Wrap-Up",
"startDate": "Start Date",
"endDate": "End Date",
+ "generateFor": "Generate for",
+ "forSelf": "Myself",
+ "forGlobal": "Everyone (global)",
"heroSubtitle": "Your Year in Movies",
"moviesWatched": "movies watched",
"watchHours": "{{hours}} hours of watch time",
diff --git a/spa/src/routes/_app/index.tsx b/spa/src/routes/_app/index.tsx
index fcc24b2..fddf3cb 100644
--- a/spa/src/routes/_app/index.tsx
+++ b/spa/src/routes/_app/index.tsx
@@ -100,6 +100,7 @@ function FeedTab() {
review={entry.review}
userName={entry.user_display_name}
userId={entry.user_id}
+ isFederated={entry.is_federated}
/>
)
return entry.user_id === auth?.user_id ? (
diff --git a/spa/src/routes/_app/movies.$id.tsx b/spa/src/routes/_app/movies.$id.tsx
index 985cd0d..4477f6c 100644
--- a/spa/src/routes/_app/movies.$id.tsx
+++ b/spa/src/routes/_app/movies.$id.tsx
@@ -1,6 +1,6 @@
import { createFileRoute, Link } from "@tanstack/react-router"
import { useTranslation } from "react-i18next"
-import { ArrowLeft, Bookmark, BookmarkCheck, Star, TrendingUp, User, Users } from "lucide-react"
+import { ArrowLeft, Bookmark, BookmarkCheck, Globe, Star, TrendingUp, User, Users } from "lucide-react"
import { StarDisplay } from "@/components/star-display"
import { RatingHistogram } from "@/components/rating-histogram"
import { EmptyState } from "@/components/empty-state"
@@ -112,7 +112,10 @@ function MovieDetailPage() {
- {r.user_display}
+
+ {r.user_display}
+ {r.is_federated && }
+
{r.watched_at.slice(0, 10)}
@@ -233,7 +236,7 @@ function PersonStrip({ items, type }: { items: (CastMemberDto | CrewMemberDto)[]
: (person as CrewMemberDto).job
return (
-
+
{person.profile_path ? (
})
@@ -245,7 +248,7 @@ function PersonStrip({ items, type }: { items: (CastMemberDto | CrewMemberDto)[]
{person.name}
{subtitle}
-
+
)
})}
diff --git a/spa/src/routes/_app/profile.tsx b/spa/src/routes/_app/profile.tsx
index dddb284..fd4e532 100644
--- a/spa/src/routes/_app/profile.tsx
+++ b/spa/src/routes/_app/profile.tsx
@@ -52,7 +52,7 @@ function ProfilePage() {
function WrapUpLink() {
const { t } = useTranslation()
const { data } = useWrapUps()
- const latest = data?.items?.find((w) => w.status === "completed")
+ const latest = data?.items?.find((w) => w.status === "Ready")
if (!latest) return null
diff --git a/spa/src/routes/_app/settings/index.tsx b/spa/src/routes/_app/settings/index.tsx
index f320a5b..4f3e3ba 100644
--- a/spa/src/routes/_app/settings/index.tsx
+++ b/spa/src/routes/_app/settings/index.tsx
@@ -1,5 +1,6 @@
import { createFileRoute, Link, useNavigate } from "@tanstack/react-router"
import { useTranslation } from "react-i18next"
+import { useMutation } from "@tanstack/react-query"
import {
ArrowLeft,
ChevronRight,
@@ -7,11 +8,14 @@ import {
Globe,
Key,
LogOut,
+ RefreshCw,
ShieldBan,
Sparkles,
User,
} from "lucide-react"
+import { Button } from "@/components/ui/button"
import { useAuth, useIsAdmin } from "@/components/auth-provider"
+import { reindexSearch } from "@/lib/api/users"
export const Route = createFileRoute("/_app/settings/")({
component: SettingsPage,
@@ -100,6 +104,8 @@ function SettingsPage() {
+ {isAdmin && }
+
- {items.map((item, i) => (
- -
- {i + 1}
-
-
{item.name}
-
{t("common.filmsAvg", { count: item.count, avg: item.avg_rating.toFixed(1) })}★
-
-
- ))}
+ {items.map((item, i) => {
+ const profilePath = profilePaths?.[i]
+ return (
+ -
+ {item.person_id ? (
+
+ {i + 1}
+
+ {profilePath && }
+ {item.name[0]}
+
+
+
{item.name}
+
{t("common.filmsAvg", { count: item.count, avg: item.avg_rating.toFixed(1) })}★
+
+
+ ) : (
+
+
{i + 1}
+
+ {profilePath && }
+ {item.name[0]}
+
+
+
{item.name}
+
{t("common.filmsAvg", { count: item.count, avg: item.avg_rating.toFixed(1) })}★
+
+
+ )}
+
+ )
+ })}
@@ -204,7 +229,7 @@ function RankCard({ title, subtitle, items }: { title: string; subtitle: string;
function MovieHighlight({ label, movie, showRuntime }: { label: string; movie?: MovieRef; showRuntime?: boolean }) {
if (!movie) return null
- return (
+ const content = (
{movie.poster_path && (
@@ -220,6 +245,10 @@ function MovieHighlight({ label, movie, showRuntime }: { label: string; movie?:
)
+ if (movie.movie_id) {
+ return {content}
+ }
+ return content
}
function ReportSkeleton() {