Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 7 additions & 5 deletions app/(tabs)/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import {
Alert,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
} from "react-native";
import { AppText as Text } from "@/src/components/common/AppText";
import { useDynamicFontSize } from "@/src/hooks/useDynamicFontSize";

import { sampleCourse } from "@/src/data/sampleCourse";
import { useAppStore } from "@/src/store";
Expand All @@ -17,6 +18,7 @@ import { CourseCardSkeleton } from "@/src/components/mobile/CourseCardSkeleton";
export default function HomeScreen() {
const router = useRouter();
const { isLoading, setLoading } = useAppStore();
const { scale } = useDynamicFontSize();

const fetchHomeData = () => {
setLoading(true);
Expand Down Expand Up @@ -74,7 +76,7 @@ export default function HomeScreen() {
>
{/* Header */}
<View style={styles.headerSection}>
<Text style={styles.headerIcon}>? </Text>
<Text style={[styles.headerIcon, { fontSize: scale(60) }]}>? </Text>
<Text style={styles.title}>Welcome to TeachLink</Text>
<Text style={styles.subtitle}>
Share and consume knowledge on the go
Expand All @@ -95,7 +97,7 @@ export default function HomeScreen() {
accessibilityHint="Opens the course viewer with a sample lesson"
>
<View style={styles.buttonContent}>
<Text style={styles.buttonIcon}>? </Text>
<Text style={[styles.buttonIcon, { fontSize: scale(28) }]}>? </Text>
<View style={styles.buttonTextContainer}>
<Text style={styles.buttonTitle}>Start Learning</Text>
<Text style={styles.buttonSubtitle}>Open course viewer</Text>
Expand All @@ -111,14 +113,14 @@ export default function HomeScreen() {
accessibilityHint="Navigates to the search screen"
>
<View style={styles.secondaryButtonContent}>
<Text style={styles.secondaryIcon}>? </Text>
<Text style={[styles.secondaryIcon, { fontSize: scale(32) }]}>? </Text>
<View style={styles.secondaryTextContainer}>
<Text style={styles.secondaryTitle}>Search</Text>
<Text style={styles.secondarySubtitle}>
Find courses and lessons
</Text>
</View>
<Text style={styles.arrow}>{">"}</Text>
<Text style={[styles.arrow, { fontSize: scale(24) }]}>{">"}</Text>
</View>
</TouchableOpacity>

Expand Down
17 changes: 12 additions & 5 deletions app/settings.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
import { useAppStore } from '@/src/store';
import React from 'react';
import { Switch, Text, View } from 'react-native';
import { Switch, View } from 'react-native';
import { AppText } from '@/src/components/common/AppText';

export default function SettingsScreen() {
const { theme, setTheme } = useAppStore();
const isDark = theme === 'dark';

return (
<View className="flex-1 bg-white dark:bg-gray-900 p-4">
<Text className="text-2xl font-bold text-gray-900 dark:text-white mb-6">
<AppText
style={{ fontSize: 24 }}
className="font-bold text-gray-900 dark:text-white mb-6"
>
Settings
</Text>
</AppText>

<View className="flex-row items-center justify-between mb-4">
<Text className="text-gray-900 dark:text-white text-lg">
<AppText
style={{ fontSize: 18 }}
className="text-gray-900 dark:text-white"
>
Dark Mode
</Text>
</AppText>
<Switch
value={isDark}
onValueChange={(value) => setTheme(value ? 'dark' : 'light')}
Expand Down
26 changes: 21 additions & 5 deletions components/themed-text.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { StyleSheet, Text, type TextProps } from 'react-native';

import { useThemeColor } from '@/hooks/use-theme-color';
import { useDynamicFontSize } from '@/src/hooks/useDynamicFontSize';

export type ThemedTextProps = TextProps & {
lightColor?: string;
Expand All @@ -16,18 +17,33 @@ export function ThemedText({
...rest
}: ThemedTextProps) {
const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text');
const { scale } = useDynamicFontSize();

const getVariantStyle = () => {
switch (type) {
case 'title': return styles.title;
case 'subtitle': return styles.subtitle;
case 'defaultSemiBold': return styles.defaultSemiBold;
case 'link': return styles.link;
default: return styles.default;
}
};

const variantStyle = getVariantStyle();
const scaledStyle = {
...variantStyle,
fontSize: scale(variantStyle.fontSize || 16),
lineHeight: variantStyle.lineHeight ? scale(variantStyle.lineHeight) : undefined,
};

return (
<Text
style={[
{ color },
type === 'default' ? styles.default : undefined,
type === 'title' ? styles.title : undefined,
type === 'defaultSemiBold' ? styles.defaultSemiBold : undefined,
type === 'subtitle' ? styles.subtitle : undefined,
type === 'link' ? styles.link : undefined,
scaledStyle,
style,
]}
allowFontScaling={false}
{...rest}
/>
);
Expand Down
43 changes: 43 additions & 0 deletions src/components/common/AppText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React from 'react';
import { Text as RNText, TextProps, StyleSheet } from 'react-native';
import { useDynamicFontSize } from '../../hooks/useDynamicFontSize';

interface AppTextProps extends TextProps {
/**
* If true, the font size will NOT be scaled.
* Useful for elements that should remain at a fixed size regardless of system settings.
*/
fixed?: boolean;
}

/**
* A wrapper around React Native's Text component that uses the useDynamicFontSize hook
* to ensure consistent scaling across the application.
*/
export const AppText: React.FC<AppTextProps> = ({ style, fixed = false, ...props }) => {
const { scale } = useDynamicFontSize();

// We flatten the style to easily extract and modify the fontSize
const flattenedStyle = StyleSheet.flatten(style) || {};

const dynamicStyle = { ...flattenedStyle };

if (!fixed && flattenedStyle.fontSize) {
dynamicStyle.fontSize = scale(flattenedStyle.fontSize);

// Also scale lineHeight if it exists to maintain proportions
if (flattenedStyle.lineHeight) {
dynamicStyle.lineHeight = scale(flattenedStyle.lineHeight);
}
}

return (
<RNText
{...props}
style={dynamicStyle}
// We set allowFontScaling to false because we are manually scaling
// via the dynamicStyle to have explicit control via our hook.
allowFontScaling={false}
/>
);
};
26 changes: 23 additions & 3 deletions src/components/common/PrimaryButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
StyleSheet,
} from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { useDynamicFontSize } from '../../hooks/useDynamicFontSize';

/**
* Props for the PrimaryButton component
Expand Down Expand Up @@ -50,12 +51,28 @@ export default function PrimaryButton({
accessibilityLabel,
}: PrimaryButtonProps) {
const isDisabled = loading || disabled;
const { scale } = useDynamicFontSize();
const buttonLabel = accessibilityLabel ?? title;

const sizeConfig = {
small: { paddingHorizontal: 12, paddingVertical: 8, borderRadius: 8, fontSize: 14 },
medium: { paddingHorizontal: 24, paddingVertical: 12, borderRadius: 12, fontSize: 16 },
large: { paddingHorizontal: 32, paddingVertical: 16, borderRadius: 12, fontSize: 18 },
small: {
paddingHorizontal: scale(12),
paddingVertical: scale(8),
borderRadius: 8,
fontSize: scale(14)
},
medium: {
paddingHorizontal: scale(24),
paddingVertical: scale(12),
borderRadius: 12,
fontSize: scale(16)
},
large: {
paddingHorizontal: scale(32),
paddingVertical: scale(16),
borderRadius: 12,
fontSize: scale(18)
},
};

const config = sizeConfig[size];
Expand Down Expand Up @@ -92,6 +109,7 @@ export default function PrimaryButton({
<>
{icon}
<Text
allowFontScaling={false}
style={[
styles.buttonText,
{ fontSize: config.fontSize, color: '#ffffff' },
Expand Down Expand Up @@ -135,6 +153,7 @@ export default function PrimaryButton({
<>
{icon}
<Text
allowFontScaling={false}
style={[
styles.buttonText,
{ fontSize: config.fontSize, color: '#ffffff' },
Expand Down Expand Up @@ -179,6 +198,7 @@ export default function PrimaryButton({
<>
{icon}
<Text
allowFontScaling={false}
style={[
styles.buttonText,
{ fontSize: config.fontSize, color: '#19c3e6' },
Expand Down
6 changes: 4 additions & 2 deletions src/components/mobile/MobileCourseViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ import {
Modal,
ScrollView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from "react-native";
import { AppText as Text } from "../common/AppText";
import { useDynamicFontSize } from "../../hooks/useDynamicFontSize";
import { SafeAreaView } from "react-native-safe-area-context";
import { useCourseProgress } from "../../hooks/useCourseProgress";
import { RootStackParamList } from "../../navigation/types";
Expand Down Expand Up @@ -47,6 +48,7 @@ export default function MobileCourseViewer({
onBack,
navigation,
}: MobileCourseViewerProps) {
const { scale } = useDynamicFontSize();
const [viewMode, setViewMode] = useState<ViewMode>(
initialViewMode || "lesson",
);
Expand Down Expand Up @@ -393,7 +395,7 @@ export default function MobileCourseViewer({
</View>

{/* Progress Bar */}
<View style={styles.progressBarContainer}>
<View style={[styles.progressBarContainer, { height: scale(8) }]}>
<View
style={[styles.progressBar, { width: `${overallProgress}%` }]}
/>
Expand Down
24 changes: 14 additions & 10 deletions src/components/mobile/MobileFormInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
StyleSheet,
} from 'react-native';
import { Eye, EyeOff, AlertCircle } from 'lucide-react-native';
import { AppText as Text } from '../common/AppText';
import { useDynamicFontSize } from '../../hooks/useDynamicFontSize';

/**
* Props for the MobileFormInput component
Expand Down Expand Up @@ -48,6 +50,7 @@ export const MobileFormInput: React.FC<MobileFormInputProps> = ({
}) => {
const [isFocused, setIsFocused] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const { scale } = useDynamicFontSize();
const isPassword = secureTextEntry === true;

const borderColor = error
Expand All @@ -63,12 +66,12 @@ export const MobileFormInput: React.FC<MobileFormInputProps> = ({
return (
<View style={styles.container}>
<View style={styles.labelRow}>
<Text style={[styles.label, { color: labelColor }]}>
<Text style={[styles.label, { color: labelColor, fontSize: scale(14) }]}>
{label}
{required && <Text style={styles.required}> *</Text>}
{required && <Text style={[styles.required, { fontSize: scale(14) }]}> *</Text>}
</Text>
{hint && !error && (
<Text style={[styles.hint, { color: isDark ? '#475569' : '#94a3b8' }]}>
<Text style={[styles.hint, { color: isDark ? '#475569' : '#94a3b8', fontSize: scale(12) }]}>
{hint}
</Text>
)}
Expand All @@ -80,7 +83,7 @@ export const MobileFormInput: React.FC<MobileFormInputProps> = ({
{
borderColor,
backgroundColor: isDark ? '#1e293b' : '#fff',
minHeight: multiline ? 100 : 52,
minHeight: multiline ? scale(100) : scale(52),
},
]}
>
Expand All @@ -93,9 +96,10 @@ export const MobileFormInput: React.FC<MobileFormInputProps> = ({
styles.input,
{
color: isDark ? '#f1f5f9' : '#1e293b',
paddingLeft: leftIcon ? 0 : 16,
paddingLeft: leftIcon ? 0 : scale(16),
textAlignVertical: multiline ? 'top' : 'center',
paddingTop: multiline ? 14 : 0,
paddingTop: multiline ? scale(14) : 0,
fontSize: scale(15),
},
]}
placeholder={placeholder}
Expand All @@ -116,18 +120,18 @@ export const MobileFormInput: React.FC<MobileFormInputProps> = ({
style={styles.rightIcon}
>
{showPassword ? (
<EyeOff size={20} color={isDark ? '#64748b' : '#94a3b8'} />
<EyeOff size={scale(20)} color={isDark ? '#64748b' : '#94a3b8'} />
) : (
<Eye size={20} color={isDark ? '#64748b' : '#94a3b8'} />
<Eye size={scale(20)} color={isDark ? '#64748b' : '#94a3b8'} />
)}
</TouchableOpacity>
)}
</View>

{error && (
<View style={styles.errorRow}>
<AlertCircle size={14} color="#ef4444" />
<Text style={styles.errorText}>{error}</Text>
<AlertCircle size={scale(14)} color="#ef4444" />
<Text style={[styles.errorText, { fontSize: scale(12) }]}>{error}</Text>
</View>
)}
</View>
Expand Down
Loading
Loading