mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-08-29 05:05:38 -04:00
Move star-rating and status-button to title detail co-location, remove unused search components
Co-locate `StarRating` and `StatusButton` under the title detail `_components/` directory since they're only used there. Delete the unused `SearchAutocomplete` and `SearchBar` components from the shared `components/` root.
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
"use client";
|
||||
|
||||
import { IconStar, IconStarFilled } from "@tabler/icons-react";
|
||||
import { motion } from "motion/react";
|
||||
import { useState } from "react";
|
||||
|
||||
interface StarRatingProps {
|
||||
value: number;
|
||||
onChange: (value: number) => void;
|
||||
}
|
||||
|
||||
export function StarRating({ value, onChange }: StarRatingProps) {
|
||||
const [hover, setHover] = useState(0);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex items-center gap-0.5"
|
||||
role="radiogroup"
|
||||
aria-label="Rating"
|
||||
onMouseLeave={() => setHover(0)}
|
||||
>
|
||||
{[1, 2, 3, 4, 5].map((star) => {
|
||||
const filled = star <= (hover || value);
|
||||
return (
|
||||
<motion.button
|
||||
key={star}
|
||||
type="button"
|
||||
onClick={() => onChange(star === value ? 0 : star)}
|
||||
onMouseEnter={() => setHover(star)}
|
||||
className="p-0.5"
|
||||
whileHover={{ scale: 1.15 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
animate={
|
||||
filled && star === value ? { scale: [1, 1.25, 1] } : { scale: 1 }
|
||||
}
|
||||
transition={{
|
||||
type: "spring" as const,
|
||||
stiffness: 400,
|
||||
damping: 15,
|
||||
}}
|
||||
>
|
||||
{filled ? (
|
||||
<IconStarFilled size={18} className="text-primary" />
|
||||
) : (
|
||||
<IconStar size={18} className="text-muted-foreground/30" />
|
||||
)}
|
||||
</motion.button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
IconCheck,
|
||||
IconPlayerPlayFilled,
|
||||
IconPlus,
|
||||
IconX,
|
||||
} from "@tabler/icons-react";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
|
||||
const watchingStyle = {
|
||||
label: "Watching",
|
||||
icon: IconPlayerPlayFilled,
|
||||
class: "text-status-watching",
|
||||
bgClass: "bg-status-watching/10 hover:bg-status-watching/15",
|
||||
borderClass: "ring-status-watching/20",
|
||||
};
|
||||
|
||||
const statusConfig = {
|
||||
watchlist: watchingStyle,
|
||||
in_progress: watchingStyle,
|
||||
completed: {
|
||||
label: "Completed",
|
||||
icon: IconCheck,
|
||||
class: "text-status-completed",
|
||||
bgClass: "bg-status-completed/10 hover:bg-status-completed/15",
|
||||
borderClass: "ring-status-completed/20",
|
||||
},
|
||||
} as const;
|
||||
|
||||
interface StatusButtonProps {
|
||||
currentStatus: string | null;
|
||||
onChange: (status: string | null) => void;
|
||||
}
|
||||
|
||||
export function StatusButton({ currentStatus, onChange }: StatusButtonProps) {
|
||||
const config =
|
||||
statusConfig[currentStatus as keyof typeof statusConfig] ?? null;
|
||||
|
||||
return (
|
||||
<AnimatePresence mode="wait" initial={false}>
|
||||
{!config ? (
|
||||
<motion.button
|
||||
key="add"
|
||||
type="button"
|
||||
onClick={() => onChange("watchlist")}
|
||||
initial={{ opacity: 0, y: 4 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -4 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
className="inline-flex h-9 items-center gap-2 rounded-lg bg-primary/10 px-4 text-sm font-medium text-primary ring-1 ring-primary/20 transition-all hover:bg-primary/15 hover:ring-primary/30 active:scale-[0.97]"
|
||||
>
|
||||
<IconPlus size={14} strokeWidth={2.5} />
|
||||
Watchlist
|
||||
</motion.button>
|
||||
) : (
|
||||
<motion.button
|
||||
key="status"
|
||||
type="button"
|
||||
onClick={() => onChange(null)}
|
||||
initial={{ opacity: 0, y: 4 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -4 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
title="Remove from library"
|
||||
className={`group inline-flex h-9 items-center gap-2 rounded-lg px-4 text-sm font-medium ring-1 transition-all active:scale-[0.97] ${config.class} ${config.bgClass} ${config.borderClass} hover:ring-destructive/30 hover:bg-destructive/10 hover:text-destructive`}
|
||||
>
|
||||
<span className="grid [&>svg]:col-start-1 [&>svg]:row-start-1">
|
||||
<config.icon
|
||||
size={14}
|
||||
className="transition-opacity group-hover:opacity-0"
|
||||
/>
|
||||
<IconX
|
||||
size={14}
|
||||
className="opacity-0 text-destructive transition-opacity group-hover:opacity-100"
|
||||
/>
|
||||
</span>
|
||||
<span className="grid [&>span]:col-start-1 [&>span]:row-start-1">
|
||||
<span className="transition-opacity group-hover:opacity-0">
|
||||
{config.label}
|
||||
</span>
|
||||
<span className="opacity-0 transition-opacity group-hover:opacity-100">
|
||||
Remove
|
||||
</span>
|
||||
</span>
|
||||
</motion.button>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
import { IconPlayerPlay } from "@tabler/icons-react";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { StarRating } from "@/components/star-rating";
|
||||
import { StatusButton } from "@/components/status-button";
|
||||
import {
|
||||
titleTypeAtom,
|
||||
userRatingAtom,
|
||||
userStatusAtom,
|
||||
} from "@/lib/atoms/title";
|
||||
import { StarRating } from "./star-rating";
|
||||
import { StatusButton } from "./status-button";
|
||||
import { useTitleActions } from "./use-title-actions";
|
||||
|
||||
export function TitleActions() {
|
||||
|
||||
Reference in New Issue
Block a user