FAQ-Bug-Fix + Component-Extraction:
- DoneSlide nutzte qkey.replace('q','a') → 'faq_q1'.replace('q','a')='faa_q1'
weil .replace nur das ERSTE q matched (in "fa**q**"), nicht das in "q1".
→ Antworten resolved gegen non-existent key, raw key gerendert.
- Fix: explizite ID-Array [1,2,4,5,8] mit `help.faq_q\${id}` / `help.faq_a\${id}`.
- Shared FaqAccordion-Component extrahiert (components/FaqAccordion.tsx)
mit 2 Varianten: 'card' (help/faq.tsx) + 'pills' (DoneSlide inline).
- app/help/faq.tsx + DoneSlide nutzen jetzt beide den shared component.
ScreenshotPointer-Alignment für iOS Screen-Time-Permission:
- iOS Family-Controls-Dialog: "Continue/Continuer/Fortfahren" ist LINKS-grau,
"Don't Allow" ist RECHTS-blau (Apple platziert decline prominent, accept
zurückhaltend bei Screen-Time-Permission). Pointer muss daher nach LINKS,
nicht zentriert wie beim NEFilter-Dialog.
- ScreenshotPointer: neuer alignment-Prop ('left'|'center'|'right') →
translateX (-80|0|+80 dp).
- ProtectionSlide iOS Phase B: pointerAlignment="left" durchgereicht.
- Phase A (url_filter) + alle Android-Phasen bleiben center.
Release-Prep (zied):
- CHANGELOG.md v0.3.0-Block erweitert (TTS, Stripe-Pricing, Keyboard-Fix,
Single-Banner, FAQ-Extraktion, i18n-Status, Backend-Pending-Migration).
- version 0.3.0 + buildNumber 10 + versionCode 10 schon vorher gesetzt.
- eas.json production-Profil ready; Android-serviceAccountKeyPath bleibt
TODO (User-Action: Google-Cloud-Service-Account anlegen).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
179 lines
4.2 KiB
TypeScript
179 lines
4.2 KiB
TypeScript
import { useState } from 'react';
|
|
import { Text, TouchableOpacity, View } from 'react-native';
|
|
import { Ionicons } from '@expo/vector-icons';
|
|
import { useColors } from '../lib/theme';
|
|
|
|
/**
|
|
* Shared FAQ Accordion-Komponente.
|
|
*
|
|
* Nutzung:
|
|
* <FaqAccordion items={[{ q: t('help.faq_q1'), a: t('help.faq_a1') }, ...]} />
|
|
*
|
|
* Varianten:
|
|
* - 'card' (default) — gruppiert in einer Card mit Trennlinien zwischen Zeilen,
|
|
* so wie auf der Settings → FAQ-Seite (iOS-Listen-Pattern).
|
|
* - 'pills' — jede Frage als eigene pill-Card, kompakt für inline-Embeds
|
|
* (z.B. End-of-Onboarding Top-5-Block).
|
|
*
|
|
* Eltern hat die Verantwortung für padding/marginHorizontal/ScrollView-Wrap. Die
|
|
* Komponente kümmert sich NUR um Rendering der Zeilen + Expand/Collapse-State.
|
|
*/
|
|
|
|
export type FaqItem = { q: string; a: string };
|
|
|
|
type Variant = 'card' | 'pills';
|
|
|
|
export function FaqAccordion({
|
|
items,
|
|
variant = 'card',
|
|
}: {
|
|
items: FaqItem[];
|
|
variant?: Variant;
|
|
}) {
|
|
const colors = useColors();
|
|
|
|
if (variant === 'pills') {
|
|
return (
|
|
<View style={{ gap: 8 }}>
|
|
{items.map((item, i) => (
|
|
<PillRow key={i} q={item.q} a={item.a} />
|
|
))}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<View
|
|
style={{
|
|
backgroundColor: colors.card,
|
|
borderRadius: 14,
|
|
overflow: 'hidden',
|
|
shadowColor: '#000',
|
|
shadowOffset: { width: 0, height: 1 },
|
|
shadowOpacity: 0.04,
|
|
shadowRadius: 3,
|
|
elevation: 1,
|
|
}}
|
|
>
|
|
{items.map((item, i) => (
|
|
<CardRow
|
|
key={i}
|
|
q={item.q}
|
|
a={item.a}
|
|
isLast={i === items.length - 1}
|
|
/>
|
|
))}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function CardRow({ q, a, isLast }: { q: string; a: string; isLast: boolean }) {
|
|
const colors = useColors();
|
|
const [open, setOpen] = useState(false);
|
|
return (
|
|
<View
|
|
style={{
|
|
borderBottomWidth: isLast ? 0 : 1,
|
|
borderBottomColor: colors.border,
|
|
}}
|
|
>
|
|
<TouchableOpacity
|
|
onPress={() => setOpen((v) => !v)}
|
|
activeOpacity={0.7}
|
|
style={{
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
paddingHorizontal: 16,
|
|
paddingVertical: 14,
|
|
gap: 12,
|
|
}}
|
|
>
|
|
<Text
|
|
style={{
|
|
flex: 1,
|
|
fontSize: 15,
|
|
fontFamily: 'Nunito_600SemiBold',
|
|
color: colors.text,
|
|
lineHeight: 21,
|
|
}}
|
|
>
|
|
{q}
|
|
</Text>
|
|
<Ionicons
|
|
name={open ? 'chevron-up' : 'chevron-down'}
|
|
size={18}
|
|
color={colors.textMuted}
|
|
/>
|
|
</TouchableOpacity>
|
|
{open ? (
|
|
<Text
|
|
style={{
|
|
paddingHorizontal: 16,
|
|
paddingBottom: 16,
|
|
fontSize: 14,
|
|
fontFamily: 'Nunito_400Regular',
|
|
color: colors.textMuted,
|
|
lineHeight: 21,
|
|
}}
|
|
>
|
|
{a}
|
|
</Text>
|
|
) : null}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function PillRow({ q, a }: { q: string; a: string }) {
|
|
const colors = useColors();
|
|
const [open, setOpen] = useState(false);
|
|
return (
|
|
<View
|
|
style={{
|
|
backgroundColor: colors.surfaceElevated,
|
|
borderRadius: 12,
|
|
paddingVertical: 12,
|
|
paddingHorizontal: 14,
|
|
}}
|
|
>
|
|
<TouchableOpacity
|
|
onPress={() => setOpen((v) => !v)}
|
|
activeOpacity={0.7}
|
|
style={{ flexDirection: 'row', alignItems: 'center', gap: 10 }}
|
|
>
|
|
<Text
|
|
style={{
|
|
flex: 1,
|
|
fontFamily: 'Nunito_600SemiBold',
|
|
fontSize: 13,
|
|
lineHeight: 19,
|
|
color: colors.text,
|
|
}}
|
|
>
|
|
{q}
|
|
</Text>
|
|
<Ionicons
|
|
name={open ? 'chevron-up' : 'chevron-down'}
|
|
size={16}
|
|
color={colors.textMuted}
|
|
/>
|
|
</TouchableOpacity>
|
|
{open ? (
|
|
<Text
|
|
style={{
|
|
marginTop: 8,
|
|
paddingTop: 8,
|
|
borderTopWidth: 1,
|
|
borderTopColor: 'rgba(0,0,0,0.06)',
|
|
fontFamily: 'Nunito_400Regular',
|
|
fontSize: 13,
|
|
lineHeight: 19,
|
|
color: colors.textMuted,
|
|
}}
|
|
>
|
|
{a}
|
|
</Text>
|
|
) : null}
|
|
</View>
|
|
);
|
|
}
|