Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/terms.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ function Terms() {
const handleAcceptTerms = useCallback(() => {
console.log('Accepting terms...');
acceptTerms();
// After accepting terms, show the onboarding walkthrough
// The onboarding will show automatically when navigating to projects view
router.navigate('/');
}, [acceptTerms, router]);

Expand Down
18 changes: 17 additions & 1 deletion components/AppHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
AlertTriangle,
ChevronRight,
CloudOff,
HelpCircle,
Menu,
RefreshCw
} from 'lucide-react-native';
Expand All @@ -29,11 +30,13 @@ import { Button } from './ui/button';
export default function AppHeader({
drawerToggleCallback,
isCloudLoading = false,
isNavigating = false
isNavigating = false,
onOnboardingPress
}: {
drawerToggleCallback: () => void;
isCloudLoading?: boolean;
isNavigating?: boolean;
onOnboardingPress?: () => void;
}) {
const {
breadcrumbs,
Expand Down Expand Up @@ -231,6 +234,19 @@ export default function AppHeader({
: null}
</View>

{/* Help/Onboarding Button */}
{onOnboardingPress && (
<Button
variant="ghost"
size="icon"
onPress={onOnboardingPress}
className="relative mr-2 size-8"
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<Icon as={HelpCircle} size={24} className="text-muted-foreground" />
</Button>
)}

{/* Menu Button with Indicators */}
<View className="relative">
<Button
Expand Down
20 changes: 20 additions & 0 deletions components/LanguageListSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Skeleton } from '@/components/ui/skeleton';
import React from 'react';
import { View } from 'react-native';

// Loading skeleton for language list
export function LanguageListSkeleton() {
return (
<View className="flex-1 gap-3">
{Array.from({ length: 6 }, (_, i) => (
<View
key={i}
className="h-16 flex-row items-center gap-3 rounded-lg border border-border bg-card px-6"
>
<Skeleton className="h-5 w-5 rounded-full" />
<Skeleton className="h-5 flex-1" />
</View>
))}
</View>
);
}
187 changes: 187 additions & 0 deletions components/OnboardingProgressIndicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import { Text } from '@/components/ui/text';
import { cn, getThemeColor } from '@/utils/styleUtils';
import { useEffect } from 'react';
import { View } from 'react-native';
import Animated, {
useAnimatedStyle,
useSharedValue,
withSpring
} from 'react-native-reanimated';

export type OnboardingStep =
| 'vision'
| 'region'
| 'language'
| 'projects'
| 'create-project'
| 'create-project-simple'
| 'bible-select-book'
| 'create-quest'
| 'record-audio'
| 'invite-collaborators';

interface OnboardingProgressIndicatorProps {
currentStep: OnboardingStep;
className?: string;
}

// Simple onboarding steps (for minimal onboarding flow - Other path)
// Language selection happens on terms page, so we start with vision
const SIMPLE_STEP_ORDER: OnboardingStep[] = [
'vision',
'create-project-simple',
'create-quest',
'record-audio',
'invite-collaborators'
];

// Bible onboarding steps (bible-create-chapter is now part of bible-select-book)
// Language selection happens on terms page, so we start with vision
const BIBLE_STEP_ORDER: OnboardingStep[] = [
'vision',
'create-project-simple',
'bible-select-book',
'record-audio',
'invite-collaborators'
];

// Original onboarding steps (for region/language flow)
const ORIGINAL_STEP_ORDER: OnboardingStep[] = [
'region',
'language',
'projects',
'create-project'
];

// Determine which step order to use based on current step
const getStepOrder = (currentStep: OnboardingStep): OnboardingStep[] => {
// If vision step, use simple order (will be updated when project type is selected)
if (currentStep === 'vision') {
return SIMPLE_STEP_ORDER;
}
if (BIBLE_STEP_ORDER.includes(currentStep)) {
return BIBLE_STEP_ORDER;
}
if (SIMPLE_STEP_ORDER.includes(currentStep)) {
return SIMPLE_STEP_ORDER;
}
return ORIGINAL_STEP_ORDER;
};

const STEP_LABELS: Record<OnboardingStep, string> = {
vision: 'Vision',
region: 'Region',
language: 'Language',
projects: 'Projects',
'create-project': 'Create',
'create-project-simple': 'Project',
'bible-select-book': 'Book',
'create-quest': 'Quest',
'record-audio': 'Record',
'invite-collaborators': 'Invite'
};

export function OnboardingProgressIndicator({
currentStep,
className
}: OnboardingProgressIndicatorProps) {
const stepOrder = getStepOrder(currentStep);
const currentStepIndex = stepOrder.indexOf(currentStep);
const progress = useSharedValue(currentStepIndex / (stepOrder.length - 1));

// Animate progress when step changes
useEffect(() => {
const targetProgress = currentStepIndex / (stepOrder.length - 1);
progress.value = withSpring(targetProgress, {
damping: 15,
stiffness: 100
});
}, [currentStepIndex, progress, stepOrder.length]);

// Animated progress bar style
const progressBarStyle = useAnimatedStyle(() => {
return {
width: `${progress.value * 100}%`
};
});

return (
<View className={cn('w-full px-6 py-4', className)}>
{/* Steps container */}
<View className="relative flex-row items-center justify-between">
{/* Progress bar background */}
<View
className="absolute left-0 right-0 top-0 h-0.5 bg-muted"
style={{ top: 12 }}
>
{/* Animated progress fill */}
<Animated.View
style={[
progressBarStyle,
{
height: '100%',
backgroundColor: getThemeColor('primary'),
borderRadius: 9999
}
]}
/>
</View>

{/* Step indicators */}
{stepOrder.map((step, index) => {
const isActive = index === currentStepIndex;
const isCompleted = index < currentStepIndex;
const isPending = index > currentStepIndex;

return (
<View key={step} className="relative z-10 items-center">
{/* Step circle */}
<View
className={cn(
'h-6 w-6 items-center justify-center rounded-full border-2',
isActive
? 'border-primary bg-primary'
: isCompleted
? 'border-primary bg-primary'
: 'border-muted-foreground bg-background'
)}
>
{isCompleted ? (
<Text className="text-xs font-semibold text-primary-foreground">
</Text>
) : (
<Text
className={cn(
'text-xs font-semibold',
isActive
? 'text-primary-foreground'
: 'text-muted-foreground'
)}
>
{index + 1}
</Text>
)}
</View>

{/* Step label */}
<Text
className={cn(
'mt-2 text-xs',
isActive
? 'font-medium text-foreground'
: isCompleted
? 'text-muted-foreground'
: 'text-muted-foreground/60'
)}
numberOfLines={1}
>
{STEP_LABELS[step]}
</Text>
</View>
);
})}
</View>
</View>
);
}
7 changes: 5 additions & 2 deletions components/WaveformVisualization.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,11 @@ export const WaveformVisualization: React.FC<WaveformVisualizationProps> = ({
// But values can be higher, so normalize similar to VADSettingsDrawer
// Using MAX_ENERGY = 20.0 to match VADSettingsDrawer normalization
const MAX_ENERGY = 20.0;
const normalizedRaw = Math.min(1.0, Math.max(0, currentEnergy / MAX_ENERGY));

const normalizedRaw = Math.min(
1.0,
Math.max(0, currentEnergy / MAX_ENERGY)
);

// Scale relative to threshold for visualization
// Threshold is already normalized (0-1), so we compare normalized values
const normalizedEnergy = Math.max(
Expand Down
5 changes: 3 additions & 2 deletions components/language-select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,10 +151,11 @@ export const LanguageSelect: React.FC<LanguageSelectProps> = ({
if (!option) return;
const lang = languages.find((l) => l.id === option.value);
if (lang) {
// If onChange is provided, use it (controlled mode)
// Always save the language and set UI language
setSavedLanguage(lang);
setUILanguage(lang);
// If onChange is provided, call it as well (for controlled mode)
if (onChange) onChange(lang);
else setUILanguage(lang);
}
}}
>
Expand Down
5 changes: 4 additions & 1 deletion eas.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@
"autoIncrement": true,
"channel": "preview",
"environment": "preview",
"distribution": "store",
"distribution": "internal",
"ios": {
"distribution": "store"
},
"env": {
"EXPO_PUBLIC_SITE_URL": "https://langquest.org",
"EXPO_PUBLIC_APP_VARIANT": "preview"
Expand Down
89 changes: 89 additions & 0 deletions hooks/useLanguagesByRegion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { system } from '@/db/powersync/system';
import { useHybridData } from '@/views/new/useHybridData';

export interface Languoid {
id: string;
name: string;
level: 'family' | 'language' | 'dialect';
parent_id: string | null;
active: boolean;
created_at: string;
last_updated: string;
}

export interface LanguageByRegion extends Languoid {
region_id: string;
region_name: string;
}

/**
* Hook to query languoids filtered by region
* Uses the languoid_region join table to connect languoids to regions
*/
export function useLanguagesByRegion(regionId: string | null) {
return useHybridData<LanguageByRegion>({
dataType: 'languages-by-region',
queryKeyParams: [regionId || ''],

// Note: languoid and languoid_region tables don't exist in local SQLite
// Use a dummy offline query that returns empty array
offlineQuery: `SELECT '' as id, '' as name, '' as level, '' as parent_id, '' as region_id, '' as region_name, 0 as active, '' as created_at, '' as last_updated WHERE 1 = 0`,

// Cloud query - fetch languoids filtered by region
cloudQueryFn: async () => {
if (!regionId) return [];

// Query languoids via languoid_region join
const { data, error } = await system.supabaseConnector.client
.from('languoid_region')
.select(
`
languoid_id,
region_id,
languoid:languoid_id (
id,
name,
level,
parent_id,
active,
created_at,
last_updated
),
region:region_id (
name
)
`
)
.eq('region_id', regionId)
.eq('active', true);

if (error) throw error;

// Transform the nested data structure
const languages: LanguageByRegion[] = (data || []).map((item: any) => {
const languoid = item.languoid;
const region = item.region;

return {
id: languoid.id,
name: languoid.name,
level: languoid.level,
parent_id: languoid.parent_id,
active: languoid.active,
created_at: languoid.created_at,
last_updated: languoid.last_updated,
region_id: item.region_id,
region_name: region?.name || ''
};
});

// Sort by name
languages.sort((a, b) => a.name.localeCompare(b.name));

return languages;
},

enableCloudQuery: !!regionId,
enableOfflineQuery: false // Languoid tables don't exist locally
});
}
Loading
Loading