This commit is contained in:
Fredrik Burmester
2026-01-18 19:33:42 +01:00
parent f9a3a1f9f6
commit 83babc2687
22 changed files with 1642 additions and 1449 deletions

View File

@@ -0,0 +1,62 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { Animated, Pressable, View } from "react-native";
import { Text } from "@/components/common/Text";
import { useTVFocusAnimation } from "../hooks/useTVFocusAnimation";
export interface TVLogoutButtonProps {
onPress: () => void;
disabled?: boolean;
}
export const TVLogoutButton: React.FC<TVLogoutButtonProps> = ({
onPress,
disabled,
}) => {
const { t } = useTranslation();
const { focused, handleFocus, handleBlur, animatedStyle } =
useTVFocusAnimation({ scaleAmount: 1.05 });
return (
<Pressable
onPress={onPress}
onFocus={handleFocus}
onBlur={handleBlur}
disabled={disabled}
focusable={!disabled}
>
<Animated.View
style={[
animatedStyle,
{
shadowColor: "#ef4444",
shadowOffset: { width: 0, height: 0 },
shadowOpacity: focused ? 0.6 : 0,
shadowRadius: focused ? 20 : 0,
},
]}
>
<View
style={{
backgroundColor: focused ? "#ef4444" : "rgba(239, 68, 68, 0.8)",
borderRadius: 12,
paddingVertical: 18,
paddingHorizontal: 48,
alignItems: "center",
justifyContent: "center",
}}
>
<Text
style={{
fontSize: 20,
fontWeight: "bold",
color: "#FFFFFF",
}}
>
{t("home.settings.log_out_button")}
</Text>
</View>
</Animated.View>
</Pressable>
);
};

View File

@@ -0,0 +1,23 @@
import React from "react";
import { Text } from "@/components/common/Text";
export interface TVSectionHeaderProps {
title: string;
}
export const TVSectionHeader: React.FC<TVSectionHeaderProps> = ({ title }) => (
<Text
style={{
fontSize: 16,
fontWeight: "600",
color: "#9CA3AF",
textTransform: "uppercase",
letterSpacing: 1,
marginTop: 32,
marginBottom: 16,
marginLeft: 8,
}}
>
{title}
</Text>
);

View File

@@ -0,0 +1,67 @@
import { Ionicons } from "@expo/vector-icons";
import React from "react";
import { Animated, Pressable, View } from "react-native";
import { Text } from "@/components/common/Text";
import { useTVFocusAnimation } from "../hooks/useTVFocusAnimation";
export interface TVSettingsOptionButtonProps {
label: string;
value: string;
onPress: () => void;
isFirst?: boolean;
disabled?: boolean;
}
export const TVSettingsOptionButton: React.FC<TVSettingsOptionButtonProps> = ({
label,
value,
onPress,
isFirst,
disabled,
}) => {
const { focused, handleFocus, handleBlur, animatedStyle } =
useTVFocusAnimation({ scaleAmount: 1.02 });
return (
<Pressable
onPress={onPress}
onFocus={handleFocus}
onBlur={handleBlur}
hasTVPreferredFocus={isFirst && !disabled}
disabled={disabled}
focusable={!disabled}
>
<Animated.View
style={[
animatedStyle,
{
backgroundColor: focused
? "rgba(255, 255, 255, 0.15)"
: "rgba(255, 255, 255, 0.05)",
borderRadius: 12,
paddingVertical: 16,
paddingHorizontal: 24,
marginBottom: 8,
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
},
]}
>
<Text style={{ fontSize: 20, color: "#FFFFFF" }}>{label}</Text>
<View style={{ flexDirection: "row", alignItems: "center" }}>
<Text
style={{
fontSize: 18,
color: "#9CA3AF",
marginRight: 12,
}}
>
{value}
</Text>
<Ionicons name='chevron-forward' size={20} color='#6B7280' />
</View>
</Animated.View>
</Pressable>
);
};

View File

@@ -0,0 +1,71 @@
import { Ionicons } from "@expo/vector-icons";
import React from "react";
import { Animated, Pressable, View } from "react-native";
import { Text } from "@/components/common/Text";
import { useTVFocusAnimation } from "../hooks/useTVFocusAnimation";
export interface TVSettingsRowProps {
label: string;
value: string;
onPress?: () => void;
isFirst?: boolean;
showChevron?: boolean;
disabled?: boolean;
}
export const TVSettingsRow: React.FC<TVSettingsRowProps> = ({
label,
value,
onPress,
isFirst,
showChevron = true,
disabled,
}) => {
const { focused, handleFocus, handleBlur, animatedStyle } =
useTVFocusAnimation({ scaleAmount: 1.02 });
return (
<Pressable
onPress={onPress}
onFocus={handleFocus}
onBlur={handleBlur}
hasTVPreferredFocus={isFirst && !disabled}
disabled={disabled}
focusable={!disabled}
>
<Animated.View
style={[
animatedStyle,
{
backgroundColor: focused
? "rgba(255, 255, 255, 0.15)"
: "rgba(255, 255, 255, 0.05)",
borderRadius: 12,
paddingVertical: 16,
paddingHorizontal: 24,
marginBottom: 8,
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
},
]}
>
<Text style={{ fontSize: 20, color: "#FFFFFF" }}>{label}</Text>
<View style={{ flexDirection: "row", alignItems: "center" }}>
<Text
style={{
fontSize: 18,
color: "#9CA3AF",
marginRight: showChevron ? 12 : 0,
}}
>
{value}
</Text>
{showChevron && (
<Ionicons name='chevron-forward' size={20} color='#6B7280' />
)}
</View>
</Animated.View>
</Pressable>
);
};

View File

@@ -0,0 +1,128 @@
import { Ionicons } from "@expo/vector-icons";
import React from "react";
import { Animated, Pressable, View } from "react-native";
import { Text } from "@/components/common/Text";
import { useTVFocusAnimation } from "../hooks/useTVFocusAnimation";
export interface TVSettingsStepperProps {
label: string;
value: number;
onDecrease: () => void;
onIncrease: () => void;
formatValue?: (value: number) => string;
isFirst?: boolean;
disabled?: boolean;
}
export const TVSettingsStepper: React.FC<TVSettingsStepperProps> = ({
label,
value,
onDecrease,
onIncrease,
formatValue,
isFirst,
disabled,
}) => {
const labelAnim = useTVFocusAnimation({ scaleAmount: 1.02 });
const minusAnim = useTVFocusAnimation({ scaleAmount: 1.1 });
const plusAnim = useTVFocusAnimation({ scaleAmount: 1.1 });
const displayValue = formatValue ? formatValue(value) : String(value);
return (
<View
style={{
backgroundColor:
labelAnim.focused || minusAnim.focused || plusAnim.focused
? "rgba(255, 255, 255, 0.15)"
: "rgba(255, 255, 255, 0.05)",
borderRadius: 12,
paddingVertical: 16,
paddingHorizontal: 24,
marginBottom: 8,
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
}}
>
<Pressable
onFocus={labelAnim.handleFocus}
onBlur={labelAnim.handleBlur}
hasTVPreferredFocus={isFirst && !disabled}
disabled={disabled}
focusable={!disabled}
>
<Animated.View style={labelAnim.animatedStyle}>
<Text style={{ fontSize: 20, color: "#FFFFFF" }}>{label}</Text>
</Animated.View>
</Pressable>
<View style={{ flexDirection: "row", alignItems: "center" }}>
<Pressable
onPress={onDecrease}
onFocus={minusAnim.handleFocus}
onBlur={minusAnim.handleBlur}
disabled={disabled}
focusable={!disabled}
>
<Animated.View
style={[
minusAnim.animatedStyle,
{
width: 40,
height: 40,
borderRadius: 10,
backgroundColor: minusAnim.focused ? "#FFFFFF" : "#4B5563",
justifyContent: "center",
alignItems: "center",
},
]}
>
<Ionicons
name='remove'
size={24}
color={minusAnim.focused ? "#000000" : "#FFFFFF"}
/>
</Animated.View>
</Pressable>
<Text
style={{
fontSize: 18,
color: "#FFFFFF",
minWidth: 60,
textAlign: "center",
marginHorizontal: 16,
}}
>
{displayValue}
</Text>
<Pressable
onPress={onIncrease}
onFocus={plusAnim.handleFocus}
onBlur={plusAnim.handleBlur}
disabled={disabled}
focusable={!disabled}
>
<Animated.View
style={[
plusAnim.animatedStyle,
{
width: 40,
height: 40,
borderRadius: 10,
backgroundColor: plusAnim.focused ? "#FFFFFF" : "#4B5563",
justifyContent: "center",
alignItems: "center",
},
]}
>
<Ionicons
name='add'
size={24}
color={plusAnim.focused ? "#000000" : "#FFFFFF"}
/>
</Animated.View>
</Pressable>
</View>
</View>
);
};

View File

@@ -0,0 +1,83 @@
import React, { useRef } from "react";
import { Animated, Pressable, TextInput } from "react-native";
import { Text } from "@/components/common/Text";
import { useTVFocusAnimation } from "../hooks/useTVFocusAnimation";
export interface TVSettingsTextInputProps {
label: string;
value: string;
placeholder?: string;
onChangeText: (text: string) => void;
onBlur?: () => void;
secureTextEntry?: boolean;
disabled?: boolean;
}
export const TVSettingsTextInput: React.FC<TVSettingsTextInputProps> = ({
label,
value,
placeholder,
onChangeText,
onBlur,
secureTextEntry,
disabled,
}) => {
const inputRef = useRef<TextInput>(null);
const { focused, handleFocus, handleBlur, animatedStyle } =
useTVFocusAnimation({ scaleAmount: 1.02 });
const handleInputBlur = () => {
handleBlur();
onBlur?.();
};
return (
<Pressable
onPress={() => inputRef.current?.focus()}
onFocus={handleFocus}
onBlur={handleInputBlur}
disabled={disabled}
focusable={!disabled}
>
<Animated.View
style={[
animatedStyle,
{
backgroundColor: focused
? "rgba(255, 255, 255, 0.15)"
: "rgba(255, 255, 255, 0.05)",
borderRadius: 12,
paddingVertical: 16,
paddingHorizontal: 24,
marginBottom: 8,
},
]}
>
<Text style={{ fontSize: 16, color: "#9CA3AF", marginBottom: 8 }}>
{label}
</Text>
<TextInput
ref={inputRef}
value={value}
placeholder={placeholder}
placeholderTextColor='#6B7280'
onChangeText={onChangeText}
onBlur={handleInputBlur}
secureTextEntry={secureTextEntry}
autoCapitalize='none'
autoCorrect={false}
style={{
fontSize: 18,
color: "#FFFFFF",
backgroundColor: "rgba(255, 255, 255, 0.05)",
borderRadius: 8,
paddingVertical: 12,
paddingHorizontal: 16,
borderWidth: focused ? 2 : 1,
borderColor: focused ? "#FFFFFF" : "#4B5563",
}}
/>
</Animated.View>
</Pressable>
);
};

View File

@@ -0,0 +1,74 @@
import React from "react";
import { Animated, Pressable, View } from "react-native";
import { Text } from "@/components/common/Text";
import { useTVFocusAnimation } from "../hooks/useTVFocusAnimation";
export interface TVSettingsToggleProps {
label: string;
value: boolean;
onToggle: (value: boolean) => void;
isFirst?: boolean;
disabled?: boolean;
}
export const TVSettingsToggle: React.FC<TVSettingsToggleProps> = ({
label,
value,
onToggle,
isFirst,
disabled,
}) => {
const { focused, handleFocus, handleBlur, animatedStyle } =
useTVFocusAnimation({ scaleAmount: 1.02 });
return (
<Pressable
onPress={() => onToggle(!value)}
onFocus={handleFocus}
onBlur={handleBlur}
hasTVPreferredFocus={isFirst && !disabled}
disabled={disabled}
focusable={!disabled}
>
<Animated.View
style={[
animatedStyle,
{
backgroundColor: focused
? "rgba(255, 255, 255, 0.15)"
: "rgba(255, 255, 255, 0.05)",
borderRadius: 12,
paddingVertical: 16,
paddingHorizontal: 24,
marginBottom: 8,
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
},
]}
>
<Text style={{ fontSize: 20, color: "#FFFFFF" }}>{label}</Text>
<View
style={{
width: 56,
height: 32,
borderRadius: 16,
backgroundColor: value ? "#34C759" : "#4B5563",
justifyContent: "center",
paddingHorizontal: 2,
}}
>
<View
style={{
width: 28,
height: 28,
borderRadius: 14,
backgroundColor: "#FFFFFF",
alignSelf: value ? "flex-end" : "flex-start",
}}
/>
</View>
</Animated.View>
</Pressable>
);
};

View File

@@ -0,0 +1,14 @@
export type { TVLogoutButtonProps } from "./TVLogoutButton";
export { TVLogoutButton } from "./TVLogoutButton";
export type { TVSectionHeaderProps } from "./TVSectionHeader";
export { TVSectionHeader } from "./TVSectionHeader";
export type { TVSettingsOptionButtonProps } from "./TVSettingsOptionButton";
export { TVSettingsOptionButton } from "./TVSettingsOptionButton";
export type { TVSettingsRowProps } from "./TVSettingsRow";
export { TVSettingsRow } from "./TVSettingsRow";
export type { TVSettingsStepperProps } from "./TVSettingsStepper";
export { TVSettingsStepper } from "./TVSettingsStepper";
export type { TVSettingsTextInputProps } from "./TVSettingsTextInput";
export { TVSettingsTextInput } from "./TVSettingsTextInput";
export type { TVSettingsToggleProps } from "./TVSettingsToggle";
export { TVSettingsToggle } from "./TVSettingsToggle";