Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ name: Create EAS Preview

on:
pull_request_target:
workflow_dispatch:

Comment on lines 3 to 6
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Manual dispatch added, but missing inputs and noisy triggers; define inputs and narrow pull_request_target types

Without inputs, workflow_dispatch runs won’t know which ref/PR to build, and the job may still trigger on many PR events unnecessarily. Define explicit inputs and restrict pull_request_target event types to when useful.

Apply:

 on:
   pull_request_target:
+    types: [labeled, synchronize, reopened]
-  workflow_dispatch:
+  workflow_dispatch:
+    inputs:
+      ref:
+        description: Git ref (branch name) to build (e.g., feature/my-branch)
+        required: true
+        type: string
+      pr:
+        description: Optional PR number to comment on (if running outside a PR event)
+        required: false
+        type: number

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
.github/workflows/preview.yml lines 3-6: the workflow currently enables
pull_request_target and workflow_dispatch but lacks dispatch inputs and allows
noisy PR triggers; add explicit workflow_dispatch inputs (e.g., ref or pr_number
and optional workflow_run flags) so manual runs know which ref/PR to build, and
constrain pull_request_target with a types list (for example opened, reopened,
synchronize, ready_for_review) or additional filters (branches or paths) to
reduce noisy triggers; update any jobs/steps that read the ref/pr to consume the
new inputs when present.

permissions:
contents: read
Expand Down
339 changes: 339 additions & 0 deletions frontend/A Design Blueprint for a Modern, Minimalist Expens.md

Large diffs are not rendered by default.

11 changes: 7 additions & 4 deletions frontend/App.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import React from 'react';
import AppNavigator from './navigation/AppNavigator';
import { PaperProvider } from 'react-native-paper';
import { AuthProvider } from './context/AuthContext';
import AppNavigator from './navigation/AppNavigator';
import { paperTheme } from './utils/theme';
import { ToastProvider } from './utils/toast';

export default function App() {
return (
<AuthProvider>
<PaperProvider>
<AppNavigator />
<PaperProvider theme={paperTheme}>
<ToastProvider>
<AppNavigator />
</ToastProvider>
</PaperProvider>
</AuthProvider>
);
Expand Down
182 changes: 182 additions & 0 deletions frontend/components/core/Button.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
// Core Button Component - Following Blueprint Specifications
// Implements the 8-second rule and haptic feedback for Gen Z engagement

import * as Haptics from 'expo-haptics';
import { LinearGradient } from 'expo-linear-gradient';
import { ActivityIndicator, Text, TouchableOpacity, View } from 'react-native';
import theme, { borderRadius, colors, shadows, spacing } from '../../utils/theme';

const Button = ({
title,
variant = 'primary', // primary, secondary, outline, ghost, destructive
size = 'medium', // small, medium, large
onPress,
disabled = false,
loading = false,
icon,
fullWidth = false,
style,
textStyle,
...props
}) => {
const handlePress = async () => {
if (disabled || loading) return;

// Haptic feedback for engagement (Gen Z preference for tactile response)
await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);

if (onPress) {
onPress();
}
};

// Size configurations following minimum touch target of 44px
const sizeConfig = {
small: {
paddingVertical: spacing.sm,
paddingHorizontal: spacing.md,
minHeight: 36,
fontSize: 14,
fontWeight: '500',
},
medium: {
paddingVertical: spacing.md,
paddingHorizontal: spacing.lg,
minHeight: 44, // Accessibility minimum
fontSize: 16,
fontWeight: '600',
},
large: {
paddingVertical: spacing.lg,
paddingHorizontal: spacing.xl,
minHeight: 52,
fontSize: 18,
fontWeight: '600',
},
};

// Variant configurations for different button types
const variantConfig = {
primary: {
useGradient: true,
gradientColors: [colors.brand.accent, colors.brand.accentAlt],
textColor: '#FFFFFF',
shadowStyle: shadows.small,
},
secondary: {
backgroundColor: colors.background.secondary,
textColor: colors.text.primary,
borderWidth: 1,
borderColor: colors.border.subtle,
shadowStyle: shadows.subtle,
},
outline: {
backgroundColor: 'transparent',
textColor: colors.brand.accent,
borderWidth: 2,
borderColor: colors.brand.accent,
},
ghost: {
backgroundColor: 'transparent',
textColor: colors.brand.accent,
},
destructive: {
backgroundColor: colors.semantic.error,
textColor: '#FFFFFF',
shadowStyle: shadows.small,
},
};

const currentSize = sizeConfig[size];
const currentVariant = variantConfig[variant];

// Base button style
const buttonStyle = {
borderRadius: borderRadius.md,
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'row',
minHeight: currentSize.minHeight,
paddingVertical: currentSize.paddingVertical,
paddingHorizontal: currentSize.paddingHorizontal,
width: fullWidth ? '100%' : 'auto',
opacity: disabled ? 0.6 : 1,
...currentVariant.shadowStyle,
...currentVariant,
...style,
};
Comment on lines +93 to +107
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid passing non-style keys into style objects

You’re spreading the entire currentVariant object into buttonStyle. Keys like useGradient, gradientColors, and textColor are not style props and pollute the style object.

Refactor to only apply style-related keys from the variant:

-  const buttonStyle = {
+  const { shadowStyle, backgroundColor, borderWidth, borderColor } = currentVariant;
+  const buttonStyle = {
     borderRadius: borderRadius.md,
     alignItems: 'center',
     justifyContent: 'center',
     flexDirection: 'row',
     minHeight: currentSize.minHeight,
     paddingVertical: currentSize.paddingVertical,
     paddingHorizontal: currentSize.paddingHorizontal,
     width: fullWidth ? '100%' : 'auto',
     opacity: disabled ? 0.6 : 1,
-    ...currentVariant.shadowStyle,
-    ...currentVariant,
+    ...(shadowStyle || {}),
+    ...(backgroundColor ? { backgroundColor } : {}),
+    ...(borderWidth ? { borderWidth } : {}),
+    ...(borderColor ? { borderColor } : {}),
     ...style,
   };

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In frontend/components/core/Button.js around lines 93 to 107, the code currently
spreads the entire currentVariant into buttonStyle which mixes non-style keys
(useGradient, gradientColors, textColor) into the style object; change this to
only spread actual style properties by extracting non-style keys first (e.g.
const { useGradient, gradientColors, textColor, ...variantStyle } =
currentVariant) and then spread variantStyle (and currentVariant.shadowStyle if
needed) into buttonStyle, or alternatively explicitly pick known style keys from
currentVariant before spreading so only valid style props are included.


// Text style
const textStyleConfig = {
fontSize: currentSize.fontSize,
fontWeight: currentSize.fontWeight,
color: currentVariant.textColor,
fontFamily: 'Inter',
...textStyle,
};

// Loading spinner color
const spinnerColor = currentVariant.textColor;

const ButtonContent = () => (
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
{loading && (
<ActivityIndicator
size="small"
color={spinnerColor}
style={{ marginRight: spacing.sm }}
/>
)}
{icon && !loading && (
<View style={{ marginRight: spacing.sm }}>
{icon}
</View>
)}
<Text style={textStyleConfig}>
{title}
</Text>
</View>
);

// Render with gradient if specified
if (currentVariant.useGradient && !disabled) {
return (
<TouchableOpacity
onPress={handlePress}
disabled={disabled || loading}
activeOpacity={0.8}
style={[buttonStyle, { backgroundColor: 'transparent' }]}
{...props}
>
<LinearGradient
colors={currentVariant.gradientColors}
style={{
...buttonStyle,
shadowColor: 'transparent', // Remove shadow from gradient container
elevation: 0,
}}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
>
<ButtonContent />
</LinearGradient>
</TouchableOpacity>
);
}
Comment on lines +141 to +165
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Disabled primary button loses its background (gradient gated by disabled)

When variant is “primary” and disabled is true, the gradient path is skipped, and no solid background is defined for the primary variant. The disabled primary button can render with a transparent background, harming legibility and affordance.

Apply this minimal fix to always render the gradient, relying on the existing opacity reduction for disabled:

-  // Render with gradient if specified
-  if (currentVariant.useGradient && !disabled) {
+  // Render with gradient if specified (even when disabled, rely on reduced opacity)
+  if (currentVariant.useGradient) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Render with gradient if specified
if (currentVariant.useGradient && !disabled) {
return (
<TouchableOpacity
onPress={handlePress}
disabled={disabled || loading}
activeOpacity={0.8}
style={[buttonStyle, { backgroundColor: 'transparent' }]}
{...props}
>
<LinearGradient
colors={currentVariant.gradientColors}
style={{
...buttonStyle,
shadowColor: 'transparent', // Remove shadow from gradient container
elevation: 0,
}}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
>
<ButtonContent />
</LinearGradient>
</TouchableOpacity>
);
}
// Render with gradient if specified (even when disabled, rely on reduced opacity)
if (currentVariant.useGradient) {
return (
<TouchableOpacity
onPress={handlePress}
disabled={disabled || loading}
activeOpacity={0.8}
style={[buttonStyle, { backgroundColor: 'transparent' }]}
{...props}
>
<LinearGradient
colors={currentVariant.gradientColors}
style={{
...buttonStyle,
shadowColor: 'transparent', // Remove shadow from gradient container
elevation: 0,
}}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
>
<ButtonContent />
</LinearGradient>
</TouchableOpacity>
);
}
🤖 Prompt for AI Agents
frontend/components/core/Button.js around lines 141-165: the current conditional
skips the gradient when disabled, causing a transparent background for disabled
primary buttons; remove the "!disabled" gate so the LinearGradient branch always
renders for variants with useGradient, keep TouchableOpacity's disabled and
loading props as-is so the built-in opacity/reduced-affordance behavior remains,
and leave the gradient colors/styles unchanged (you may keep the transparent
background on the TouchableOpacity wrapper and elevation/shadow adjustments on
the LinearGradient).


// Regular button without gradient
return (
<TouchableOpacity
onPress={handlePress}
disabled={disabled || loading}
activeOpacity={0.8}
style={buttonStyle}
{...props}
>
<ButtonContent />
</TouchableOpacity>
);
};

export default Button;
export { Button as ModernButton };
Loading
Loading