Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
c61c58f
feat: hero home screen, richer cards, inline AI actions
claude Mar 17, 2026
fb1ea93
chore: add EAS config and expo-updates for OTA deployment
claude Mar 17, 2026
9c3529c
ci: add GitHub Actions workflow for EAS Update preview deploys
claude Mar 17, 2026
fcfb273
fix: remove reanimated babel plugin causing CI export failure
claude Mar 17, 2026
1fb3ea3
fix: add missing babel-preset-expo dependency
claude Mar 17, 2026
cd163cb
fix: disable New Architecture and remove expo-symbols for Expo Go
claude Mar 17, 2026
49a7172
fix: switch runtimeVersion to sdkVersion for Expo Go compatibility
claude Mar 17, 2026
2c8a178
fix: set correct EAS project ID in updates URL
claude Mar 17, 2026
44f83be
fix: downgrade to SDK 54 for Expo Go App Store compatibility
claude Mar 17, 2026
8940ff7
fix: restore SDK 55 with correct expo-router version (55.x not 5.x)
claude Mar 17, 2026
97d3a26
fix: add .npmrc with legacy-peer-deps for expo-router 55.x
claude Mar 17, 2026
77fcb82
Fix create screen crash: align native module versions with Expo SDK 55
claude Mar 17, 2026
7401190
Update package-lock.json with correct native module versions
claude Mar 17, 2026
711a2d4
Fix all missing/wrong package versions for Expo SDK 55
claude Mar 17, 2026
e246a78
Switch AI provider from Anthropic to Azure AI Foundry
claude Mar 17, 2026
4fbf6a8
Fix AI endpoint for Anthropic-native Azure format
claude Mar 17, 2026
e10fad5
Fix Azure auth header to Ocp-Apim-Subscription-Key
claude Mar 17, 2026
0edcb64
Add debug logging to diagnose Azure auth error
claude Mar 17, 2026
3c1fa75
Show full debug info in UI error message for iOS Expo Go
claude Mar 17, 2026
29bfbcc
Replace AI error Alert with copyable debug modal
claude Mar 17, 2026
7539a6e
Add Test Connection button to Settings with debug modal
claude Mar 17, 2026
9784a77
Fix Azure AI Foundry auth: use ?api-version query param, not header
claude Mar 18, 2026
5610149
Switch AI calls to Anthropic SDK with Azure AI Foundry config
claude Mar 18, 2026
6785c55
Add @anthropic-ai/sdk dependency
claude Mar 18, 2026
b9eaab8
Add TipTap-powered document editor to article detail screen
claude Mar 18, 2026
579425e
Add AI hero image generation and AI Collaborator for article editing
claude Mar 18, 2026
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
47 changes: 47 additions & 0 deletions .github/workflows/preview.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: EAS Preview Update

on:
push:
branches:
- claude/article-ideas-react-native-puYVn
- main
- master

jobs:
update:
name: Publish EAS Update
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm

- name: Install dependencies
run: npm ci

- name: Setup Expo
uses: expo/expo-github-action@v8
with:
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}

- name: Initialize EAS project (creates if not exists)
run: eas init --non-interactive --force
env:
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}

- name: Publish preview update
run: |
eas update \
--channel preview \
--message "$(git log -1 --pretty=%s)" \
--platform ios \
--non-interactive
env:
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
EXPO_OFFLINE: "1"
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
legacy-peer-deps=true
8 changes: 7 additions & 1 deletion app.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"orientation": "default",
"icon": "./assets/icon.png",
"userInterfaceStyle": "dark",
"newArchEnabled": true,
"newArchEnabled": false,
"splash": {
"image": "./assets/splash-icon.png",
"resizeMode": "contain",
Expand All @@ -28,6 +28,12 @@
"bundler": "metro",
"favicon": "./assets/favicon.png"
},
"updates": {
"url": "https://u.expo.dev/6a1a260a-d832-47c9-bcf7-52cbef213b47"
},
"runtimeVersion": {
"policy": "sdkVersion"
},
"plugins": [
"expo-router"
]
Expand Down
223 changes: 131 additions & 92 deletions app/(tabs)/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useCallback, useEffect } from 'react';
import React, { useState, useCallback } from 'react';
import {
StyleSheet,
Text,
Expand Down Expand Up @@ -28,6 +28,22 @@ const STATUS_FILTERS: { label: string; value: IdeaStatus | 'all' }[] = [
{ label: 'Published', value: 'published' },
];

function StatBadge({ value, label, accent }: { value: number; label: string; accent?: boolean }) {
return (
<View style={statStyles.badge}>
<Text style={[statStyles.value, accent && statStyles.valueAccent]}>{value}</Text>
<Text style={statStyles.label}>{label}</Text>
</View>
);
}

const statStyles = StyleSheet.create({
badge: { alignItems: 'center', flex: 1 },
value: { color: Colors.textPrimary, fontSize: 22, fontWeight: '800', letterSpacing: -0.5 },
valueAccent: { color: Colors.accentBright },
label: { color: Colors.textMuted, fontSize: 11, marginTop: 1, fontWeight: '500' },
});

export default function HomeScreen() {
const [ideas, setIdeas] = useState<ArticleIdea[]>([]);
const [search, setSearch] = useState('');
Expand Down Expand Up @@ -56,22 +72,18 @@ export default function HomeScreen() {

const handleDelete = (idea: ArticleIdea) => {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning);
Alert.alert(
'Delete Idea',
`Delete "${idea.title}"? This cannot be undone.`,
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Delete',
style: 'destructive',
onPress: async () => {
await deleteIdea(idea.id);
await load();
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
},
Alert.alert('Delete Idea', `Delete "${idea.title}"? This cannot be undone.`, [
{ text: 'Cancel', style: 'cancel' },
{
text: 'Delete',
style: 'destructive',
onPress: async () => {
await deleteIdea(idea.id);
await load();
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
},
]
);
},
]);
};

const filtered = ideas.filter((idea) => {
Expand All @@ -84,6 +96,15 @@ export default function HomeScreen() {
return matchesSearch && matchesFilter;
});

// Stats for hero
const aiCount = ideas.filter(
(i) =>
i.aiContent?.outline ||
i.aiContent?.intro ||
i.aiContent?.improvedTitles?.length
).length;
const publishedCount = ideas.filter((i) => i.status === 'published').length;

const renderItem = ({ item, index }: { item: ArticleIdea; index: number }) => (
<View style={[styles.cardWrapper, isTablet && index % 2 === 0 && styles.cardWrapperLeft]}>
<IdeaCard
Expand All @@ -94,19 +115,37 @@ export default function HomeScreen() {
</View>
);

return (
<SafeAreaView style={styles.safeArea} edges={['top']}>
{/* Header */}
<View style={styles.header}>
<View>
<Text style={styles.appName}>Nova</Text>
<Text style={styles.appSub}>
{ideas.length} idea{ideas.length !== 1 ? 's' : ''}
</Text>
const ListHeader = (
<>
{/* ── Hero ── */}
<LinearGradient
colors={[
'rgba(124, 106, 247, 0.18)',
'rgba(124, 106, 247, 0.06)',
'transparent',
]}
style={styles.hero}
start={{ x: 0, y: 0 }}
end={{ x: 0.5, y: 1 }}
>
<View style={styles.heroTop}>
<View style={styles.heroTitleRow}>
<Text style={styles.heroStar}>✦</Text>
<Text style={styles.heroName}>Nova</Text>
</View>
<Text style={styles.heroTagline}>Article ideas, amplified by AI</Text>
</View>
</View>

{/* Search */}
<View style={styles.statsRow}>
<StatBadge value={ideas.length} label="ideas" />
<View style={styles.statDivider} />
<StatBadge value={aiCount} label="AI enhanced" accent />
<View style={styles.statDivider} />
<StatBadge value={publishedCount} label="published" />
</View>
</LinearGradient>

{/* ── Search ── */}
<View style={styles.searchRow}>
<View style={styles.searchBox}>
<Text style={styles.searchIcon}>🔍</Text>
Expand All @@ -126,7 +165,7 @@ export default function HomeScreen() {
</View>
</View>

{/* Filters */}
{/* ── Status filters ── */}
<FlatList
horizontal
data={STATUS_FILTERS}
Expand All @@ -151,24 +190,21 @@ export default function HomeScreen() {
}}
style={styles.filterBar}
/>
</>
);

{/* Ideas list */}
return (
<SafeAreaView style={styles.safeArea} edges={['top']}>
<FlatList
data={filtered}
keyExtractor={(item) => item.id}
numColumns={numColumns}
key={numColumns} // re-render when numColumns changes
contentContainerStyle={[
styles.list,
filtered.length === 0 && styles.listEmpty,
]}
key={numColumns}
ListHeaderComponent={ListHeader}
contentContainerStyle={[styles.list, filtered.length === 0 && styles.listEmpty]}
renderItem={renderItem}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
tintColor={Colors.accent}
/>
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={Colors.accent} />
}
ListEmptyComponent={<EmptyState searching={!!search || filter !== 'all'} />}
showsVerticalScrollIndicator={false}
Expand Down Expand Up @@ -200,25 +236,52 @@ const styles = StyleSheet.create({
flex: 1,
backgroundColor: Colors.bg,
},
header: {
flexDirection: 'row',
alignItems: 'flex-end',
justifyContent: 'space-between',
// Hero
hero: {
paddingHorizontal: 20,
paddingTop: 8,
paddingBottom: 16,
paddingTop: 16,
paddingBottom: 24,
marginBottom: 4,
},
heroTop: {
marginBottom: 20,
},
heroTitleRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
marginBottom: 4,
},
appName: {
heroStar: {
color: Colors.accent,
fontSize: 22,
},
heroName: {
color: Colors.textPrimary,
fontSize: 34,
fontWeight: '800',
letterSpacing: -0.5,
fontSize: 40,
fontWeight: '900',
letterSpacing: -1,
lineHeight: 44,
},
heroTagline: {
color: Colors.textSecondary,
fontSize: 14,
marginLeft: 30,
},
appSub: {
color: Colors.textMuted,
fontSize: 13,
marginTop: 2,
statsRow: {
flexDirection: 'row',
backgroundColor: 'rgba(255,255,255,0.04)',
borderRadius: 16,
borderWidth: 1,
borderColor: Colors.border,
paddingVertical: 14,
},
statDivider: {
width: StyleSheet.hairlineWidth,
backgroundColor: Colors.border,
marginVertical: 4,
},
// Search
searchRow: {
paddingHorizontal: 16,
marginBottom: 12,
Expand All @@ -234,27 +297,17 @@ const styles = StyleSheet.create({
borderColor: Colors.border,
gap: 8,
},
searchIcon: {
fontSize: 16,
},
searchIcon: { fontSize: 16 },
searchInput: {
flex: 1,
color: Colors.textPrimary,
fontSize: 15,
padding: 0,
},
clearBtn: {
color: Colors.textMuted,
fontSize: 14,
},
filterBar: {
flexGrow: 0,
marginBottom: 12,
},
filterList: {
paddingHorizontal: 16,
gap: 8,
},
clearBtn: { color: Colors.textMuted, fontSize: 14 },
// Filters
filterBar: { flexGrow: 0, marginBottom: 12 },
filterList: { paddingHorizontal: 16, gap: 8 },
filterChip: {
paddingHorizontal: 14,
paddingVertical: 7,
Expand All @@ -267,36 +320,22 @@ const styles = StyleSheet.create({
backgroundColor: Colors.accentSoft,
borderColor: Colors.accent,
},
filterLabel: {
color: Colors.textSecondary,
fontSize: 13,
fontWeight: '500',
},
filterLabelActive: {
color: Colors.accentBright,
fontWeight: '600',
},
list: {
paddingHorizontal: 16,
paddingBottom: 100,
},
listEmpty: {
flex: 1,
},
cardWrapper: {
flex: 1,
},
cardWrapperLeft: {
marginRight: 6,
},
filterLabel: { color: Colors.textSecondary, fontSize: 13, fontWeight: '500' },
filterLabelActive: { color: Colors.accentBright, fontWeight: '600' },
// List
list: { paddingHorizontal: 16, paddingBottom: 100 },
listEmpty: { flex: 1 },
cardWrapper: { flex: 1 },
cardWrapperLeft: { marginRight: 6 },
// FAB
fab: {
position: 'absolute',
right: 20,
bottom: 30,
shadowColor: Colors.accent,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.4,
shadowRadius: 12,
shadowOpacity: 0.45,
shadowRadius: 14,
elevation: 8,
},
fabGradient: {
Expand Down
Loading
Loading