fertig
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 366 KiB |
BIN
assets/icon.png
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 366 KiB |
|
After Width: | Height: | Size: 366 KiB |
|
After Width: | Height: | Size: 282 KiB |
|
After Width: | Height: | Size: 737 KiB |
|
After Width: | Height: | Size: 687 KiB |
|
After Width: | Height: | Size: 784 KiB |
|
After Width: | Height: | Size: 601 KiB |
|
After Width: | Height: | Size: 647 KiB |
|
After Width: | Height: | Size: 642 KiB |
|
After Width: | Height: | Size: 860 KiB |
|
|
@ -21,6 +21,7 @@ interface ButtonProps {
|
||||||
textStyle?: TextStyle;
|
textStyle?: TextStyle;
|
||||||
accessibilityLabel?: string;
|
accessibilityLabel?: string;
|
||||||
accessibilityHint?: string;
|
accessibilityHint?: string;
|
||||||
|
icon?: React.ReactNode; // NEW
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Button: React.FC<ButtonProps> = ({
|
export const Button: React.FC<ButtonProps> = ({
|
||||||
|
|
@ -34,6 +35,7 @@ export const Button: React.FC<ButtonProps> = ({
|
||||||
textStyle,
|
textStyle,
|
||||||
accessibilityLabel,
|
accessibilityLabel,
|
||||||
accessibilityHint,
|
accessibilityHint,
|
||||||
|
icon,
|
||||||
}) => {
|
}) => {
|
||||||
const isDisabled = disabled || loading;
|
const isDisabled = disabled || loading;
|
||||||
|
|
||||||
|
|
@ -70,7 +72,10 @@ export const Button: React.FC<ButtonProps> = ({
|
||||||
size="small"
|
size="small"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Text style={textStyles}>{title}</Text>
|
<>
|
||||||
|
{icon && icon}
|
||||||
|
<Text style={[textStyles, icon ? { marginLeft: 8 } : null]}>{title}</Text>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { Button } from './Button';
|
||||||
import { spacing } from '../lib/theme';
|
import { spacing } from '../lib/theme';
|
||||||
|
|
||||||
interface ButtonGridProps {
|
interface ButtonGridProps {
|
||||||
options: { value: string; label: string }[];
|
options: { value: string; label: string; icon?: React.ReactNode }[];
|
||||||
selectedValue: string;
|
selectedValue: string;
|
||||||
onSelect: (value: string) => void;
|
onSelect: (value: string) => void;
|
||||||
columns?: number;
|
columns?: number;
|
||||||
|
|
@ -22,6 +22,7 @@ export const ButtonGrid: React.FC<ButtonGridProps> = ({
|
||||||
<View key={option.value} style={[styles.buttonWrapper, { width: `${100 / columns}%` }]}>
|
<View key={option.value} style={[styles.buttonWrapper, { width: `${100 / columns}%` }]}>
|
||||||
<Button
|
<Button
|
||||||
title={option.label}
|
title={option.label}
|
||||||
|
icon={option.icon}
|
||||||
onPress={() => onSelect(option.value)}
|
onPress={() => onSelect(option.value)}
|
||||||
variant={selectedValue === option.value ? 'primary' : 'outline'}
|
variant={selectedValue === option.value ? 'primary' : 'outline'}
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,8 @@ export interface EmptyStateProps {
|
||||||
title: string;
|
title: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
actionLabel?: string;
|
actionLabel?: string;
|
||||||
onAction?: () => void;
|
onAction?: () => void; // Restored
|
||||||
|
buttonStyle?: import('react-native').ViewStyle;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EmptyState: React.FC<EmptyStateProps> = ({
|
export const EmptyState: React.FC<EmptyStateProps> = ({
|
||||||
|
|
@ -17,6 +18,7 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
|
||||||
description,
|
description,
|
||||||
actionLabel,
|
actionLabel,
|
||||||
onAction,
|
onAction,
|
||||||
|
buttonStyle,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
|
|
@ -28,7 +30,8 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
|
||||||
title={actionLabel}
|
title={actionLabel}
|
||||||
onPress={onAction}
|
onPress={onAction}
|
||||||
size="md"
|
size="md"
|
||||||
style={styles.button}
|
// Fix: Flatten the array to satisfy the strict ViewStyle type expected by Button
|
||||||
|
style={StyleSheet.flatten([styles.button, buttonStyle])}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { View, Text, StyleSheet, TextInput } from 'react-native';
|
||||||
import { Card } from './Card';
|
import { Card } from './Card';
|
||||||
import { Button } from './Button';
|
import { Button } from './Button';
|
||||||
import { Picker } from './Picker';
|
import { Picker } from './Picker';
|
||||||
|
import { ButtonGrid } from './ButtonGrid'; // NEW
|
||||||
import { colors, spacing, typography, borderRadius } from '../lib/theme';
|
import { colors, spacing, typography, borderRadius } from '../lib/theme';
|
||||||
import { GlazeLayer, ApplicationMethod, GlazePosition } from '../types';
|
import { GlazeLayer, ApplicationMethod, GlazePosition } from '../types';
|
||||||
import { getGlaze } from '../lib/db/repositories';
|
import { getGlaze } from '../lib/db/repositories';
|
||||||
|
|
@ -76,9 +77,8 @@ export const GlazeLayerCard: React.FC<GlazeLayerCardProps> = ({
|
||||||
|
|
||||||
<Text style={styles.glazeName}>{glazeName}</Text>
|
<Text style={styles.glazeName}>{glazeName}</Text>
|
||||||
|
|
||||||
<Picker
|
<Text style={styles.label}>Application Method</Text>
|
||||||
label="Application Method"
|
<ButtonGrid
|
||||||
value={layer.application}
|
|
||||||
options={[
|
options={[
|
||||||
{ value: 'brush', label: 'Brushing' },
|
{ value: 'brush', label: 'Brushing' },
|
||||||
{ value: 'dip', label: 'Dipping' },
|
{ value: 'dip', label: 'Dipping' },
|
||||||
|
|
@ -86,12 +86,13 @@ export const GlazeLayerCard: React.FC<GlazeLayerCardProps> = ({
|
||||||
{ value: 'spray', label: 'Spraying' },
|
{ value: 'spray', label: 'Spraying' },
|
||||||
{ value: 'other', label: 'Other' },
|
{ value: 'other', label: 'Other' },
|
||||||
]}
|
]}
|
||||||
onValueChange={handleApplicationChange}
|
selectedValue={layer.application}
|
||||||
|
onSelect={handleApplicationChange}
|
||||||
|
columns={2}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Picker
|
<Text style={styles.label}>Position on Piece</Text>
|
||||||
label="Position on Piece"
|
<ButtonGrid
|
||||||
value={layer.position}
|
|
||||||
options={[
|
options={[
|
||||||
{ value: 'entire', label: 'Entire Piece' },
|
{ value: 'entire', label: 'Entire Piece' },
|
||||||
{ value: 'top', label: 'Top' },
|
{ value: 'top', label: 'Top' },
|
||||||
|
|
@ -101,7 +102,9 @@ export const GlazeLayerCard: React.FC<GlazeLayerCardProps> = ({
|
||||||
{ value: 'outside', label: 'Outside' },
|
{ value: 'outside', label: 'Outside' },
|
||||||
{ value: 'other', label: 'Other' },
|
{ value: 'other', label: 'Other' },
|
||||||
]}
|
]}
|
||||||
onValueChange={handlePositionChange}
|
selectedValue={layer.position}
|
||||||
|
onSelect={handlePositionChange}
|
||||||
|
columns={2}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<View style={styles.coatsContainer}>
|
<View style={styles.coatsContainer}>
|
||||||
|
|
|
||||||
|
|
@ -111,16 +111,19 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||||
};
|
};
|
||||||
|
|
||||||
await AsyncStorage.setItem(AUTH_STORAGE_KEY, JSON.stringify(currentUser));
|
await AsyncStorage.setItem(AUTH_STORAGE_KEY, JSON.stringify(currentUser));
|
||||||
setUser(currentUser);
|
|
||||||
|
|
||||||
// Load onboarding status for returning user
|
// Load onboarding status for returning user BEFORE setting user state
|
||||||
|
// This prevents the flickering of the Onboarding screen
|
||||||
const onboardingData = await AsyncStorage.getItem(ONBOARDING_STORAGE_KEY);
|
const onboardingData = await AsyncStorage.getItem(ONBOARDING_STORAGE_KEY);
|
||||||
|
let isCompleted = false;
|
||||||
if (onboardingData) {
|
if (onboardingData) {
|
||||||
const completedUsers = JSON.parse(onboardingData);
|
const completedUsers = JSON.parse(onboardingData);
|
||||||
setHasCompletedOnboarding(completedUsers[currentUser.id] === true);
|
isCompleted = completedUsers[currentUser.id] === true;
|
||||||
} else {
|
|
||||||
setHasCompletedOnboarding(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update states together
|
||||||
|
setHasCompletedOnboarding(isCompleted);
|
||||||
|
setUser(currentUser);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Sign in failed:', error);
|
console.error('Sign in failed:', error);
|
||||||
throw error;
|
throw error;
|
||||||
|
|
|
||||||
|
|
@ -4,46 +4,46 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const colors = {
|
export const colors = {
|
||||||
// Primary colors - Coral/Orange for buttons and accents
|
// Primary colors - Sage/Olive green from new palette
|
||||||
primary: '#E07A5F', // Coral/Orange accent
|
primary: '#8B9A7C', // Sage/Olive green
|
||||||
primaryLight: '#BF6B50', // Darker coral for cards/active states
|
primaryLight: '#B0B1A5', // Lighter sage
|
||||||
primaryDark: '#C56548', // Darker coral for active states
|
primaryDark: '#7A7B70', // Darker sage
|
||||||
|
|
||||||
// Accent colors - Green for status indicators
|
// Accent colors - Beige/Sand from new palette
|
||||||
accent: '#537A2F', // Green accent
|
accent: '#DECCA3', // Beige/Sand
|
||||||
accentLight: '#6B9A3E', // Lighter green
|
accentLight: '#E8DCC0', // Lighter beige
|
||||||
accentDark: '#3F5D24', // Darker green
|
accentDark: '#C9B78E', // Darker beige
|
||||||
|
|
||||||
// Backgrounds - Dark brown theme
|
// Backgrounds - Light cream theme
|
||||||
background: '#3D352E', // Main dark brown
|
background: '#F8F6F0', // Main cream/off-white
|
||||||
backgroundSecondary: '#4F4640', // Card/Surface warm brown
|
backgroundSecondary: '#EEEEEE', // Light gray for cards
|
||||||
card: '#4F4640', // Same as backgroundSecondary
|
card: '#FFFFFF', // White cards
|
||||||
|
|
||||||
// Text - Light on dark
|
// Text - Dark on light backgrounds
|
||||||
text: '#D2CCC5', // Primary text (light warm)
|
text: '#3D352E', // Primary text (dark brown)
|
||||||
textSecondary: '#BFC0C1', // Secondary text
|
textSecondary: '#5A514B', // Secondary text
|
||||||
textTertiary: '#959693', // Dimmed text/icons
|
textTertiary: '#7A7570', // Dimmed text/icons
|
||||||
buttonText: '#FFFFFF', // White text for primary buttons (better contrast)
|
buttonText: '#FFFFFF', // White text for primary buttons
|
||||||
|
|
||||||
// Borders - Subtle on dark backgrounds
|
// Borders - Subtle on light backgrounds
|
||||||
border: '#6B6460', // Slightly lighter than card for visibility
|
border: '#DECCA3', // Beige border
|
||||||
borderLight: '#5A514B', // Chip background color
|
borderLight: '#EEEEEE', // Light gray border
|
||||||
|
|
||||||
// Status colors
|
// Status colors
|
||||||
success: '#537A2F', // Green accent (same as accent)
|
success: '#7A9B6B', // Soft green
|
||||||
successLight: '#6B9A3E', // Lighter green background
|
successLight: '#A3C494', // Lighter green
|
||||||
warning: '#E07A5F', // Use coral for warnings on dark bg
|
warning: '#D4A574', // Warm amber
|
||||||
error: '#DC2626', // Keep red for errors (good contrast)
|
error: '#C5675C', // Soft red
|
||||||
info: '#E07A5F', // Use coral for info
|
info: '#9A9B8E', // Sage (same as primary)
|
||||||
|
|
||||||
// Step type colors - Adjusted for dark backgrounds
|
// Step type colors - Adjusted for light backgrounds
|
||||||
forming: '#C89B7C', // Lighter clay tone
|
forming: '#C89B7C', // Clay tone
|
||||||
trimming: '#A89080', // Neutral tan
|
trimming: '#9A9B8E', // Sage green
|
||||||
drying: '#D9C5A0', // Lighter sand
|
drying: '#DECCA3', // Beige/sand
|
||||||
bisqueFiring: '#D67B59', // Coral variant
|
bisqueFiring: '#D4A574', // Warm amber
|
||||||
glazing: '#9B8B9B', // Lighter mauve
|
glazing: '#A89B9B', // Mauve
|
||||||
glazeFiring: '#E07A5F', // Coral (same as primary)
|
glazeFiring: '#C5675C', // Terracotta
|
||||||
misc: '#959693', // Same as textTertiary
|
misc: '#7A7570', // Gray
|
||||||
};
|
};
|
||||||
|
|
||||||
export const spacing = {
|
export const spacing = {
|
||||||
|
|
@ -125,13 +125,13 @@ export const stepTypeColors = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const stepTypeIcons: Record<string, string> = {
|
export const stepTypeIcons: Record<string, string> = {
|
||||||
forming: '🏺',
|
forming: 'hand-front-right',
|
||||||
trimming: '🔧',
|
trimming: 'content-cut',
|
||||||
drying: '☀️',
|
drying: 'weather-windy',
|
||||||
bisque_firing: '🔥',
|
bisque_firing: 'fire',
|
||||||
glazing: '🎨',
|
glazing: 'brush',
|
||||||
glaze_firing: '⚡',
|
glaze_firing: 'lightning-bolt',
|
||||||
misc: '📝',
|
misc: 'notebook-outline',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const stepTypeLabels: Record<string, string> = {
|
export const stepTypeLabels: Record<string, string> = {
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,9 @@ import { Step, StepType } from '../../types';
|
||||||
*/
|
*/
|
||||||
const STEP_ORDER: StepType[] = [
|
const STEP_ORDER: StepType[] = [
|
||||||
'forming',
|
'forming',
|
||||||
|
'trimming',
|
||||||
'drying',
|
'drying',
|
||||||
'bisque_firing',
|
'bisque_firing',
|
||||||
'trimming',
|
|
||||||
'glazing',
|
'glazing',
|
||||||
'glaze_firing',
|
'glaze_firing',
|
||||||
'misc',
|
'misc',
|
||||||
|
|
@ -44,17 +44,30 @@ export function sortStepsByLogicalOrder(steps: Step[]): Step[] {
|
||||||
* Returns null if all typical steps are completed
|
* Returns null if all typical steps are completed
|
||||||
*/
|
*/
|
||||||
export function suggestNextStep(completedSteps: Step[]): StepType | null {
|
export function suggestNextStep(completedSteps: Step[]): StepType | null {
|
||||||
|
if (completedSteps.length === 0) {
|
||||||
|
return 'forming';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the highest step index completed so far
|
||||||
|
let highestIndex = -1;
|
||||||
const completedTypes = new Set(completedSteps.map(s => s.type));
|
const completedTypes = new Set(completedSteps.map(s => s.type));
|
||||||
|
|
||||||
// Find first step type not yet completed
|
for (const stepType of completedTypes) {
|
||||||
for (const stepType of STEP_ORDER) {
|
const index = getStepOrderIndex(stepType as StepType);
|
||||||
if (stepType === 'misc') continue; // Skip misc, it's optional
|
if (index > highestIndex && index < 999) { // 999 is invalid/misc
|
||||||
if (!completedTypes.has(stepType)) {
|
highestIndex = index;
|
||||||
return stepType;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null; // All steps completed
|
// Suggest the next step in the sequence
|
||||||
|
if (highestIndex !== -1 && highestIndex < STEP_ORDER.length - 1) {
|
||||||
|
const nextStepInfo = STEP_ORDER[highestIndex + 1];
|
||||||
|
if (nextStepInfo !== 'misc') {
|
||||||
|
return nextStepInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -136,18 +136,19 @@ function MainTabs() {
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
tabBarContainer: {
|
tabBarContainer: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
backgroundColor: colors.background,
|
backgroundColor: '#FFFFFF',
|
||||||
height: 65,
|
height: 65,
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
bottom: 20,
|
bottom: 20,
|
||||||
left: 10,
|
left: 10,
|
||||||
right: 10,
|
right: 10,
|
||||||
borderRadius: 25,
|
borderRadius: 25,
|
||||||
shadowColor: colors.text,
|
// Strong floating shadow
|
||||||
shadowOffset: { width: 0, height: -2 },
|
shadowColor: '#000000',
|
||||||
shadowOpacity: 0.1,
|
shadowOffset: { width: 0, height: 8 },
|
||||||
shadowRadius: 8,
|
shadowOpacity: 0.15,
|
||||||
elevation: 8,
|
shadowRadius: 16,
|
||||||
|
elevation: 12,
|
||||||
},
|
},
|
||||||
tabItem: {
|
tabItem: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
|
@ -156,18 +157,31 @@ const styles = StyleSheet.create({
|
||||||
paddingVertical: 8,
|
paddingVertical: 8,
|
||||||
},
|
},
|
||||||
tabItemActive: {
|
tabItemActive: {
|
||||||
backgroundColor: colors.primaryLight,
|
backgroundColor: '#DCDCDC', // Darker inner gray
|
||||||
borderWidth: 2,
|
borderColor: '#FFFFFF', // Light outer border
|
||||||
borderColor: colors.primary,
|
borderWidth: 3,
|
||||||
|
borderRadius: 18,
|
||||||
|
marginHorizontal: 5,
|
||||||
|
marginVertical: 1, // Maximized height
|
||||||
|
paddingTop: 1,
|
||||||
|
paddingBottom: 5, // Push text up away from border
|
||||||
|
// Refined shadow
|
||||||
|
shadowColor: '#000000',
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
shadowOpacity: 0.15,
|
||||||
|
shadowRadius: 3,
|
||||||
|
elevation: 3,
|
||||||
},
|
},
|
||||||
tabItemActiveFirst: {
|
tabItemActiveFirst: {
|
||||||
borderRadius: 25,
|
borderTopLeftRadius: 20,
|
||||||
|
borderBottomLeftRadius: 20,
|
||||||
},
|
},
|
||||||
tabItemActiveLast: {
|
tabItemActiveLast: {
|
||||||
borderRadius: 25,
|
borderTopRightRadius: 20,
|
||||||
|
borderBottomRightRadius: 20,
|
||||||
},
|
},
|
||||||
tabItemActiveMiddle: {
|
tabItemActiveMiddle: {
|
||||||
borderRadius: 25,
|
borderRadius: 18,
|
||||||
},
|
},
|
||||||
imageIconContainer: {
|
imageIconContainer: {
|
||||||
width: 40,
|
width: 40,
|
||||||
|
|
@ -245,14 +259,16 @@ export function AppNavigator() {
|
||||||
options={{ headerShown: false }}
|
options={{ headerShown: false }}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : !hasCompletedOnboarding ? (
|
||||||
// App screens - logged in (initial route determined by hasCompletedOnboarding)
|
// Onboarding flow - explicit separation ensures it's not skipped
|
||||||
<>
|
|
||||||
<RootStack.Screen
|
<RootStack.Screen
|
||||||
name="Onboarding"
|
name="Onboarding"
|
||||||
component={OnboardingScreen}
|
component={OnboardingScreen}
|
||||||
options={{ headerShown: false }}
|
options={{ headerShown: false }}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
// App screens - logged in AND onboarding completed
|
||||||
|
<>
|
||||||
<RootStack.Screen
|
<RootStack.Screen
|
||||||
name="MainTabs"
|
name="MainTabs"
|
||||||
component={MainTabs}
|
component={MainTabs}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { NavigatorScreenParams } from '@react-navigation/native';
|
import { NavigatorScreenParams } from '@react-navigation/native';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { GlazeLayer, UserListType } from '../types';
|
import { GlazeLayer, UserListType, StepType } from '../types';
|
||||||
|
|
||||||
export type RootStackParamList = {
|
export type RootStackParamList = {
|
||||||
Login: undefined;
|
Login: undefined;
|
||||||
|
|
@ -14,6 +14,7 @@ export type RootStackParamList = {
|
||||||
selectedGlazeIds?: string[]; // DEPRECATED - keep for backwards compat
|
selectedGlazeIds?: string[]; // DEPRECATED - keep for backwards compat
|
||||||
newGlazeLayers?: GlazeLayer[]; // NEW
|
newGlazeLayers?: GlazeLayer[]; // NEW
|
||||||
mixNotes?: string;
|
mixNotes?: string;
|
||||||
|
initialStepType?: StepType; // NEW
|
||||||
_timestamp?: number;
|
_timestamp?: number;
|
||||||
_editorKey?: string
|
_editorKey?: string
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,25 @@ import { RootStackParamList } from '../navigation/types';
|
||||||
import { Project, Step } from '../types';
|
import { Project, Step } from '../types';
|
||||||
import { getAllProjects, getStepsByProject } from '../lib/db/repositories';
|
import { getAllProjects, getStepsByProject } from '../lib/db/repositories';
|
||||||
import { Card, PotteryIcon } from '../components';
|
import { Card, PotteryIcon } from '../components';
|
||||||
import { colors, spacing, typography, borderRadius, stepTypeLabels } from '../lib/theme';
|
import { colors, spacing, typography, borderRadius, stepTypeLabels, shadows } from '../lib/theme';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
|
|
||||||
const settingsIcon = require('../../assets/images/settings_icon.png');
|
const settingsIcon = require('../../assets/images/settings_icon.png');
|
||||||
|
|
||||||
|
const heroImages = [
|
||||||
|
require('../../assets/images/hero_pottery.jpg'),
|
||||||
|
require('../../assets/images/pottery_bg_1.png'),
|
||||||
|
require('../../assets/images/pottery_bg_2.png'),
|
||||||
|
require('../../assets/images/pottery_bg_3.png'),
|
||||||
|
require('../../assets/images/pottery_bg_4.png'),
|
||||||
|
require('../../assets/images/pottery_bg_5.png'), // NEW
|
||||||
|
require('../../assets/images/pottery_bg_6.png'), // NEW
|
||||||
|
require('../../assets/images/pottery_bg_7.png'), // NEW
|
||||||
|
];
|
||||||
|
|
||||||
|
// Select random image once when module loads (on app start/reload)
|
||||||
|
const randomHeroImage = heroImages[Math.floor(Math.random() * heroImages.length)];
|
||||||
|
|
||||||
type NavigationProp = NativeStackNavigationProp<RootStackParamList>;
|
type NavigationProp = NativeStackNavigationProp<RootStackParamList>;
|
||||||
|
|
||||||
export const HomeScreen: React.FC = () => {
|
export const HomeScreen: React.FC = () => {
|
||||||
|
|
@ -83,7 +97,7 @@ export const HomeScreen: React.FC = () => {
|
||||||
// Calculate stats
|
// Calculate stats
|
||||||
const totalProjects = projects.length;
|
const totalProjects = projects.length;
|
||||||
const inProgressProjects = projects.filter(p => p.status === 'in_progress').length;
|
const inProgressProjects = projects.filter(p => p.status === 'in_progress').length;
|
||||||
const completedProjects = projects.filter(p => p.status === 'completed').length;
|
const completedProjects = projects.filter(p => p.status === 'done').length;
|
||||||
|
|
||||||
// Get recent projects (last 3)
|
// Get recent projects (last 3)
|
||||||
const recentProjects = projects
|
const recentProjects = projects
|
||||||
|
|
@ -115,39 +129,39 @@ export const HomeScreen: React.FC = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.container} edges={['top']}>
|
<SafeAreaView style={styles.container} edges={[]}>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
style={styles.scrollView}
|
style={styles.scrollView}
|
||||||
contentContainerStyle={styles.scrollContent}
|
contentContainerStyle={styles.scrollContentNoPadding}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
>
|
>
|
||||||
|
{/* Hero Section with Full-Width Edge-to-Edge Image */}
|
||||||
|
<View style={styles.heroSection}>
|
||||||
|
{/* Hero Image - edge to edge */}
|
||||||
|
<Image source={randomHeroImage} style={styles.heroImage} resizeMode="cover" />
|
||||||
|
{/* Content Overlay */}
|
||||||
|
<View style={styles.heroOverlay}>
|
||||||
{/* Top App Bar */}
|
{/* Top App Bar */}
|
||||||
<View style={styles.topBar}>
|
<View style={styles.topBar}>
|
||||||
<View style={styles.greetingContainer}>
|
<View style={styles.greetingContainer}>
|
||||||
<Text style={styles.greetingText}>Welcome Back,</Text>
|
<Text style={styles.greetingText}>Welcome Back,</Text>
|
||||||
<Text style={styles.nameText}>{user?.name || 'Potter'}</Text>
|
<Text style={styles.nameText}>{user?.name || 'Potter'}</Text>
|
||||||
</View>
|
</View>
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.settingsButton}
|
|
||||||
onPress={() => navigation.navigate('MainTabs', { screen: 'Settings' })}
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
source={settingsIcon}
|
|
||||||
style={styles.settingsIconImage}
|
|
||||||
resizeMode="contain"
|
|
||||||
/>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Stats Section */}
|
{/* Stats */}
|
||||||
<View style={styles.statsContainer}>
|
<View style={styles.statsContainer}>
|
||||||
<View style={styles.statCard}>
|
<View style={styles.statsRow}>
|
||||||
<Text style={styles.statLabel}>Total Projects</Text>
|
<View style={styles.statItem}>
|
||||||
<Text style={styles.statNumber}>{totalProjects}</Text>
|
<Text style={styles.statNumber}>{totalProjects}</Text>
|
||||||
|
<Text style={styles.statLabel}>Projects</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.statCard}>
|
<View style={styles.statItem}>
|
||||||
<Text style={styles.statLabel}>In Progress</Text>
|
|
||||||
<Text style={styles.statNumber}>{inProgressProjects}</Text>
|
<Text style={styles.statNumber}>{inProgressProjects}</Text>
|
||||||
|
<Text style={styles.statLabel}>In Progress</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|
@ -159,12 +173,6 @@ export const HomeScreen: React.FC = () => {
|
||||||
>
|
>
|
||||||
<Text style={styles.primaryButtonText}>New Pottery Project</Text>
|
<Text style={styles.primaryButtonText}>New Pottery Project</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.secondaryButton}
|
|
||||||
onPress={handleBrowseTips}
|
|
||||||
>
|
|
||||||
<Text style={styles.secondaryButtonText}>Browse Pottery Tips</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Section Header */}
|
{/* Section Header */}
|
||||||
|
|
@ -248,12 +256,16 @@ const styles = StyleSheet.create({
|
||||||
paddingHorizontal: spacing.lg,
|
paddingHorizontal: spacing.lg,
|
||||||
paddingBottom: 100,
|
paddingBottom: 100,
|
||||||
},
|
},
|
||||||
|
scrollContentNoPadding: {
|
||||||
|
paddingBottom: 100,
|
||||||
|
},
|
||||||
topBar: {
|
topBar: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'flex-start',
|
justifyContent: 'flex-start',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingVertical: spacing.lg,
|
paddingTop: spacing.xl * 3,
|
||||||
marginBottom: spacing.lg,
|
paddingHorizontal: spacing.lg,
|
||||||
|
marginBottom: spacing.md,
|
||||||
gap: spacing.lg,
|
gap: spacing.lg,
|
||||||
},
|
},
|
||||||
greetingContainer: {
|
greetingContainer: {
|
||||||
|
|
@ -279,40 +291,70 @@ const styles = StyleSheet.create({
|
||||||
height: 32,
|
height: 32,
|
||||||
opacity: 0.8,
|
opacity: 0.8,
|
||||||
},
|
},
|
||||||
statsContainer: {
|
heroSection: {
|
||||||
flexDirection: 'row',
|
position: 'relative',
|
||||||
gap: spacing.md,
|
height: 380,
|
||||||
marginBottom: spacing.xl,
|
marginBottom: spacing.xl,
|
||||||
},
|
},
|
||||||
statCard: {
|
heroImage: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
opacity: 0.21,
|
||||||
|
},
|
||||||
|
heroOverlay: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: colors.card,
|
},
|
||||||
padding: spacing.lg,
|
statsContainer: {
|
||||||
borderRadius: borderRadius.lg,
|
flex: 1,
|
||||||
borderWidth: 1,
|
justifyContent: 'center',
|
||||||
borderColor: colors.border,
|
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
paddingHorizontal: spacing.lg,
|
||||||
|
},
|
||||||
|
statsRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: spacing.xl * 2,
|
||||||
|
},
|
||||||
|
statItem: {
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
statNumber: {
|
||||||
|
fontSize: 48,
|
||||||
|
fontWeight: typography.fontWeight.bold,
|
||||||
|
color: '#8B9A7C', // Sage green from palette
|
||||||
|
textShadowColor: 'rgba(0, 0, 0, 0.2)',
|
||||||
|
textShadowOffset: { width: 1, height: 1 },
|
||||||
|
textShadowRadius: 3,
|
||||||
},
|
},
|
||||||
statLabel: {
|
statLabel: {
|
||||||
fontSize: typography.fontSize.sm,
|
fontSize: typography.fontSize.sm,
|
||||||
color: colors.textSecondary,
|
color: colors.textSecondary,
|
||||||
marginBottom: spacing.xs,
|
textTransform: 'uppercase',
|
||||||
},
|
letterSpacing: 0.5,
|
||||||
statNumber: {
|
textShadowColor: 'rgba(0, 0, 0, 0.1)',
|
||||||
fontSize: 32,
|
textShadowOffset: { width: 1, height: 1 },
|
||||||
fontWeight: typography.fontWeight.bold,
|
textShadowRadius: 2,
|
||||||
color: colors.primary,
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
buttonGroup: {
|
buttonGroup: {
|
||||||
gap: spacing.md,
|
gap: spacing.md,
|
||||||
marginBottom: spacing.xl,
|
marginBottom: spacing.xl,
|
||||||
|
paddingHorizontal: spacing.lg,
|
||||||
},
|
},
|
||||||
primaryButton: {
|
primaryButton: {
|
||||||
backgroundColor: colors.primaryLight,
|
backgroundColor: '#8B9A7C', // Sage green from palette
|
||||||
paddingVertical: spacing.md,
|
paddingVertical: spacing.md,
|
||||||
paddingHorizontal: spacing.lg,
|
paddingHorizontal: spacing.lg,
|
||||||
borderRadius: borderRadius.lg,
|
borderRadius: borderRadius.lg,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
// Floating effect
|
||||||
|
shadowColor: '#000000',
|
||||||
|
shadowOffset: { width: 0, height: 4 },
|
||||||
|
shadowOpacity: 0.3,
|
||||||
|
shadowRadius: 8,
|
||||||
|
elevation: 8,
|
||||||
},
|
},
|
||||||
primaryButtonText: {
|
primaryButtonText: {
|
||||||
fontSize: typography.fontSize.md,
|
fontSize: typography.fontSize.md,
|
||||||
|
|
@ -338,6 +380,7 @@ const styles = StyleSheet.create({
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginBottom: spacing.md,
|
marginBottom: spacing.md,
|
||||||
|
paddingHorizontal: spacing.lg,
|
||||||
},
|
},
|
||||||
sectionTitle: {
|
sectionTitle: {
|
||||||
fontSize: typography.fontSize.lg,
|
fontSize: typography.fontSize.lg,
|
||||||
|
|
@ -351,6 +394,7 @@ const styles = StyleSheet.create({
|
||||||
},
|
},
|
||||||
projectsList: {
|
projectsList: {
|
||||||
gap: spacing.md,
|
gap: spacing.md,
|
||||||
|
paddingHorizontal: spacing.lg,
|
||||||
},
|
},
|
||||||
projectItem: {
|
projectItem: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
|
@ -359,8 +403,9 @@ const styles = StyleSheet.create({
|
||||||
backgroundColor: colors.card,
|
backgroundColor: colors.card,
|
||||||
padding: spacing.md,
|
padding: spacing.md,
|
||||||
borderRadius: borderRadius.lg,
|
borderRadius: borderRadius.lg,
|
||||||
borderWidth: 1,
|
borderWidth: 2,
|
||||||
borderColor: colors.border,
|
borderColor: colors.border,
|
||||||
|
...shadows.md,
|
||||||
},
|
},
|
||||||
projectItemLeft: {
|
projectItemLeft: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
|
|
||||||
|
|
@ -63,9 +63,11 @@ export const LoginScreen: React.FC = () => {
|
||||||
|
|
||||||
{/* Hero Image */}
|
{/* Hero Image */}
|
||||||
<View style={styles.heroContainer}>
|
<View style={styles.heroContainer}>
|
||||||
<View style={styles.heroImagePlaceholder}>
|
<Image
|
||||||
<Text style={styles.heroEmoji}>🏺</Text>
|
source={require('../../assets/images/app_logo.png')}
|
||||||
</View>
|
style={styles.heroImage}
|
||||||
|
resizeMode="contain"
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Form */}
|
{/* Form */}
|
||||||
|
|
@ -91,7 +93,7 @@ export const LoginScreen: React.FC = () => {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.primaryButton}
|
style={[styles.primaryButton, { marginTop: spacing.xl }]}
|
||||||
onPress={handleLogin}
|
onPress={handleLogin}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
|
|
@ -100,25 +102,8 @@ export const LoginScreen: React.FC = () => {
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
{/* Divider */}
|
|
||||||
<View style={styles.divider}>
|
|
||||||
<View style={styles.dividerLine} />
|
|
||||||
<Text style={styles.dividerText}>OR</Text>
|
|
||||||
<View style={styles.dividerLine} />
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Social Buttons */}
|
|
||||||
<View style={styles.socialButtons}>
|
|
||||||
<TouchableOpacity style={styles.socialButton}>
|
|
||||||
<Text style={styles.socialButtonText}>G</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<TouchableOpacity style={styles.socialButton}>
|
|
||||||
<Text style={styles.socialButtonText}>🍎</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Sign Up Link */}
|
{/* Sign Up Link */}
|
||||||
<View style={styles.signupContainer}>
|
<View style={[styles.signupContainer, { marginTop: spacing.xl }]}>
|
||||||
<Text style={styles.signupText}>Don't have an account? </Text>
|
<Text style={styles.signupText}>Don't have an account? </Text>
|
||||||
<TouchableOpacity onPress={() => navigation.navigate('SignUp')}>
|
<TouchableOpacity onPress={() => navigation.navigate('SignUp')}>
|
||||||
<Text style={styles.signupLink}>Create Account</Text>
|
<Text style={styles.signupLink}>Create Account</Text>
|
||||||
|
|
@ -165,16 +150,10 @@ const styles = StyleSheet.create({
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginVertical: spacing.xl * 1.5,
|
marginVertical: spacing.xl * 1.5,
|
||||||
},
|
},
|
||||||
heroImagePlaceholder: {
|
heroImage: {
|
||||||
width: 200,
|
width: 180,
|
||||||
height: 200,
|
height: 180,
|
||||||
borderRadius: borderRadius.lg,
|
borderRadius: borderRadius.lg,
|
||||||
backgroundColor: colors.primaryLight,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
heroEmoji: {
|
|
||||||
fontSize: 100,
|
|
||||||
},
|
},
|
||||||
form: {
|
form: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { View, Text, StyleSheet, ScrollView, TouchableOpacity } from 'react-native';
|
import { View, Text, StyleSheet, ScrollView, TouchableOpacity } from 'react-native';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
import { Card } from '../components';
|
import { Card } from '../components';
|
||||||
import { colors, spacing, typography, borderRadius } from '../lib/theme';
|
import { colors, spacing, typography, borderRadius } from '../lib/theme';
|
||||||
|
|
||||||
|
|
@ -9,7 +10,7 @@ interface Tip {
|
||||||
title: string;
|
title: string;
|
||||||
content: string;
|
content: string;
|
||||||
category: string;
|
category: string;
|
||||||
icon: string;
|
icon: keyof typeof MaterialCommunityIcons.glyphMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
const POTTERY_TIPS: Tip[] = [
|
const POTTERY_TIPS: Tip[] = [
|
||||||
|
|
@ -18,227 +19,243 @@ const POTTERY_TIPS: Tip[] = [
|
||||||
title: 'Wedging Clay',
|
title: 'Wedging Clay',
|
||||||
content: 'Always wedge your clay thoroughly before throwing to remove air bubbles and ensure consistent texture. Aim for 50-100 wedges.',
|
content: 'Always wedge your clay thoroughly before throwing to remove air bubbles and ensure consistent texture. Aim for 50-100 wedges.',
|
||||||
category: 'Basics',
|
category: 'Basics',
|
||||||
icon: '🏺',
|
icon: 'shape-plus',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '2',
|
id: '2',
|
||||||
title: 'Drying Time',
|
title: 'Drying Time',
|
||||||
content: 'Dry pieces slowly and evenly. Cover with plastic for controlled drying. Uneven drying leads to cracks!',
|
content: 'Dry pieces slowly and evenly. Cover with plastic for controlled drying. Uneven drying leads to cracks!',
|
||||||
category: 'Drying',
|
category: 'Drying',
|
||||||
icon: '☀️',
|
icon: 'weather-sunny',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '3',
|
id: '3',
|
||||||
title: 'Bisque Firing',
|
title: 'Bisque Firing',
|
||||||
content: 'Bisque fire to Cone 04 (1945°F) for most clay bodies. This makes pieces porous and ready for glazing.',
|
content: 'Bisque fire to Cone 04 (1945°F) for most clay bodies. This makes pieces porous and ready for glazing.',
|
||||||
category: 'Firing',
|
category: 'Firing',
|
||||||
icon: '🔥',
|
icon: 'fire',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '4',
|
id: '4',
|
||||||
title: 'Glaze Application',
|
title: 'Glaze Application',
|
||||||
content: 'Apply 2-3 coats of glaze for best results. Too thin = bare spots, too thick = running. Test on tiles first!',
|
content: 'Apply 2-3 coats of glaze for best results. Too thin = bare spots, too thick = running. Test on tiles first!',
|
||||||
category: 'Glazing',
|
category: 'Glazing',
|
||||||
icon: '🎨',
|
icon: 'brush',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '5',
|
id: '5',
|
||||||
title: 'Cone 6 is Popular',
|
title: 'Cone 6 is Popular',
|
||||||
content: 'Cone 6 (2232°F) is the most common mid-range firing temperature. Great balance of durability and glaze options.',
|
content: 'Cone 6 (2232°F) is the most common mid-range firing temperature. Great balance of durability and glaze options.',
|
||||||
category: 'Firing',
|
category: 'Firing',
|
||||||
icon: '⚡',
|
icon: 'flash',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '6',
|
id: '6',
|
||||||
title: 'Document Everything',
|
title: 'Document Everything',
|
||||||
content: 'Keep detailed notes! Record clay type, glaze brands, cone numbers, and firing schedules. This app helps!',
|
content: 'Keep detailed notes! Record clay type, glaze brands, cone numbers, and firing schedules. This app helps!',
|
||||||
category: 'Best Practice',
|
category: 'Best Practice',
|
||||||
icon: '📝',
|
icon: 'notebook-edit',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '7',
|
id: '7',
|
||||||
title: 'Test Tiles',
|
title: 'Test Tiles',
|
||||||
content: 'Always make test tiles for new glaze combinations. Save yourself from ruined pieces!',
|
content: 'Always make test tiles for new glaze combinations. Save yourself from ruined pieces!',
|
||||||
category: 'Glazing',
|
category: 'Glazing',
|
||||||
icon: '🧪',
|
icon: 'flask',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '8',
|
id: '8',
|
||||||
title: 'Thickness Matters',
|
title: 'Thickness Matters',
|
||||||
content: 'Keep walls consistent - about 1/4 to 3/8 inch thick. Thinner walls fire more evenly.',
|
content: 'Keep walls consistent - about 1/4 to 3/8 inch thick. Thinner walls fire more evenly.',
|
||||||
category: 'Forming',
|
category: 'Forming',
|
||||||
icon: '📏',
|
icon: 'ruler',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '9',
|
id: '9',
|
||||||
title: 'Trimming Timing',
|
title: 'Trimming Timing',
|
||||||
content: 'Trim when leather-hard (firm but not dry). Too wet = distorts, too dry = cracks.',
|
content: 'Trim when leather-hard (firm but not dry). Too wet = distorts, too dry = cracks.',
|
||||||
category: 'Trimming',
|
category: 'Trimming',
|
||||||
icon: '🔧',
|
icon: 'tools',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '10',
|
id: '10',
|
||||||
title: 'Foot Rings',
|
title: 'Foot Rings',
|
||||||
content: 'Add a foot ring for stability and to elevate the piece from surfaces. Enhances the final look!',
|
content: 'Add a foot ring for stability and to elevate the piece from surfaces. Enhances the final look!',
|
||||||
category: 'Trimming',
|
category: 'Trimming',
|
||||||
icon: '⭕',
|
icon: 'circle-double',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '11',
|
id: '11',
|
||||||
title: 'Reclaim Clay',
|
title: 'Reclaim Clay',
|
||||||
content: 'Save all scraps! Dry completely, crush, add water, and wedge thoroughly to reuse. Nothing goes to waste.',
|
content: 'Save all scraps! Dry completely, crush, add water, and wedge thoroughly to reuse. Nothing goes to waste.',
|
||||||
category: 'Recycling',
|
category: 'Recycling',
|
||||||
icon: '♻️',
|
icon: 'recycle',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '12',
|
id: '12',
|
||||||
title: 'Never Mix Clay Bodies',
|
title: 'Never Mix Clay Bodies',
|
||||||
content: 'Keep different clay types separate to avoid firing issues. Different clays shrink at different rates!',
|
content: 'Keep different clay types separate to avoid firing issues. Different clays shrink at different rates!',
|
||||||
category: 'Recycling',
|
category: 'Recycling',
|
||||||
icon: '⚠️',
|
icon: 'alert-circle',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '13',
|
id: '13',
|
||||||
title: 'Wire Tool',
|
title: 'Wire Tool',
|
||||||
content: 'Use a twisted wire for cutting clay. Keep it taut for clean cuts and remove pieces from the wheel.',
|
content: 'Use a twisted wire for cutting clay. Keep it taut for clean cuts and remove pieces from the wheel.',
|
||||||
category: 'Tools',
|
category: 'Tools',
|
||||||
icon: '✂️',
|
icon: 'content-cut',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '14',
|
id: '14',
|
||||||
title: 'Sponge Control',
|
title: 'Sponge Control',
|
||||||
content: 'Don\'t over-sponge! Too much water weakens your piece and can cause collapse. Light touch is key.',
|
content: 'Don\'t over-sponge! Too much water weakens your piece and can cause collapse. Light touch is key.',
|
||||||
category: 'Tools',
|
category: 'Tools',
|
||||||
icon: '🧽',
|
icon: 'water',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '15',
|
id: '15',
|
||||||
title: 'Underglaze vs Glaze',
|
title: 'Underglaze vs Glaze',
|
||||||
content: 'Underglazes go on greenware or bisque, glazes only on bisque. Knowing the difference prevents mistakes!',
|
content: 'Underglazes go on greenware or bisque, glazes only on bisque. Knowing the difference prevents mistakes!',
|
||||||
category: 'Surface',
|
category: 'Surface',
|
||||||
icon: '🖌️',
|
icon: 'brush-variant',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '16',
|
id: '16',
|
||||||
title: 'Sgraffito Timing',
|
title: 'Sgraffito Timing',
|
||||||
content: 'Scratch through slip or underglaze when leather-hard for crisp, clean lines. Sharp tools work best!',
|
content: 'Scratch through slip or underglaze when leather-hard for crisp, clean lines. Sharp tools work best!',
|
||||||
category: 'Surface',
|
category: 'Surface',
|
||||||
icon: '✏️',
|
icon: 'pencil',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '17',
|
id: '17',
|
||||||
title: 'Compression',
|
title: 'Compression',
|
||||||
content: 'Compress the bottom of thrown pieces to prevent cracking. Press firmly while centering to strengthen clay.',
|
content: 'Compress the bottom of thrown pieces to prevent cracking. Press firmly while centering to strengthen clay.',
|
||||||
category: 'Forming',
|
category: 'Forming',
|
||||||
icon: '💪',
|
icon: 'weight',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '18',
|
id: '18',
|
||||||
title: 'S-Cracks Prevention',
|
title: 'S-Cracks Prevention',
|
||||||
content: 'Pull from center outward when throwing to avoid S-cracks in the bottom. Consistent technique is crucial!',
|
content: 'Pull from center outward when throwing to avoid S-cracks in the bottom. Consistent technique is crucial!',
|
||||||
category: 'Forming',
|
category: 'Forming',
|
||||||
icon: '🌀',
|
icon: 'rotate-3d-variant',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '19',
|
id: '19',
|
||||||
title: 'Silica Dust Safety',
|
title: 'Silica Dust Safety',
|
||||||
content: 'Always wet-clean clay dust. Never sweep dry - silica dust is harmful to lungs! Your health matters.',
|
content: 'Always wet-clean clay dust. Never sweep dry - silica dust is harmful to lungs! Your health matters.',
|
||||||
category: 'Safety',
|
category: 'Safety',
|
||||||
icon: '🫁',
|
icon: 'air-filter',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '20',
|
id: '20',
|
||||||
title: 'Ventilation',
|
title: 'Ventilation',
|
||||||
content: 'Ensure good airflow when glazing and firing. Fumes can be toxic. Open windows or use exhaust fans!',
|
content: 'Ensure good airflow when glazing and firing. Fumes can be toxic. Open windows or use exhaust fans!',
|
||||||
category: 'Safety',
|
category: 'Safety',
|
||||||
icon: '🌬️',
|
icon: 'weather-windy',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '21',
|
id: '21',
|
||||||
title: 'Centering is Key',
|
title: 'Centering is Key',
|
||||||
content: 'Take your time centering. A well-centered piece prevents wobbling and uneven walls throughout the throwing process.',
|
content: 'Take your time centering. A well-centered piece prevents wobbling and uneven walls throughout the throwing process.',
|
||||||
category: 'Basics',
|
category: 'Basics',
|
||||||
icon: '🎯',
|
icon: 'target',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '22',
|
id: '22',
|
||||||
title: 'Coning Up & Down',
|
title: 'Coning Up & Down',
|
||||||
content: 'Cone clay up and down 2-3 times to align clay particles and strengthen the clay body before forming.',
|
content: 'Cone clay up and down 2-3 times to align clay particles and strengthen the clay body before forming.',
|
||||||
category: 'Basics',
|
category: 'Basics',
|
||||||
icon: '🔄',
|
icon: 'arrow-up-down',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '23',
|
id: '23',
|
||||||
title: 'Slow Bisque Start',
|
title: 'Slow Bisque Start',
|
||||||
content: 'Start bisque firing slowly (150°F/hr to 400°F) to allow water to escape without cracking your pieces.',
|
content: 'Start bisque firing slowly (150°F/hr to 400°F) to allow water to escape without cracking your pieces.',
|
||||||
category: 'Firing',
|
category: 'Firing',
|
||||||
icon: '🐌',
|
icon: 'speedometer-slow',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '24',
|
id: '24',
|
||||||
title: 'Pyrometric Cones',
|
title: 'Pyrometric Cones',
|
||||||
content: 'Always use witness cones! Digital controllers can fail. Cones don\'t lie about actual heatwork achieved.',
|
content: 'Always use witness cones! Digital controllers can fail. Cones don\'t lie about actual heatwork achieved.',
|
||||||
category: 'Firing',
|
category: 'Firing',
|
||||||
icon: '🎗️',
|
icon: 'cone',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '25',
|
id: '25',
|
||||||
title: 'Kiln Loading',
|
title: 'Kiln Loading',
|
||||||
content: 'Stack bisque with 1" spacing for proper airflow. Touching pieces can stick together or crack during firing.',
|
content: 'Stack bisque with 1" spacing for proper airflow. Touching pieces can stick together or crack during firing.',
|
||||||
category: 'Firing',
|
category: 'Firing',
|
||||||
icon: '📦',
|
icon: 'view-grid',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '26',
|
id: '26',
|
||||||
title: 'Glaze Thickness Test',
|
title: 'Glaze Thickness Test',
|
||||||
content: 'Dip a finger in wet glaze - you should see skin tone through it. Too opaque means it\'s too thick!',
|
content: 'Dip a finger in wet glaze - you should see skin tone through it. Too opaque means it\'s too thick!',
|
||||||
category: 'Glazing',
|
category: 'Glazing',
|
||||||
icon: '👆',
|
icon: 'gesture-tap',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '27',
|
id: '27',
|
||||||
title: 'Wax Resist',
|
title: 'Wax Resist',
|
||||||
content: 'Wax the bottom of pieces before glazing to prevent them from sticking to kiln shelves. Essential step!',
|
content: 'Wax the bottom of pieces before glazing to prevent them from sticking to kiln shelves. Essential step!',
|
||||||
category: 'Glazing',
|
category: 'Glazing',
|
||||||
icon: '🕯️',
|
icon: 'candle',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '28',
|
id: '28',
|
||||||
title: 'Stir Your Glaze',
|
title: 'Stir Your Glaze',
|
||||||
content: 'Always stir glazes thoroughly before use! Glaze settles and consistency directly affects color and texture.',
|
content: 'Always stir glazes thoroughly before use! Glaze settles and consistency directly affects color and texture.',
|
||||||
category: 'Glazing',
|
category: 'Glazing',
|
||||||
icon: '🥄',
|
icon: 'spoon-sugar',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '29',
|
id: '29',
|
||||||
title: 'Clay Storage',
|
title: 'Clay Storage',
|
||||||
content: 'Store clay in airtight containers or wrap tightly in plastic. Add a damp towel for extra moisture retention.',
|
content: 'Store clay in airtight containers or wrap tightly in plastic. Add a damp towel for extra moisture retention.',
|
||||||
category: 'Best Practice',
|
category: 'Best Practice',
|
||||||
icon: '📦',
|
icon: 'package-variant-closed',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '30',
|
id: '30',
|
||||||
title: 'Clean Water',
|
title: 'Clean Water',
|
||||||
content: 'Change throwing water frequently. Dirty water contaminates your clay with sediment and weakens it.',
|
content: 'Change throwing water frequently. Dirty water contaminates your clay with sediment and weakens it.',
|
||||||
category: 'Best Practice',
|
category: 'Best Practice',
|
||||||
icon: '💧',
|
icon: 'water-check',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '31',
|
id: '31',
|
||||||
title: 'Handle Attachment',
|
title: 'Handle Attachment',
|
||||||
content: 'Attach handles to leather-hard pieces. Score surfaces, apply slip, and compress joints firmly for strength.',
|
content: 'Attach handles to leather-hard pieces. Score surfaces, apply slip, and compress joints firmly for strength.',
|
||||||
category: 'Forming',
|
category: 'Forming',
|
||||||
icon: '🤝',
|
icon: 'handshake',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '32',
|
id: '32',
|
||||||
title: 'Warping Prevention',
|
title: 'Warping Prevention',
|
||||||
content: 'Dry thick and thin sections at the same rate by covering thinner areas with plastic. Prevents warping!',
|
content: 'Dry thick and thin sections at the same rate by covering thinner areas with plastic. Prevents warping!',
|
||||||
category: 'Drying',
|
category: 'Drying',
|
||||||
icon: '🛡️',
|
icon: 'shield-outline',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const categoryColors: Record<string, string> = {
|
||||||
|
'Basics': colors.textSecondary,
|
||||||
|
'Drying': colors.drying,
|
||||||
|
'Firing': colors.glazeFiring,
|
||||||
|
'Glazing': colors.glazing,
|
||||||
|
'Forming': colors.forming,
|
||||||
|
'Trimming': colors.trimming,
|
||||||
|
'Recycling': colors.success,
|
||||||
|
'Tools': colors.accentDark,
|
||||||
|
'Surface': colors.glazing,
|
||||||
|
'Safety': colors.error,
|
||||||
|
'Best Practice': colors.primary,
|
||||||
|
};
|
||||||
|
|
||||||
export const NewsScreen: React.FC = () => {
|
export const NewsScreen: React.FC = () => {
|
||||||
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
|
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
|
||||||
|
|
||||||
|
|
@ -288,10 +305,12 @@ export const NewsScreen: React.FC = () => {
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<ScrollView style={styles.content} contentContainerStyle={styles.listContent}>
|
<ScrollView style={styles.content} contentContainerStyle={styles.listContent}>
|
||||||
{filteredTips.map((tip) => (
|
{filteredTips.map((tip) => {
|
||||||
|
const iconColor = categoryColors[tip.category] || colors.primary;
|
||||||
|
return (
|
||||||
<Card key={tip.id} style={styles.tipCard}>
|
<Card key={tip.id} style={styles.tipCard}>
|
||||||
<View style={styles.tipHeader}>
|
<View style={styles.tipHeader}>
|
||||||
<Text style={styles.tipIcon}>{tip.icon}</Text>
|
<MaterialCommunityIcons name={tip.icon} size={32} color={iconColor} />
|
||||||
<View style={styles.tipHeaderText}>
|
<View style={styles.tipHeaderText}>
|
||||||
<Text style={styles.tipTitle}>{tip.title}</Text>
|
<Text style={styles.tipTitle}>{tip.title}</Text>
|
||||||
<Text style={styles.tipCategory}>{tip.category}</Text>
|
<Text style={styles.tipCategory}>{tip.category}</Text>
|
||||||
|
|
@ -299,7 +318,8 @@ export const NewsScreen: React.FC = () => {
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.tipContent}>{tip.content}</Text>
|
<Text style={styles.tipContent}>{tip.content}</Text>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
|
|
@ -374,9 +394,6 @@ const styles = StyleSheet.create({
|
||||||
marginBottom: spacing.md,
|
marginBottom: spacing.md,
|
||||||
gap: spacing.md,
|
gap: spacing.md,
|
||||||
},
|
},
|
||||||
tipIcon: {
|
|
||||||
fontSize: 32,
|
|
||||||
},
|
|
||||||
tipHeaderText: {
|
tipHeaderText: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -46,8 +46,8 @@ export const OnboardingScreen: React.FC = () => {
|
||||||
|
|
||||||
const handleComplete = async () => {
|
const handleComplete = async () => {
|
||||||
// Save onboarding completion status
|
// Save onboarding completion status
|
||||||
|
// This will trigger the AppNavigator to switch to MainTabs automatically
|
||||||
await completeOnboarding();
|
await completeOnboarding();
|
||||||
navigation.replace('MainTabs', { screen: 'Projects' });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const slide = SLIDES[currentSlide];
|
const slide = SLIDES[currentSlide];
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import {
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
|
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
|
||||||
import * as ImagePicker from 'expo-image-picker';
|
import * as ImagePicker from 'expo-image-picker';
|
||||||
|
import { MaterialCommunityIcons } from '@expo/vector-icons'; // NEW
|
||||||
import { useNavigation, useRoute, RouteProp, useFocusEffect } from '@react-navigation/native';
|
import { useNavigation, useRoute, RouteProp, useFocusEffect } from '@react-navigation/native';
|
||||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||||
import { RootStackParamList } from '../navigation/types';
|
import { RootStackParamList } from '../navigation/types';
|
||||||
|
|
@ -22,7 +23,7 @@ import {
|
||||||
getStepsByProject,
|
getStepsByProject,
|
||||||
} from '../lib/db/repositories';
|
} from '../lib/db/repositories';
|
||||||
import { Button, Input, Card } from '../components';
|
import { Button, Input, Card } from '../components';
|
||||||
import { colors, spacing, typography, stepTypeIcons, stepTypeLabels, borderRadius } from '../lib/theme';
|
import { colors, spacing, typography, stepTypeIcons, stepTypeLabels, borderRadius, stepTypeColors, shadows } from '../lib/theme';
|
||||||
import { formatDate } from '../lib/utils/datetime';
|
import { formatDate } from '../lib/utils/datetime';
|
||||||
import { sortStepsByLogicalOrder, suggestNextStep } from '../lib/utils/stepOrdering';
|
import { sortStepsByLogicalOrder, suggestNextStep } from '../lib/utils/stepOrdering';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
|
|
@ -57,9 +58,15 @@ export const ProjectDetailScreen: React.FC = () => {
|
||||||
console.log('ProjectDetail focused - reloading steps');
|
console.log('ProjectDetail focused - reloading steps');
|
||||||
loadSteps();
|
loadSteps();
|
||||||
}
|
}
|
||||||
}, [route.params.projectId, isNew])
|
}, [isNew])
|
||||||
);
|
);
|
||||||
|
|
||||||
|
React.useLayoutEffect(() => {
|
||||||
|
navigation.setOptions({
|
||||||
|
headerShown: false,
|
||||||
|
});
|
||||||
|
}, [navigation]);
|
||||||
|
|
||||||
const loadSteps = async () => {
|
const loadSteps = async () => {
|
||||||
try {
|
try {
|
||||||
const projectSteps = await getStepsByProject(route.params.projectId);
|
const projectSteps = await getStepsByProject(route.params.projectId);
|
||||||
|
|
@ -216,6 +223,14 @@ export const ProjectDetailScreen: React.FC = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.safeArea} edges={['top']}>
|
<SafeAreaView style={styles.safeArea} edges={['top']}>
|
||||||
|
<View style={styles.header}>
|
||||||
|
<TouchableOpacity style={styles.backButton} onPress={() => navigation.goBack()}>
|
||||||
|
<MaterialCommunityIcons name="chevron-left" size={24} color={colors.text} />
|
||||||
|
<Text style={styles.backButtonText}>Back</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<Text style={styles.headerTitle}>Project Overview</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
<KeyboardAwareScrollView
|
<KeyboardAwareScrollView
|
||||||
style={styles.container}
|
style={styles.container}
|
||||||
contentContainerStyle={styles.content}
|
contentContainerStyle={styles.content}
|
||||||
|
|
@ -231,7 +246,7 @@ export const ProjectDetailScreen: React.FC = () => {
|
||||||
<Image source={{ uri: coverImage }} style={styles.coverImagePreview} />
|
<Image source={{ uri: coverImage }} style={styles.coverImagePreview} />
|
||||||
) : (
|
) : (
|
||||||
<View style={styles.imagePlaceholder}>
|
<View style={styles.imagePlaceholder}>
|
||||||
<Text style={styles.imagePlaceholderText}>📷</Text>
|
<MaterialCommunityIcons name="camera-plus-outline" size={48} color={colors.textSecondary} />
|
||||||
<Text style={styles.imagePlaceholderLabel}>Add Cover Photo</Text>
|
<Text style={styles.imagePlaceholderLabel}>Add Cover Photo</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
@ -258,6 +273,7 @@ export const ProjectDetailScreen: React.FC = () => {
|
||||||
styles.statusButton,
|
styles.statusButton,
|
||||||
styles.statusButtonInProgress,
|
styles.statusButtonInProgress,
|
||||||
status === 'in_progress' && styles.statusButtonActive,
|
status === 'in_progress' && styles.statusButtonActive,
|
||||||
|
status === 'in_progress' && styles.statusButtonInProgressActive,
|
||||||
]}
|
]}
|
||||||
onPress={() => setStatus('in_progress')}
|
onPress={() => setStatus('in_progress')}
|
||||||
>
|
>
|
||||||
|
|
@ -273,6 +289,7 @@ export const ProjectDetailScreen: React.FC = () => {
|
||||||
styles.statusButton,
|
styles.statusButton,
|
||||||
styles.statusButtonDone,
|
styles.statusButtonDone,
|
||||||
status === 'done' && styles.statusButtonActive,
|
status === 'done' && styles.statusButtonActive,
|
||||||
|
status === 'done' && styles.statusButtonDoneActive,
|
||||||
]}
|
]}
|
||||||
onPress={() => setStatus('done')}
|
onPress={() => setStatus('done')}
|
||||||
>
|
>
|
||||||
|
|
@ -289,6 +306,7 @@ export const ProjectDetailScreen: React.FC = () => {
|
||||||
title={isNew ? 'Create Project' : 'Save Changes'}
|
title={isNew ? 'Create Project' : 'Save Changes'}
|
||||||
onPress={handleSave}
|
onPress={handleSave}
|
||||||
loading={saving}
|
loading={saving}
|
||||||
|
style={styles.sageButton}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|
@ -298,24 +316,34 @@ export const ProjectDetailScreen: React.FC = () => {
|
||||||
<Text style={styles.sectionTitle}>Process Steps</Text>
|
<Text style={styles.sectionTitle}>Process Steps</Text>
|
||||||
<Button
|
<Button
|
||||||
title="Add Step"
|
title="Add Step"
|
||||||
|
icon={<MaterialCommunityIcons name="plus" size={20} color={colors.buttonText} />}
|
||||||
onPress={handleAddStep}
|
onPress={handleAddStep}
|
||||||
size="sm"
|
size="sm"
|
||||||
|
style={styles.sageButton}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{steps.length > 0 && status === 'in_progress' && (() => {
|
{steps.length > 0 && status === 'in_progress' && (() => {
|
||||||
const nextStep = suggestNextStep(steps);
|
const nextStep = suggestNextStep(steps);
|
||||||
return nextStep ? (
|
return nextStep ? (
|
||||||
<Card style={styles.suggestionCard}>
|
<TouchableOpacity
|
||||||
|
activeOpacity={0.8}
|
||||||
|
onPress={() => navigation.navigate('StepEditor', {
|
||||||
|
projectId: route.params.projectId,
|
||||||
|
initialStepType: nextStep
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Card style={[styles.suggestionCard, { backgroundColor: '#F0F4E8' }]}>
|
||||||
<View style={styles.suggestionHeader}>
|
<View style={styles.suggestionHeader}>
|
||||||
<Text style={styles.suggestionIcon}>💡</Text>
|
<MaterialCommunityIcons name="lightbulb-on-outline" size={20} color={colors.primaryDark} />
|
||||||
<Text style={styles.suggestionTitle}>Suggested Next Step</Text>
|
<Text style={[styles.suggestionTitle, { color: colors.primaryDark }]}>Suggested Next Step</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.suggestionContent}>
|
<View style={styles.suggestionContent}>
|
||||||
<Text style={styles.suggestionStepIcon}>{stepTypeIcons[nextStep]}</Text>
|
<MaterialCommunityIcons name={stepTypeIcons[nextStep] as any} size={24} color={stepTypeColors[nextStep]} />
|
||||||
<Text style={styles.suggestionStepLabel}>{stepTypeLabels[nextStep]}</Text>
|
<Text style={[styles.suggestionStepLabel, { color: colors.text }]}>{stepTypeLabels[nextStep]}</Text>
|
||||||
</View>
|
</View>
|
||||||
</Card>
|
</Card>
|
||||||
|
</TouchableOpacity>
|
||||||
) : null;
|
) : null;
|
||||||
})()}
|
})()}
|
||||||
|
|
||||||
|
|
@ -330,7 +358,7 @@ export const ProjectDetailScreen: React.FC = () => {
|
||||||
{/* Timeline dot and line */}
|
{/* Timeline dot and line */}
|
||||||
<View style={styles.timelineLeft}>
|
<View style={styles.timelineLeft}>
|
||||||
<View style={styles.timelineDot}>
|
<View style={styles.timelineDot}>
|
||||||
<Text style={styles.timelineDotIcon}>{stepTypeIcons[step.type]}</Text>
|
<MaterialCommunityIcons name={stepTypeIcons[step.type] as any} size={20} color={colors.background} />
|
||||||
</View>
|
</View>
|
||||||
{index < steps.length - 1 && <View style={styles.timelineLine} />}
|
{index < steps.length - 1 && <View style={styles.timelineLine} />}
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -367,7 +395,8 @@ export const ProjectDetailScreen: React.FC = () => {
|
||||||
title="Delete Project"
|
title="Delete Project"
|
||||||
onPress={handleDelete}
|
onPress={handleDelete}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
style={styles.deleteButton}
|
style={styles.terracottaButton}
|
||||||
|
textStyle={styles.terracottaText}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
@ -379,7 +408,40 @@ export const ProjectDetailScreen: React.FC = () => {
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
safeArea: {
|
safeArea: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: colors.background,
|
backgroundColor: colors.backgroundSecondary,
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingHorizontal: spacing.md,
|
||||||
|
paddingVertical: spacing.md,
|
||||||
|
backgroundColor: colors.backgroundSecondary,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: colors.borderLight,
|
||||||
|
},
|
||||||
|
headerTitle: {
|
||||||
|
flex: 1,
|
||||||
|
textAlign: 'center',
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: '800',
|
||||||
|
color: colors.text,
|
||||||
|
},
|
||||||
|
backButton: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: colors.card,
|
||||||
|
borderRadius: borderRadius.full,
|
||||||
|
paddingVertical: spacing.xs,
|
||||||
|
paddingHorizontal: spacing.sm,
|
||||||
|
paddingRight: spacing.md,
|
||||||
|
...shadows.sm,
|
||||||
|
zIndex: 10, // Ensure it sits above the absolute title
|
||||||
|
},
|
||||||
|
backButtonText: {
|
||||||
|
fontSize: typography.fontSize.md,
|
||||||
|
fontWeight: typography.fontWeight.medium,
|
||||||
|
color: colors.text,
|
||||||
|
marginLeft: -4,
|
||||||
},
|
},
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
|
@ -545,9 +607,15 @@ const styles = StyleSheet.create({
|
||||||
statusButtonInProgress: {
|
statusButtonInProgress: {
|
||||||
borderColor: colors.primary,
|
borderColor: colors.primary,
|
||||||
},
|
},
|
||||||
|
statusButtonInProgressActive: {
|
||||||
|
backgroundColor: '#E8E9E4', // Light sage tint
|
||||||
|
},
|
||||||
statusButtonDone: {
|
statusButtonDone: {
|
||||||
borderColor: colors.success,
|
borderColor: colors.success,
|
||||||
},
|
},
|
||||||
|
statusButtonDoneActive: {
|
||||||
|
backgroundColor: '#D4E8C8', // Light green tint
|
||||||
|
},
|
||||||
statusButtonArchived: {
|
statusButtonArchived: {
|
||||||
borderColor: colors.textTertiary,
|
borderColor: colors.textTertiary,
|
||||||
},
|
},
|
||||||
|
|
@ -598,4 +666,15 @@ const styles = StyleSheet.create({
|
||||||
fontWeight: typography.fontWeight.bold,
|
fontWeight: typography.fontWeight.bold,
|
||||||
color: colors.text,
|
color: colors.text,
|
||||||
},
|
},
|
||||||
|
sageButton: {
|
||||||
|
backgroundColor: '#8B9A7C',
|
||||||
|
borderColor: '#7A896C',
|
||||||
|
},
|
||||||
|
terracottaButton: {
|
||||||
|
borderColor: '#C5675C',
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
},
|
||||||
|
terracottaText: {
|
||||||
|
color: '#C5675C',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -144,6 +144,7 @@ export const ProjectsScreen: React.FC = () => {
|
||||||
description="Start your first piece!"
|
description="Start your first piece!"
|
||||||
actionLabel="New Project"
|
actionLabel="New Project"
|
||||||
onAction={handleCreateProject}
|
onAction={handleCreateProject}
|
||||||
|
buttonStyle={styles.sageButton}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<FlatList
|
<FlatList
|
||||||
|
|
@ -169,6 +170,7 @@ export const ProjectsScreen: React.FC = () => {
|
||||||
title="New Project"
|
title="New Project"
|
||||||
onPress={handleCreateProject}
|
onPress={handleCreateProject}
|
||||||
size="md"
|
size="md"
|
||||||
|
style={styles.sageButton}
|
||||||
accessibilityLabel="Create new project"
|
accessibilityLabel="Create new project"
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -327,5 +329,15 @@ const styles = StyleSheet.create({
|
||||||
gap: spacing.md,
|
gap: spacing.md,
|
||||||
paddingHorizontal: spacing.lg,
|
paddingHorizontal: spacing.lg,
|
||||||
},
|
},
|
||||||
|
sageButton: {
|
||||||
|
backgroundColor: '#8B9A7C',
|
||||||
|
borderColor: '#7A896C',
|
||||||
|
// Floating effect
|
||||||
|
shadowColor: '#000000',
|
||||||
|
shadowOffset: { width: 0, height: 4 },
|
||||||
|
shadowOpacity: 0.3,
|
||||||
|
shadowRadius: 8,
|
||||||
|
elevation: 8,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { View, Text, StyleSheet, ScrollView, Switch, Alert, Share } from 'react-native';
|
import { View, Text, StyleSheet, ScrollView, Switch, Alert, Share, TouchableOpacity } from 'react-native';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||||
|
|
@ -7,7 +7,7 @@ import { RootStackParamList } from '../navigation/types';
|
||||||
import { getSettings, updateSettings, getAllProjects, getStepsByProject, getListItems } from '../lib/db/repositories';
|
import { getSettings, updateSettings, getAllProjects, getStepsByProject, getListItems } from '../lib/db/repositories';
|
||||||
import { Settings } from '../types';
|
import { Settings } from '../types';
|
||||||
import { Card, Button } from '../components';
|
import { Card, Button } from '../components';
|
||||||
import { colors, spacing, typography } from '../lib/theme';
|
import { colors, spacing, typography, borderRadius } from '../lib/theme';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
|
|
||||||
type NavigationProp = NativeStackNavigationProp<RootStackParamList>;
|
type NavigationProp = NativeStackNavigationProp<RootStackParamList>;
|
||||||
|
|
@ -18,6 +18,7 @@ export const SettingsScreen: React.FC = () => {
|
||||||
const [settings, setSettings] = useState<Settings>({
|
const [settings, setSettings] = useState<Settings>({
|
||||||
unitSystem: 'imperial',
|
unitSystem: 'imperial',
|
||||||
tempUnit: 'F',
|
tempUnit: 'F',
|
||||||
|
weightUnit: 'lb',
|
||||||
analyticsOptIn: false,
|
analyticsOptIn: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -120,59 +121,63 @@ export const SettingsScreen: React.FC = () => {
|
||||||
<Text style={styles.sectionTitle}>Units</Text>
|
<Text style={styles.sectionTitle}>Units</Text>
|
||||||
|
|
||||||
<View style={styles.setting}>
|
<View style={styles.setting}>
|
||||||
<View>
|
<View style={styles.settingHeader}>
|
||||||
<Text style={styles.settingLabel}>Temperature Unit</Text>
|
<Text style={styles.settingLabel}>Temperature Unit</Text>
|
||||||
<Text style={styles.settingValue}>
|
|
||||||
{settings.tempUnit === 'F' ? 'Fahrenheit (°F)' : 'Celsius (°C)'}
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
<Switch
|
<View style={styles.toggleContainer}>
|
||||||
value={settings.tempUnit === 'C'}
|
<TouchableOpacity
|
||||||
onValueChange={(value) => handleToggle('tempUnit', value ? 'C' : 'F')}
|
style={[styles.toggleButton, settings.tempUnit === 'F' && styles.toggleButtonActive]}
|
||||||
trackColor={{ false: colors.border, true: colors.primary }}
|
onPress={() => handleToggle('tempUnit', 'F')}
|
||||||
thumbColor={settings.tempUnit === 'C' ? colors.background : colors.background}
|
>
|
||||||
ios_backgroundColor={colors.border}
|
<Text style={[styles.toggleText, settings.tempUnit === 'F' && styles.toggleTextActive]}>°F</Text>
|
||||||
/>
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.toggleButton, settings.tempUnit === 'C' && styles.toggleButtonActive]}
|
||||||
|
onPress={() => handleToggle('tempUnit', 'C')}
|
||||||
|
>
|
||||||
|
<Text style={[styles.toggleText, settings.tempUnit === 'C' && styles.toggleTextActive]}>°C</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.setting}>
|
<View style={styles.setting}>
|
||||||
<View>
|
<View style={styles.settingHeader}>
|
||||||
<Text style={styles.settingLabel}>Unit System</Text>
|
<Text style={styles.settingLabel}>Unit System</Text>
|
||||||
<Text style={styles.settingValue}>
|
|
||||||
{settings.unitSystem === 'imperial' ? 'Imperial (lb/in)' : 'Metric (kg/cm)'}
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
<Switch
|
<View style={styles.toggleContainer}>
|
||||||
value={settings.unitSystem === 'metric'}
|
<TouchableOpacity
|
||||||
onValueChange={(value) =>
|
style={[styles.toggleButton, settings.unitSystem === 'imperial' && styles.toggleButtonActive]}
|
||||||
handleToggle('unitSystem', value ? 'metric' : 'imperial')
|
onPress={() => handleToggle('unitSystem', 'imperial')}
|
||||||
}
|
>
|
||||||
trackColor={{ false: colors.border, true: colors.primary }}
|
<Text style={[styles.toggleText, settings.unitSystem === 'imperial' && styles.toggleTextActive]}>Imperial</Text>
|
||||||
thumbColor={settings.unitSystem === 'metric' ? colors.background : colors.background}
|
</TouchableOpacity>
|
||||||
ios_backgroundColor={colors.border}
|
<TouchableOpacity
|
||||||
/>
|
style={[styles.toggleButton, settings.unitSystem === 'metric' && styles.toggleButtonActive]}
|
||||||
|
onPress={() => handleToggle('unitSystem', 'metric')}
|
||||||
|
>
|
||||||
|
<Text style={[styles.toggleText, settings.unitSystem === 'metric' && styles.toggleTextActive]}>Metric</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={[styles.setting, styles.noBorder]}>
|
<View style={[styles.setting, styles.noBorder]}>
|
||||||
<View>
|
<View style={styles.settingHeader}>
|
||||||
<Text style={styles.settingLabel}>Weight Unit</Text>
|
<Text style={styles.settingLabel}>Weight Unit</Text>
|
||||||
<Text style={styles.settingValue}>
|
|
||||||
{settings.weightUnit === 'lb' ? 'Pounds (lb)' :
|
|
||||||
settings.weightUnit === 'oz' ? 'Ounces (oz)' :
|
|
||||||
settings.weightUnit === 'kg' ? 'Kilograms (kg)' : 'Grams (g)'}
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
<Switch
|
<View style={styles.toggleContainer}>
|
||||||
value={settings.weightUnit === 'kg' || settings.weightUnit === 'g'}
|
<TouchableOpacity
|
||||||
onValueChange={(value) => {
|
style={[styles.toggleButton, (settings.weightUnit === 'lb' || settings.weightUnit === 'oz') && styles.toggleButtonActive]}
|
||||||
// Toggle between imperial (lb) and metric (kg)
|
onPress={() => handleToggle('weightUnit', 'lb')}
|
||||||
const newUnit = value ? 'kg' : 'lb';
|
>
|
||||||
handleToggle('weightUnit', newUnit);
|
<Text style={[styles.toggleText, (settings.weightUnit === 'lb' || settings.weightUnit === 'oz') && styles.toggleTextActive]}>lb</Text>
|
||||||
}}
|
</TouchableOpacity>
|
||||||
trackColor={{ false: colors.border, true: colors.primary }}
|
<TouchableOpacity
|
||||||
thumbColor={(settings.weightUnit === 'kg' || settings.weightUnit === 'g') ? colors.background : colors.background}
|
style={[styles.toggleButton, (settings.weightUnit === 'kg' || settings.weightUnit === 'g') && styles.toggleButtonActive]}
|
||||||
ios_backgroundColor={colors.border}
|
onPress={() => handleToggle('weightUnit', 'kg')}
|
||||||
/>
|
>
|
||||||
|
<Text style={[styles.toggleText, (settings.weightUnit === 'kg' || settings.weightUnit === 'g') && styles.toggleTextActive]}>kg</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|
@ -283,9 +288,8 @@ const styles = StyleSheet.create({
|
||||||
letterSpacing: 0.5,
|
letterSpacing: 0.5,
|
||||||
},
|
},
|
||||||
setting: {
|
setting: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'column',
|
||||||
justifyContent: 'space-between',
|
alignItems: 'flex-start',
|
||||||
alignItems: 'center',
|
|
||||||
paddingVertical: spacing.md,
|
paddingVertical: spacing.md,
|
||||||
borderBottomWidth: 2,
|
borderBottomWidth: 2,
|
||||||
borderBottomColor: colors.border,
|
borderBottomColor: colors.border,
|
||||||
|
|
@ -297,9 +301,16 @@ const styles = StyleSheet.create({
|
||||||
},
|
},
|
||||||
settingValue: {
|
settingValue: {
|
||||||
fontSize: typography.fontSize.sm,
|
fontSize: typography.fontSize.sm,
|
||||||
color: colors.textSecondary,
|
|
||||||
marginTop: spacing.xs,
|
marginTop: spacing.xs,
|
||||||
},
|
},
|
||||||
|
settingHeader: {
|
||||||
|
marginBottom: spacing.sm,
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
settingInfo: {
|
||||||
|
flex: 1,
|
||||||
|
paddingRight: spacing.sm,
|
||||||
|
},
|
||||||
infoRow: {
|
infoRow: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
|
|
@ -341,6 +352,35 @@ const styles = StyleSheet.create({
|
||||||
listButton: {
|
listButton: {
|
||||||
marginTop: spacing.sm,
|
marginTop: spacing.sm,
|
||||||
},
|
},
|
||||||
|
toggleContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
backgroundColor: colors.background,
|
||||||
|
borderRadius: borderRadius.md,
|
||||||
|
padding: 0,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.border,
|
||||||
|
width: '100%',
|
||||||
|
overflow: 'hidden',
|
||||||
|
},
|
||||||
|
toggleButton: {
|
||||||
|
flex: 1,
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingHorizontal: spacing.sm,
|
||||||
|
paddingVertical: spacing.sm,
|
||||||
|
borderRadius: borderRadius.md,
|
||||||
|
},
|
||||||
|
toggleButtonActive: {
|
||||||
|
backgroundColor: colors.primary,
|
||||||
|
},
|
||||||
|
toggleText: {
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
fontWeight: typography.fontWeight.medium,
|
||||||
|
},
|
||||||
|
toggleTextActive: {
|
||||||
|
color: colors.buttonText,
|
||||||
|
fontWeight: typography.fontWeight.bold,
|
||||||
|
},
|
||||||
noBorder: {
|
noBorder: {
|
||||||
borderBottomWidth: 0,
|
borderBottomWidth: 0,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
Platform,
|
Platform,
|
||||||
Alert,
|
Alert,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
|
Image,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
|
|
@ -82,40 +83,15 @@ export const SignUpScreen: React.FC = () => {
|
||||||
|
|
||||||
{/* Hero Image */}
|
{/* Hero Image */}
|
||||||
<View style={styles.heroContainer}>
|
<View style={styles.heroContainer}>
|
||||||
<View style={styles.heroImagePlaceholder}>
|
<Image
|
||||||
<Text style={styles.heroEmoji}>🏺</Text>
|
source={require('../../assets/images/app_logo.png')}
|
||||||
</View>
|
style={styles.heroImage}
|
||||||
|
resizeMode="contain"
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Form */}
|
{/* Form */}
|
||||||
<View style={styles.form}>
|
<View style={styles.form}>
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.primaryButton}
|
|
||||||
onPress={handleSignUp}
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
<Text style={styles.primaryButtonText}>
|
|
||||||
{loading ? 'Creating Account...' : 'Create Account'}
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
{/* Divider */}
|
|
||||||
<View style={styles.divider}>
|
|
||||||
<View style={styles.dividerLine} />
|
|
||||||
<Text style={styles.dividerText}>OR</Text>
|
|
||||||
<View style={styles.dividerLine} />
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Social Buttons */}
|
|
||||||
<View style={styles.socialButtons}>
|
|
||||||
<TouchableOpacity style={styles.socialButton}>
|
|
||||||
<Text style={styles.socialButtonText}>G</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<TouchableOpacity style={styles.socialButton}>
|
|
||||||
<Text style={styles.socialButtonText}>🍎</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Input Fields */}
|
{/* Input Fields */}
|
||||||
<Input
|
<Input
|
||||||
label="Name"
|
label="Name"
|
||||||
|
|
@ -156,8 +132,19 @@ export const SignUpScreen: React.FC = () => {
|
||||||
autoComplete="password"
|
autoComplete="password"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Create Account Button */}
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.primaryButton, { marginTop: spacing.xl }]}
|
||||||
|
onPress={handleSignUp}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<Text style={styles.primaryButtonText}>
|
||||||
|
{loading ? 'Creating Account...' : 'Create Account'}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
{/* Login Link */}
|
{/* Login Link */}
|
||||||
<View style={styles.loginContainer}>
|
<View style={[styles.loginContainer, { marginTop: spacing.md }]}>
|
||||||
<Text style={styles.loginText}>Already have an account? </Text>
|
<Text style={styles.loginText}>Already have an account? </Text>
|
||||||
<TouchableOpacity onPress={() => navigation.navigate('Login')}>
|
<TouchableOpacity onPress={() => navigation.navigate('Login')}>
|
||||||
<Text style={styles.loginLink}>Log In</Text>
|
<Text style={styles.loginLink}>Log In</Text>
|
||||||
|
|
@ -203,16 +190,10 @@ const styles = StyleSheet.create({
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginVertical: spacing.xl,
|
marginVertical: spacing.xl,
|
||||||
},
|
},
|
||||||
heroImagePlaceholder: {
|
heroImage: {
|
||||||
width: 180,
|
width: 180,
|
||||||
height: 180,
|
height: 180,
|
||||||
borderRadius: borderRadius.lg,
|
borderRadius: borderRadius.lg,
|
||||||
backgroundColor: colors.primaryLight,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
heroEmoji: {
|
|
||||||
fontSize: 90,
|
|
||||||
},
|
},
|
||||||
form: {
|
form: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { View, Text, StyleSheet, Alert, Image, TouchableOpacity, FlatList, TextInput, findNodeHandle } from 'react-native';
|
import { View, Text, StyleSheet, Alert, Image, TouchableOpacity, TextInput } from 'react-native';
|
||||||
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
|
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
|
||||||
import * as ImagePicker from 'expo-image-picker';
|
import * as ImagePicker from 'expo-image-picker';
|
||||||
import { useNavigation, useRoute, RouteProp, CommonActions } from '@react-navigation/native';
|
import { useNavigation, useRoute, RouteProp, CommonActions } from '@react-navigation/native';
|
||||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { RootStackParamList } from '../navigation/types';
|
import { RootStackParamList } from '../navigation/types';
|
||||||
import {
|
import {
|
||||||
StepType,
|
StepType,
|
||||||
|
|
@ -15,26 +16,26 @@ import {
|
||||||
KilnType,
|
KilnType,
|
||||||
KilnPosition,
|
KilnPosition,
|
||||||
GlazeLayer,
|
GlazeLayer,
|
||||||
GlazePosition,
|
|
||||||
ApplicationMethod
|
ApplicationMethod
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { createStep, getStep, updateStep, deleteStep, getSettings } from '../lib/db/repositories';
|
import { createStep, getStep, updateStep, deleteStep, getSettings } from '../lib/db/repositories';
|
||||||
import { getConeTemperature, getBisqueCones, getGlazeFiringCones } from '../lib/utils';
|
import { getConeTemperature } from '../lib/utils';
|
||||||
import { Button, Input, Card, Picker, WeightInput, ButtonGrid, DimensionsInput, GlazeLayerCard } from '../components';
|
import { Button, Input, Card, WeightInput, ButtonGrid, DimensionsInput, GlazeLayerCard } from '../components';
|
||||||
import { colors, spacing, typography, borderRadius } from '../lib/theme';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
|
import { colors, spacing, typography, borderRadius, stepTypeIcons, stepTypeColors, shadows } from '../lib/theme';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
|
|
||||||
type NavigationProp = NativeStackNavigationProp<RootStackParamList, 'StepEditor'>;
|
type NavigationProp = NativeStackNavigationProp<RootStackParamList, 'StepEditor'>;
|
||||||
type RouteProps = RouteProp<RootStackParamList, 'StepEditor'>;
|
type RouteProps = RouteProp<RootStackParamList, 'StepEditor'>;
|
||||||
|
|
||||||
const STEP_TYPES: { value: StepType; label: string }[] = [
|
const STEP_TYPES: { value: StepType; label: string }[] = [
|
||||||
{ value: 'forming', label: '🏺 Forming' },
|
{ value: 'forming', label: 'Forming' },
|
||||||
{ value: 'drying', label: '☀️ Drying' },
|
{ value: 'trimming', label: 'Trimming' },
|
||||||
{ value: 'bisque_firing', label: '🔥 Bisque Firing' },
|
{ value: 'drying', label: 'Drying' },
|
||||||
{ value: 'trimming', label: '🔧 Trimming' },
|
{ value: 'bisque_firing', label: 'Bisque Firing' },
|
||||||
{ value: 'glazing', label: '🎨 Glazing' },
|
{ value: 'glazing', label: 'Glazing' },
|
||||||
{ value: 'glaze_firing', label: '⚡ Glaze Firing' },
|
{ value: 'glaze_firing', label: 'Glaze Firing' },
|
||||||
{ value: 'misc', label: '📝 Misc' },
|
{ value: 'misc', label: 'Misc' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const StepEditorScreen: React.FC = () => {
|
export const StepEditorScreen: React.FC = () => {
|
||||||
|
|
@ -44,14 +45,15 @@ export const StepEditorScreen: React.FC = () => {
|
||||||
const { projectId, stepId } = route.params;
|
const { projectId, stepId } = route.params;
|
||||||
const isEditing = !!stepId;
|
const isEditing = !!stepId;
|
||||||
|
|
||||||
// Create unique editor key for this screen instance
|
|
||||||
const editorKey = React.useRef(route.params._editorKey || `editor-${Date.now()}`).current;
|
|
||||||
|
|
||||||
// Refs for scrolling to glaze layers section
|
// Refs for scrolling to glaze layers section
|
||||||
const scrollViewRef = useRef<KeyboardAwareScrollView>(null);
|
const scrollViewRef = useRef<KeyboardAwareScrollView>(null);
|
||||||
const glazeLayersSectionRef = useRef<View>(null);
|
const glazeLayersSectionRef = useRef<View>(null);
|
||||||
|
|
||||||
const [stepType, setStepType] = useState<StepType>('forming');
|
React.useLayoutEffect(() => {
|
||||||
|
navigation.setOptions({ headerShown: false });
|
||||||
|
}, [navigation]);
|
||||||
|
|
||||||
|
const [stepType, setStepType] = useState<StepType>(route.params.initialStepType || 'forming');
|
||||||
const [notes, setNotes] = useState('');
|
const [notes, setNotes] = useState('');
|
||||||
const [photos, setPhotos] = useState<string[]>([]);
|
const [photos, setPhotos] = useState<string[]>([]);
|
||||||
|
|
||||||
|
|
@ -66,7 +68,7 @@ export const StepEditorScreen: React.FC = () => {
|
||||||
const [productionMethod, setProductionMethod] = useState<ProductionMethod>('wheel');
|
const [productionMethod, setProductionMethod] = useState<ProductionMethod>('wheel');
|
||||||
const [dimensions, setDimensions] = useState<Dimensions | undefined>(undefined);
|
const [dimensions, setDimensions] = useState<Dimensions | undefined>(undefined);
|
||||||
|
|
||||||
// Firing fields (existing + new)
|
// Firing fields
|
||||||
const [cone, setCone] = useState('');
|
const [cone, setCone] = useState('');
|
||||||
const [temperature, setTemperature] = useState('');
|
const [temperature, setTemperature] = useState('');
|
||||||
const [duration, setDuration] = useState('');
|
const [duration, setDuration] = useState('');
|
||||||
|
|
@ -77,11 +79,9 @@ export const StepEditorScreen: React.FC = () => {
|
||||||
const [kilnPositionNotes, setKilnPositionNotes] = useState('');
|
const [kilnPositionNotes, setKilnPositionNotes] = useState('');
|
||||||
const [firingSchedule, setFiringSchedule] = useState('');
|
const [firingSchedule, setFiringSchedule] = useState('');
|
||||||
|
|
||||||
// Glazing fields (multi-layer)
|
// Glazing fields
|
||||||
const [glazeLayers, setGlazeLayers] = useState<GlazeLayer[]>([]);
|
const [glazeLayers, setGlazeLayers] = useState<GlazeLayer[]>([]);
|
||||||
const [customGlazeName, setCustomGlazeName] = useState('');
|
const [customGlazeName, setCustomGlazeName] = useState('');
|
||||||
|
|
||||||
// Legacy glazing fields (for backwards compatibility)
|
|
||||||
const [coats, setCoats] = useState('2');
|
const [coats, setCoats] = useState('2');
|
||||||
const [applicationMethod, setApplicationMethod] = useState<ApplicationMethod>('brush');
|
const [applicationMethod, setApplicationMethod] = useState<ApplicationMethod>('brush');
|
||||||
const [selectedGlazeIds, setSelectedGlazeIds] = useState<string[]>([]);
|
const [selectedGlazeIds, setSelectedGlazeIds] = useState<string[]>([]);
|
||||||
|
|
@ -90,9 +90,6 @@ export const StepEditorScreen: React.FC = () => {
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [loading, setLoading] = useState(isEditing);
|
const [loading, setLoading] = useState(isEditing);
|
||||||
|
|
||||||
// Track if we're adding glazes from catalog (new flow)
|
|
||||||
const [isAddingFromCatalog, setIsAddingFromCatalog] = useState(false);
|
|
||||||
|
|
||||||
// Load user's preferences
|
// Load user's preferences
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadUserPreferences();
|
loadUserPreferences();
|
||||||
|
|
@ -117,127 +114,15 @@ export const StepEditorScreen: React.FC = () => {
|
||||||
}, [stepId]);
|
}, [stepId]);
|
||||||
|
|
||||||
// Callback ref for receiving glaze selection from GlazePicker
|
// Callback ref for receiving glaze selection from GlazePicker
|
||||||
// This is a clean, direct approach without route params or useEffects
|
|
||||||
const glazeSelectionCallbackRef = useRef<((layers: GlazeLayer[]) => void) | null>(null);
|
const glazeSelectionCallbackRef = useRef<((layers: GlazeLayer[]) => void) | null>(null);
|
||||||
|
|
||||||
// TEMPORARILY DISABLED - Auto-save conflicts with new glaze layer flow
|
// Auto-save whenever any field changes
|
||||||
// This useEffect was causing immediate navigation.reset() back to ProjectDetail
|
|
||||||
// when adding glazes from catalog, preventing users from editing glaze layers.
|
|
||||||
// TODO: Re-enable with proper flow detection once new glaze flow is stable.
|
|
||||||
/*
|
|
||||||
// Also watch for route param changes directly and auto-save
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Skip auto-save if we're in the new add_layer flow
|
|
||||||
// Check both the flag AND if we just processed newGlazeLayers
|
|
||||||
if (isAddingFromCatalog || (route.params && 'newGlazeLayers' in route.params)) {
|
|
||||||
console.log('Skipping auto-save - user is adding from catalog or new layers present');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let hasChanges = false;
|
|
||||||
|
|
||||||
if (route.params && 'selectedGlazeIds' in route.params && route.params.selectedGlazeIds) {
|
|
||||||
console.log('Route params changed - selected glaze IDs:', route.params.selectedGlazeIds);
|
|
||||||
setSelectedGlazeIds(route.params.selectedGlazeIds);
|
|
||||||
|
|
||||||
// IMPORTANT: Automatically set stepType to 'glazing' when glazes are selected
|
|
||||||
if (stepType !== 'glazing') {
|
|
||||||
console.log('Auto-setting stepType to glazing');
|
|
||||||
setStepType('glazing');
|
|
||||||
}
|
|
||||||
|
|
||||||
hasChanges = true;
|
|
||||||
}
|
|
||||||
if (route.params && 'mixNotes' in route.params && route.params.mixNotes) {
|
|
||||||
console.log('Route params changed - mix notes:', route.params.mixNotes);
|
|
||||||
setMixNotes(route.params.mixNotes);
|
|
||||||
hasChanges = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-save after state updates
|
|
||||||
if (hasChanges) {
|
|
||||||
console.log('Auto-save triggered - stepType:', stepType, 'isEditing:', isEditing, 'stepId:', stepId);
|
|
||||||
|
|
||||||
// Use setTimeout to wait for state updates to complete
|
|
||||||
setTimeout(async () => {
|
|
||||||
try {
|
|
||||||
// Always use 'glazing' type when we have glaze IDs
|
|
||||||
const finalStepType = route.params?.selectedGlazeIds ? 'glazing' : stepType;
|
|
||||||
|
|
||||||
const stepData: any = {
|
|
||||||
projectId,
|
|
||||||
type: finalStepType,
|
|
||||||
notesMarkdown: notes,
|
|
||||||
photoUris: photos,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add glazing data if we have glaze selections
|
|
||||||
if (route.params?.selectedGlazeIds) {
|
|
||||||
stepData.glazing = {
|
|
||||||
glazeIds: route.params.selectedGlazeIds,
|
|
||||||
coats: coats ? parseInt(coats) : undefined,
|
|
||||||
application: applicationMethod,
|
|
||||||
mixNotes: route.params.mixNotes || undefined,
|
|
||||||
};
|
|
||||||
console.log('Glazing data prepared:', stepData.glazing);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isEditing && stepId) {
|
|
||||||
console.log('Updating existing step:', stepId);
|
|
||||||
await updateStep(stepId, stepData);
|
|
||||||
console.log('✅ Auto-saved step with glazes');
|
|
||||||
|
|
||||||
// Reset navigation to ProjectDetail, keeping MainTabs in stack
|
|
||||||
console.log('Resetting stack to MainTabs → ProjectDetail');
|
|
||||||
navigation.dispatch(
|
|
||||||
CommonActions.reset({
|
|
||||||
index: 1, // ProjectDetail is the active screen
|
|
||||||
routes: [
|
|
||||||
{ name: 'MainTabs' }, // Keep MainTabs in stack for back button
|
|
||||||
{ name: 'ProjectDetail', params: { projectId } }, // ProjectDetail is active
|
|
||||||
],
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else if (!isEditing) {
|
|
||||||
// If creating a new step, create it first
|
|
||||||
console.log('Creating new step with type:', finalStepType);
|
|
||||||
const newStep = await createStep(stepData);
|
|
||||||
if (newStep) {
|
|
||||||
console.log('✅ Auto-created step with glazes, new stepId:', newStep.id);
|
|
||||||
|
|
||||||
// Reset navigation to ProjectDetail, keeping MainTabs in stack
|
|
||||||
console.log('Resetting stack to MainTabs → ProjectDetail');
|
|
||||||
navigation.dispatch(
|
|
||||||
CommonActions.reset({
|
|
||||||
index: 1, // ProjectDetail is the active screen
|
|
||||||
routes: [
|
|
||||||
{ name: 'MainTabs' }, // Keep MainTabs in stack for back button
|
|
||||||
{ name: 'ProjectDetail', params: { projectId } }, // ProjectDetail is active
|
|
||||||
],
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Auto-save failed:', error);
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
}, [route.params?.selectedGlazeIds, route.params?.mixNotes]);
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Auto-save whenever any field changes (but only for existing steps)
|
|
||||||
useEffect(() => {
|
|
||||||
// Don't auto-save on initial load or if we're still loading
|
|
||||||
if (loading) return;
|
if (loading) return;
|
||||||
|
|
||||||
// Only auto-save for existing steps that have been edited
|
|
||||||
if (isEditing && stepId) {
|
if (isEditing && stepId) {
|
||||||
console.log('Field changed - triggering auto-save');
|
|
||||||
|
|
||||||
// Debounce the save by 1 second to avoid saving on every keystroke
|
|
||||||
const timeoutId = setTimeout(async () => {
|
const timeoutId = setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
|
// Prepare data...
|
||||||
const stepData: any = {
|
const stepData: any = {
|
||||||
projectId,
|
projectId,
|
||||||
type: stepType,
|
type: stepType,
|
||||||
|
|
@ -279,39 +164,15 @@ export const StepEditorScreen: React.FC = () => {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Auto-save error:', error);
|
console.error('Auto-save error:', error);
|
||||||
}
|
}
|
||||||
}, 1000); // Wait 1 second after last change
|
}, 1000);
|
||||||
|
|
||||||
return () => clearTimeout(timeoutId);
|
return () => clearTimeout(timeoutId);
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
stepType,
|
stepType, notes, photos,
|
||||||
notes,
|
clayBody, clayWeight, productionMethod, dimensions,
|
||||||
photos,
|
cone, temperature, duration, kilnNotes, kilnType, kilnCustomName, kilnPosition, kilnPositionNotes, firingSchedule,
|
||||||
// Forming fields
|
glazeLayers, coats, applicationMethod, selectedGlazeIds, mixNotes,
|
||||||
clayBody,
|
isEditing, stepId, loading, projectId, tempUnit
|
||||||
clayWeight,
|
|
||||||
productionMethod,
|
|
||||||
dimensions,
|
|
||||||
// Firing fields
|
|
||||||
cone,
|
|
||||||
temperature,
|
|
||||||
duration,
|
|
||||||
kilnNotes,
|
|
||||||
kilnType,
|
|
||||||
kilnCustomName,
|
|
||||||
kilnPosition,
|
|
||||||
kilnPositionNotes,
|
|
||||||
firingSchedule,
|
|
||||||
// Glazing fields
|
|
||||||
glazeLayers,
|
|
||||||
coats,
|
|
||||||
applicationMethod,
|
|
||||||
selectedGlazeIds,
|
|
||||||
mixNotes,
|
|
||||||
// Meta
|
|
||||||
isEditing,
|
|
||||||
stepId,
|
|
||||||
loading,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const loadStep = async () => {
|
const loadStep = async () => {
|
||||||
|
|
@ -322,7 +183,6 @@ export const StepEditorScreen: React.FC = () => {
|
||||||
navigation.goBack();
|
navigation.goBack();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setStepType(step.type);
|
setStepType(step.type);
|
||||||
setNotes(step.notesMarkdown || '');
|
setNotes(step.notesMarkdown || '');
|
||||||
setPhotos(step.photoUris || []);
|
setPhotos(step.photoUris || []);
|
||||||
|
|
@ -351,11 +211,9 @@ export const StepEditorScreen: React.FC = () => {
|
||||||
} else if (step.type === 'glazing') {
|
} else if (step.type === 'glazing') {
|
||||||
const glazing = (step as any).glazing;
|
const glazing = (step as any).glazing;
|
||||||
if (glazing) {
|
if (glazing) {
|
||||||
// New multi-layer format
|
|
||||||
if (glazing.glazeLayers && glazing.glazeLayers.length > 0) {
|
if (glazing.glazeLayers && glazing.glazeLayers.length > 0) {
|
||||||
setGlazeLayers(glazing.glazeLayers);
|
setGlazeLayers(glazing.glazeLayers);
|
||||||
} else {
|
} else {
|
||||||
// Legacy format - keep for compatibility
|
|
||||||
setCoats(glazing.coats?.toString() || '2');
|
setCoats(glazing.coats?.toString() || '2');
|
||||||
setApplicationMethod(glazing.application || 'brush');
|
setApplicationMethod(glazing.application || 'brush');
|
||||||
setSelectedGlazeIds(glazing.glazeIds || []);
|
setSelectedGlazeIds(glazing.glazeIds || []);
|
||||||
|
|
@ -373,7 +231,7 @@ export const StepEditorScreen: React.FC = () => {
|
||||||
|
|
||||||
const handleConeChange = (value: string) => {
|
const handleConeChange = (value: string) => {
|
||||||
setCone(value);
|
setCone(value);
|
||||||
const temp = getConeTemperature(value, tempUnit); // Pass user's preferred unit
|
const temp = getConeTemperature(value, tempUnit);
|
||||||
if (temp) {
|
if (temp) {
|
||||||
setTemperature(temp.value.toString());
|
setTemperature(temp.value.toString());
|
||||||
}
|
}
|
||||||
|
|
@ -385,13 +243,11 @@ export const StepEditorScreen: React.FC = () => {
|
||||||
Alert.alert('Permission needed', 'We need camera permissions to take photos');
|
Alert.alert('Permission needed', 'We need camera permissions to take photos');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await ImagePicker.launchCameraAsync({
|
const result = await ImagePicker.launchCameraAsync({
|
||||||
allowsEditing: true,
|
allowsEditing: true,
|
||||||
aspect: [4, 3],
|
aspect: [4, 3],
|
||||||
quality: 0.8,
|
quality: 0.8,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!result.canceled && result.assets[0]) {
|
if (!result.canceled && result.assets[0]) {
|
||||||
setPhotos([...photos, result.assets[0].uri]);
|
setPhotos([...photos, result.assets[0].uri]);
|
||||||
}
|
}
|
||||||
|
|
@ -403,29 +259,23 @@ export const StepEditorScreen: React.FC = () => {
|
||||||
Alert.alert('Permission needed', 'We need camera roll permissions to add photos');
|
Alert.alert('Permission needed', 'We need camera roll permissions to add photos');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await ImagePicker.launchImageLibraryAsync({
|
const result = await ImagePicker.launchImageLibraryAsync({
|
||||||
mediaTypes: 'images',
|
mediaTypes: 'images',
|
||||||
allowsEditing: true,
|
allowsEditing: true,
|
||||||
aspect: [4, 3],
|
aspect: [4, 3],
|
||||||
quality: 0.8,
|
quality: 0.8,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!result.canceled && result.assets[0]) {
|
if (!result.canceled && result.assets[0]) {
|
||||||
setPhotos([...photos, result.assets[0].uri]);
|
setPhotos([...photos, result.assets[0].uri]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddPhoto = () => {
|
const handleAddPhoto = () => {
|
||||||
Alert.alert(
|
Alert.alert('Add Photo', 'Choose a source', [
|
||||||
'Add Photo',
|
|
||||||
'Choose a source',
|
|
||||||
[
|
|
||||||
{ text: 'Camera', onPress: takePhoto },
|
{ text: 'Camera', onPress: takePhoto },
|
||||||
{ text: 'Gallery', onPress: pickImage },
|
{ text: 'Gallery', onPress: pickImage },
|
||||||
{ text: 'Cancel', style: 'cancel' },
|
{ text: 'Cancel', style: 'cancel' },
|
||||||
]
|
]);
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const removePhoto = (index: number) => {
|
const removePhoto = (index: number) => {
|
||||||
|
|
@ -433,19 +283,11 @@ export const StepEditorScreen: React.FC = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddFromCatalog = () => {
|
const handleAddFromCatalog = () => {
|
||||||
console.log('Add from Catalog clicked - setting up callback and navigating to GlazePicker');
|
|
||||||
|
|
||||||
// Set up callback BEFORE navigating - this will be called directly by GlazePicker
|
|
||||||
glazeSelectionCallbackRef.current = (newLayers: GlazeLayer[]) => {
|
glazeSelectionCallbackRef.current = (newLayers: GlazeLayer[]) => {
|
||||||
console.log('Callback received layers:', newLayers);
|
|
||||||
setGlazeLayers(prevLayers => [...prevLayers, ...newLayers]);
|
setGlazeLayers(prevLayers => [...prevLayers, ...newLayers]);
|
||||||
|
|
||||||
if (stepType !== 'glazing') {
|
if (stepType !== 'glazing') {
|
||||||
console.log('Setting stepType to glazing');
|
|
||||||
setStepType('glazing');
|
setStepType('glazing');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scroll to glaze layers section after state update
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (glazeLayersSectionRef.current && scrollViewRef.current) {
|
if (glazeLayersSectionRef.current && scrollViewRef.current) {
|
||||||
glazeLayersSectionRef.current.measure((x, y, width, height, pageX, pageY) => {
|
glazeLayersSectionRef.current.measure((x, y, width, height, pageX, pageY) => {
|
||||||
|
|
@ -456,7 +298,6 @@ export const StepEditorScreen: React.FC = () => {
|
||||||
}
|
}
|
||||||
}, 200);
|
}, 200);
|
||||||
};
|
};
|
||||||
|
|
||||||
navigation.navigate('GlazePicker', {
|
navigation.navigate('GlazePicker', {
|
||||||
projectId,
|
projectId,
|
||||||
stepId,
|
stepId,
|
||||||
|
|
@ -497,7 +338,6 @@ export const StepEditorScreen: React.FC = () => {
|
||||||
} else if (stepType === 'glazing') {
|
} else if (stepType === 'glazing') {
|
||||||
stepData.glazing = {
|
stepData.glazing = {
|
||||||
glazeLayers: glazeLayers.length > 0 ? glazeLayers : undefined,
|
glazeLayers: glazeLayers.length > 0 ? glazeLayers : undefined,
|
||||||
// Legacy fields for backwards compatibility
|
|
||||||
glazeIds: selectedGlazeIds.length > 0 ? selectedGlazeIds : undefined,
|
glazeIds: selectedGlazeIds.length > 0 ? selectedGlazeIds : undefined,
|
||||||
coats: coats ? parseInt(coats) : undefined,
|
coats: coats ? parseInt(coats) : undefined,
|
||||||
application: applicationMethod,
|
application: applicationMethod,
|
||||||
|
|
@ -507,20 +347,16 @@ export const StepEditorScreen: React.FC = () => {
|
||||||
|
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
await updateStep(stepId!, stepData);
|
await updateStep(stepId!, stepData);
|
||||||
console.log('✅ Step updated successfully');
|
|
||||||
} else {
|
} else {
|
||||||
const newStep = await createStep(stepData);
|
await createStep(stepData);
|
||||||
console.log('✅ Step created successfully, id:', newStep?.id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset navigation to ProjectDetail, keeping MainTabs in stack
|
|
||||||
console.log('Resetting stack to MainTabs → ProjectDetail after manual save');
|
|
||||||
navigation.dispatch(
|
navigation.dispatch(
|
||||||
CommonActions.reset({
|
CommonActions.reset({
|
||||||
index: 1, // ProjectDetail is the active screen
|
index: 1,
|
||||||
routes: [
|
routes: [
|
||||||
{ name: 'MainTabs' }, // Keep MainTabs in stack for back button
|
{ name: 'MainTabs' },
|
||||||
{ name: 'ProjectDetail', params: { projectId } }, // ProjectDetail is active
|
{ name: 'ProjectDetail', params: { projectId } },
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
@ -533,10 +369,7 @@ export const StepEditorScreen: React.FC = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
Alert.alert(
|
Alert.alert('Delete Step', 'Are you sure you want to delete this step?', [
|
||||||
'Delete Step',
|
|
||||||
'Are you sure you want to delete this step?',
|
|
||||||
[
|
|
||||||
{ text: 'Cancel', style: 'cancel' },
|
{ text: 'Cancel', style: 'cancel' },
|
||||||
{
|
{
|
||||||
text: 'Delete',
|
text: 'Delete',
|
||||||
|
|
@ -544,16 +377,17 @@ export const StepEditorScreen: React.FC = () => {
|
||||||
onPress: async () => {
|
onPress: async () => {
|
||||||
try {
|
try {
|
||||||
await deleteStep(stepId!);
|
await deleteStep(stepId!);
|
||||||
Alert.alert('Success', 'Step deleted');
|
|
||||||
navigation.goBack();
|
navigation.goBack();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to delete step:', error);
|
|
||||||
Alert.alert('Error', 'Failed to delete step');
|
Alert.alert('Error', 'Failed to delete step');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]);
|
||||||
);
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
navigation.goBack();
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderFormingFields = () => {
|
const renderFormingFields = () => {
|
||||||
|
|
@ -572,22 +406,19 @@ export const StepEditorScreen: React.FC = () => {
|
||||||
onChangeText={setClayBody}
|
onChangeText={setClayBody}
|
||||||
placeholder="e.g., B-Mix, Porcelain"
|
placeholder="e.g., B-Mix, Porcelain"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<WeightInput
|
<WeightInput
|
||||||
label="Clay Weight"
|
label="Clay Weight"
|
||||||
value={clayWeight}
|
value={clayWeight}
|
||||||
onChange={setClayWeight}
|
onChange={setClayWeight}
|
||||||
userPreferredUnit={weightUnit}
|
userPreferredUnit={weightUnit}
|
||||||
/>
|
/>
|
||||||
|
<Text style={styles.sectionLabel}>Production Method</Text>
|
||||||
<Text style={styles.label}>Production Method</Text>
|
|
||||||
<ButtonGrid
|
<ButtonGrid
|
||||||
options={productionMethods}
|
options={productionMethods}
|
||||||
selectedValue={productionMethod}
|
selectedValue={productionMethod}
|
||||||
onSelect={(value) => setProductionMethod(value as ProductionMethod)}
|
onSelect={(value) => setProductionMethod(value as ProductionMethod)}
|
||||||
columns={2}
|
columns={2}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DimensionsInput
|
<DimensionsInput
|
||||||
label="Dimensions"
|
label="Dimensions"
|
||||||
value={dimensions}
|
value={dimensions}
|
||||||
|
|
@ -605,7 +436,6 @@ export const StepEditorScreen: React.FC = () => {
|
||||||
{ value: 'raku', label: 'Raku' },
|
{ value: 'raku', label: 'Raku' },
|
||||||
{ value: 'other', label: 'Other' },
|
{ value: 'other', label: 'Other' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const kilnPositions = [
|
const kilnPositions = [
|
||||||
{ value: 'top', label: 'Top' },
|
{ value: 'top', label: 'Top' },
|
||||||
{ value: 'middle', label: 'Middle' },
|
{ value: 'middle', label: 'Middle' },
|
||||||
|
|
@ -635,15 +465,13 @@ export const StepEditorScreen: React.FC = () => {
|
||||||
placeholder="Total firing time"
|
placeholder="Total firing time"
|
||||||
keyboardType="numeric"
|
keyboardType="numeric"
|
||||||
/>
|
/>
|
||||||
|
<Text style={styles.sectionLabel}>Kiln Type</Text>
|
||||||
<Text style={styles.label}>Kiln Type</Text>
|
|
||||||
<ButtonGrid
|
<ButtonGrid
|
||||||
options={kilnTypes}
|
options={kilnTypes}
|
||||||
selectedValue={kilnType}
|
selectedValue={kilnType}
|
||||||
onSelect={(value) => setKilnType(value as KilnType)}
|
onSelect={(value) => setKilnType(value as KilnType)}
|
||||||
columns={2}
|
columns={2}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{kilnType === 'other' && (
|
{kilnType === 'other' && (
|
||||||
<Input
|
<Input
|
||||||
label="Custom Kiln Name"
|
label="Custom Kiln Name"
|
||||||
|
|
@ -652,15 +480,13 @@ export const StepEditorScreen: React.FC = () => {
|
||||||
placeholder="Enter kiln name"
|
placeholder="Enter kiln name"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<Text style={styles.sectionLabel}>Position in Kiln</Text>
|
||||||
<Text style={styles.label}>Position in Kiln</Text>
|
|
||||||
<ButtonGrid
|
<ButtonGrid
|
||||||
options={kilnPositions}
|
options={kilnPositions}
|
||||||
selectedValue={kilnPosition}
|
selectedValue={kilnPosition}
|
||||||
onSelect={(value) => setKilnPosition(value as KilnPosition)}
|
onSelect={(value) => setKilnPosition(value as KilnPosition)}
|
||||||
columns={2}
|
columns={2}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{kilnPosition === 'other' && (
|
{kilnPosition === 'other' && (
|
||||||
<Input
|
<Input
|
||||||
label="Position Notes"
|
label="Position Notes"
|
||||||
|
|
@ -669,7 +495,6 @@ export const StepEditorScreen: React.FC = () => {
|
||||||
placeholder="Describe position"
|
placeholder="Describe position"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
label="Firing Schedule"
|
label="Firing Schedule"
|
||||||
value={firingSchedule}
|
value={firingSchedule}
|
||||||
|
|
@ -678,7 +503,6 @@ export const StepEditorScreen: React.FC = () => {
|
||||||
multiline
|
multiline
|
||||||
numberOfLines={4}
|
numberOfLines={4}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
label="Kiln Notes"
|
label="Kiln Notes"
|
||||||
value={kilnNotes}
|
value={kilnNotes}
|
||||||
|
|
@ -693,7 +517,6 @@ export const StepEditorScreen: React.FC = () => {
|
||||||
const renderGlazingFields = () => {
|
const renderGlazingFields = () => {
|
||||||
const handleAddCustomGlaze = () => {
|
const handleAddCustomGlaze = () => {
|
||||||
if (!customGlazeName.trim()) return;
|
if (!customGlazeName.trim()) return;
|
||||||
|
|
||||||
const newLayer: GlazeLayer = {
|
const newLayer: GlazeLayer = {
|
||||||
customGlazeName: customGlazeName.trim(),
|
customGlazeName: customGlazeName.trim(),
|
||||||
application: 'brush',
|
application: 'brush',
|
||||||
|
|
@ -701,7 +524,6 @@ export const StepEditorScreen: React.FC = () => {
|
||||||
coats: 2,
|
coats: 2,
|
||||||
notes: '',
|
notes: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
setGlazeLayers([...glazeLayers, newLayer]);
|
setGlazeLayers([...glazeLayers, newLayer]);
|
||||||
setCustomGlazeName('');
|
setCustomGlazeName('');
|
||||||
};
|
};
|
||||||
|
|
@ -719,8 +541,7 @@ export const StepEditorScreen: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<View ref={glazeLayersSectionRef}>
|
<View ref={glazeLayersSectionRef}>
|
||||||
<Text style={styles.label}>Glaze Layers</Text>
|
<Text style={styles.sectionLabel}>Glaze Layers</Text>
|
||||||
|
|
||||||
{glazeLayers.map((layer, index) => (
|
{glazeLayers.map((layer, index) => (
|
||||||
<GlazeLayerCard
|
<GlazeLayerCard
|
||||||
key={index}
|
key={index}
|
||||||
|
|
@ -733,7 +554,7 @@ export const StepEditorScreen: React.FC = () => {
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.addGlazeContainer}>
|
<View style={styles.addGlazeContainer}>
|
||||||
<Text style={styles.label}>Add Custom Glaze</Text>
|
<Text style={styles.sectionLabel}>Add Custom Glaze</Text>
|
||||||
<View style={styles.customGlazeRow}>
|
<View style={styles.customGlazeRow}>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={styles.customGlazeInput}
|
style={styles.customGlazeInput}
|
||||||
|
|
@ -776,7 +597,27 @@ export const StepEditorScreen: React.FC = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const stepTypeOptions = STEP_TYPES.map(type => ({
|
||||||
|
...type,
|
||||||
|
icon: (
|
||||||
|
<MaterialCommunityIcons
|
||||||
|
name={stepTypeIcons[type.value] as any}
|
||||||
|
size={24}
|
||||||
|
color={stepType === type.value ? colors.buttonText : stepTypeColors[type.value]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<SafeAreaView style={styles.safeArea} edges={['top']}>
|
||||||
|
<View style={styles.header}>
|
||||||
|
<TouchableOpacity style={styles.backButton} onPress={handleCancel}>
|
||||||
|
<MaterialCommunityIcons name="chevron-left" size={24} color={colors.text} />
|
||||||
|
<Text style={styles.backButtonText}>Back</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<Text style={styles.headerTitle}>{isEditing ? 'Edit Step' : 'Add Step'}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
<KeyboardAwareScrollView
|
<KeyboardAwareScrollView
|
||||||
ref={scrollViewRef}
|
ref={scrollViewRef}
|
||||||
style={styles.container}
|
style={styles.container}
|
||||||
|
|
@ -787,25 +628,19 @@ export const StepEditorScreen: React.FC = () => {
|
||||||
keyboardShouldPersistTaps="handled"
|
keyboardShouldPersistTaps="handled"
|
||||||
>
|
>
|
||||||
<Card>
|
<Card>
|
||||||
<Text style={styles.label}>Step Type</Text>
|
<Text style={styles.sectionLabel}>Step Type</Text>
|
||||||
<View style={styles.typeGrid}>
|
<ButtonGrid
|
||||||
{STEP_TYPES.map((type) => (
|
options={stepTypeOptions}
|
||||||
<Button
|
selectedValue={stepType}
|
||||||
key={type.value}
|
onSelect={(value) => setStepType(value as StepType)}
|
||||||
title={type.label}
|
columns={2}
|
||||||
onPress={() => setStepType(type.value)}
|
|
||||||
variant={stepType === type.value ? 'primary' : 'outline'}
|
|
||||||
size="sm"
|
|
||||||
style={styles.typeButton}
|
|
||||||
/>
|
/>
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{stepType === 'forming' && renderFormingFields()}
|
{stepType === 'forming' && renderFormingFields()}
|
||||||
{(stepType === 'bisque_firing' || stepType === 'glaze_firing') && renderFiringFields()}
|
{(stepType === 'bisque_firing' || stepType === 'glaze_firing') && renderFiringFields()}
|
||||||
{stepType === 'glazing' && renderGlazingFields()}
|
{stepType === 'glazing' && renderGlazingFields()}
|
||||||
|
|
||||||
<Text style={styles.label}>Photos</Text>
|
<Text style={styles.sectionLabel}>Photos</Text>
|
||||||
<View style={styles.photosContainer}>
|
<View style={styles.photosContainer}>
|
||||||
{photos.map((uri, index) => (
|
{photos.map((uri, index) => (
|
||||||
<View key={index} style={styles.photoWrapper}>
|
<View key={index} style={styles.photoWrapper}>
|
||||||
|
|
@ -856,10 +691,51 @@ export const StepEditorScreen: React.FC = () => {
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
</KeyboardAwareScrollView>
|
</KeyboardAwareScrollView>
|
||||||
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
|
safeArea: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: colors.background,
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center', // content centered horizontally via absolute positioning
|
||||||
|
paddingHorizontal: spacing.md,
|
||||||
|
paddingVertical: spacing.sm,
|
||||||
|
backgroundColor: colors.background,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: colors.border,
|
||||||
|
position: 'relative', // Context for absolute children
|
||||||
|
},
|
||||||
|
headerTitle: {
|
||||||
|
position: 'absolute',
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
textAlign: 'center',
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: '800',
|
||||||
|
color: colors.text,
|
||||||
|
},
|
||||||
|
backButton: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: colors.card,
|
||||||
|
borderRadius: borderRadius.full,
|
||||||
|
paddingVertical: spacing.xs,
|
||||||
|
paddingHorizontal: spacing.sm,
|
||||||
|
paddingRight: spacing.md,
|
||||||
|
...shadows.sm,
|
||||||
|
zIndex: 10,
|
||||||
|
},
|
||||||
|
backButtonText: {
|
||||||
|
fontSize: typography.fontSize.md,
|
||||||
|
fontWeight: typography.fontWeight.medium,
|
||||||
|
color: colors.text,
|
||||||
|
marginLeft: -4,
|
||||||
|
},
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: colors.backgroundSecondary,
|
backgroundColor: colors.backgroundSecondary,
|
||||||
|
|
@ -868,22 +744,14 @@ const styles = StyleSheet.create({
|
||||||
padding: spacing.md,
|
padding: spacing.md,
|
||||||
paddingBottom: spacing.xxl,
|
paddingBottom: spacing.xxl,
|
||||||
},
|
},
|
||||||
label: {
|
sectionLabel: {
|
||||||
fontSize: typography.fontSize.sm,
|
fontSize: typography.fontSize.sm,
|
||||||
fontWeight: typography.fontWeight.bold,
|
fontWeight: typography.fontWeight.bold,
|
||||||
color: colors.text,
|
color: colors.text,
|
||||||
marginBottom: spacing.sm,
|
marginBottom: spacing.sm,
|
||||||
letterSpacing: 0.5,
|
letterSpacing: 0.5,
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
},
|
marginTop: spacing.md,
|
||||||
typeGrid: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
gap: spacing.sm,
|
|
||||||
marginBottom: spacing.lg,
|
|
||||||
},
|
|
||||||
typeButton: {
|
|
||||||
minWidth: '30%',
|
|
||||||
},
|
},
|
||||||
note: {
|
note: {
|
||||||
fontSize: typography.fontSize.sm,
|
fontSize: typography.fontSize.sm,
|
||||||
|
|
@ -891,6 +759,9 @@ const styles = StyleSheet.create({
|
||||||
fontStyle: 'italic',
|
fontStyle: 'italic',
|
||||||
marginTop: spacing.sm,
|
marginTop: spacing.sm,
|
||||||
},
|
},
|
||||||
|
typeGrid: {
|
||||||
|
marginBottom: 120, // Much larger separation as requested
|
||||||
|
},
|
||||||
photosContainer: {
|
photosContainer: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
|
|
@ -951,44 +822,6 @@ const styles = StyleSheet.create({
|
||||||
deleteButtonInSticky: {
|
deleteButtonInSticky: {
|
||||||
marginTop: spacing.md,
|
marginTop: spacing.md,
|
||||||
},
|
},
|
||||||
catalogButtonInSticky: {
|
|
||||||
marginBottom: spacing.md,
|
|
||||||
},
|
|
||||||
applicationGrid: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
gap: spacing.sm,
|
|
||||||
marginBottom: spacing.lg,
|
|
||||||
},
|
|
||||||
applicationButton: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
paddingHorizontal: spacing.md,
|
|
||||||
paddingVertical: spacing.sm,
|
|
||||||
borderRadius: borderRadius.md,
|
|
||||||
borderWidth: 2,
|
|
||||||
borderColor: colors.border,
|
|
||||||
backgroundColor: colors.backgroundSecondary,
|
|
||||||
gap: spacing.xs,
|
|
||||||
},
|
|
||||||
applicationButtonActive: {
|
|
||||||
borderColor: colors.primary,
|
|
||||||
backgroundColor: colors.primaryLight,
|
|
||||||
},
|
|
||||||
applicationIcon: {
|
|
||||||
fontSize: 18,
|
|
||||||
},
|
|
||||||
applicationText: {
|
|
||||||
fontSize: typography.fontSize.sm,
|
|
||||||
fontWeight: typography.fontWeight.bold,
|
|
||||||
color: colors.textSecondary,
|
|
||||||
},
|
|
||||||
applicationTextActive: {
|
|
||||||
color: colors.text,
|
|
||||||
},
|
|
||||||
glazePickerButton: {
|
|
||||||
marginBottom: spacing.sm,
|
|
||||||
},
|
|
||||||
addGlazeContainer: {
|
addGlazeContainer: {
|
||||||
marginBottom: spacing.md,
|
marginBottom: spacing.md,
|
||||||
},
|
},
|
||||||
|
|
@ -1018,11 +851,4 @@ const styles = StyleSheet.create({
|
||||||
marginTop: spacing.xs,
|
marginTop: spacing.xs,
|
||||||
marginBottom: spacing.md,
|
marginBottom: spacing.md,
|
||||||
},
|
},
|
||||||
stickyButtonContainer: {
|
|
||||||
padding: spacing.md,
|
|
||||||
paddingBottom: spacing.xl,
|
|
||||||
backgroundColor: colors.backgroundSecondary,
|
|
||||||
borderTopWidth: 2,
|
|
||||||
borderTopColor: colors.border,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
farben ändern
|
||||||
|
|
||||||
|
suggested next step -> wenn mn auf add step klickt -> der suggested step
|
||||||
|
auf suggested steps klicken im project overview
|
||||||
|
Glazing: unter layer kein scroll karousell ->button auswahl
|
||||||
|
suggested step: immer der nächste step, egal ob man was übersprungen hat, z.B. nach glazing kann man nicht mehr trimmen
|
||||||
|
|
||||||
|
icons
|
||||||