Files
sofa/apps/native/src/components/dashboard/continue-watching-card.tsx
T
jakeandClaude Opus 4.6 2b0c683b7b refactor: replace Biome with oxlint + oxfmt
Migrate the entire monorepo from Biome 2.4.7 to oxlint 1.56.0 (linter)
and oxfmt 0.41.0 (formatter) for faster lint/format and broader rule
coverage.

- Add `.oxlintrc.json` with React, TypeScript, unicorn, import plugins
  and correctness/suspicious categories
- Add `.oxfmtrc.json` with 2-space indent, import sorting, and Tailwind
  class sorting (all 30+ custom className attributes migrated)
- Add `docs/.oxlintrc.json` and `docs/.oxfmtrc.json` with Next.js plugin
- Update all 12 workspace package.json scripts: `oxlint`, `oxfmt`,
  `oxfmt --check`
- Add `format:check` turbo task and CI step
- Update VS Code settings/extensions to use `oxc.oxc-vscode`
- Update CI path triggers from `biome.json` to new config files
- Remove all `biome-ignore` comments and fix shadowed variables
- Delete `biome.json` and `docs/biome.json`
- Reformat entire codebase with oxfmt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 13:26:50 -04:00

123 lines
4.7 KiB
TypeScript

import { useLingui } from "@lingui/react/macro";
import { Link } from "expo-router";
import { Pressable, View } from "react-native";
import { GestureDetector } from "react-native-gesture-handler";
import Animated from "react-native-reanimated";
import { Image } from "@/components/ui/image";
import { Text } from "@/components/ui/text";
import { usePressAnimation } from "@/hooks/use-press-animation";
import { titleActions } from "@/lib/title-actions";
export interface ContinueWatchingItem {
title: {
id: string;
title: string;
backdropPath: string | null;
backdropThumbHash?: string | null;
};
watchedEpisodes: number;
totalEpisodes: number;
nextEpisode?: {
seasonNumber: number;
episodeNumber: number;
name: string | null;
stillPath: string | null;
stillThumbHash?: string | null;
} | null;
}
export function ContinueWatchingCard({ item }: { item: ContinueWatchingItem }) {
const { t } = useLingui();
const { animatedStyle, gesture: tapGesture } = usePressAnimation();
const progressLabel = `${item.watchedEpisodes} of ${item.totalEpisodes} episodes`;
const nextEpLabel = item.nextEpisode
? `Next: Season ${item.nextEpisode.seasonNumber} Episode ${item.nextEpisode.episodeNumber}`
: undefined;
const cardLabel = [item.title.title, progressLabel, nextEpLabel].filter(Boolean).join(", ");
return (
<GestureDetector gesture={tapGesture}>
<Animated.View className="w-[200px]" style={animatedStyle}>
<Link href={`/title/${item.title.id}` as `/title/${string}`} asChild>
<Link.Trigger withAppleZoom>
<Pressable accessibilityRole="button" accessibilityLabel={cardLabel}>
<View
className="bg-card overflow-hidden rounded-[12px] border"
style={{
borderColor: "rgba(255,255,255,0.06)",
borderCurve: "continuous",
}}
>
<View className="h-28 w-[200px]">
{(item.nextEpisode?.stillPath || item.title.backdropPath) && (
<Image
source={{
uri: (item.nextEpisode?.stillPath ?? item.title.backdropPath) as string,
}}
thumbHash={item.nextEpisode?.stillThumbHash ?? item.title.backdropThumbHash}
recyclingKey={item.title.id}
className="h-full w-full"
contentFit="cover"
/>
)}
<View
className="absolute inset-0"
style={{ backgroundColor: "rgba(0,0,0,0.4)" }}
/>
{item.nextEpisode && (
<View className="absolute right-2.5 bottom-3 left-2.5">
<Text
numberOfLines={1}
className="font-sans text-xs font-medium text-white/80"
>
S{item.nextEpisode.seasonNumber} E{item.nextEpisode.episodeNumber}
</Text>
</View>
)}
<View
className="absolute right-0 bottom-0 left-0 h-[3px]"
style={{ backgroundColor: "rgba(255,255,255,0.1)" }}
>
<View
className="bg-status-watching h-full"
style={{
width: `${item.totalEpisodes > 0 ? (item.watchedEpisodes / item.totalEpisodes) * 100 : 0}%`,
}}
/>
</View>
</View>
<View className="p-2.5">
<Text numberOfLines={1} className="text-foreground font-sans text-sm font-medium">
{item.title.title}
</Text>
{item.nextEpisode && (
<Text numberOfLines={1} className="text-muted-foreground mt-0.5 text-xs">
{item.nextEpisode.name}
</Text>
)}
</View>
</View>
</Pressable>
</Link.Trigger>
<Link.Preview />
<Link.Menu>
<Link.MenuAction
title={t`Mark as Completed`}
icon="checkmark.circle"
onPress={() => titleActions.markCompleted(item.title.id, item.title.title)}
/>
<Link.MenuAction
title={t`Remove from Library`}
icon="trash"
destructive
onPress={() => titleActions.removeFromLibrary(item.title.id)}
/>
</Link.Menu>
</Link>
</Animated.View>
</GestureDetector>
);
}