diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx
index 5182a81..469e934 100644
--- a/app/(tabs)/_layout.tsx
+++ b/app/(tabs)/_layout.tsx
@@ -38,7 +38,7 @@ export default function TabLayout() {
elevation: 0,
shadowOpacity: 0,
},
- tabBarActiveTintColor: theme.colors.tabActive,
+ tabBarActiveTintColor: theme.colors.primary,
tabBarInactiveTintColor: theme.colors.tabInactive,
tabBarShowLabel: true,
tabBarLabelStyle: {
diff --git a/app/(tabs)/social.tsx b/app/(tabs)/social.tsx
deleted file mode 100644
index 38a2075..0000000
--- a/app/(tabs)/social.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-// app/(tabs)/social.tsx
-import { View } from 'react-native';
-import { Text } from '@/components/ui/text';
-import { Button } from '@/components/ui/button';
-import { Bell } from 'lucide-react-native';
-import Header from '@/components/Header';
-import { TabScreen } from '@/components/layout/TabScreen';
-
-export default function SocialScreen() {
- return (
-
- console.log('Open notifications')}
- >
-
-
-
-
-
- }
- />
-
- Social Screen
-
-
- );
-}
\ No newline at end of file
diff --git a/app/(tabs)/social/_layout.tsx b/app/(tabs)/social/_layout.tsx
new file mode 100644
index 0000000..21cfe7f
--- /dev/null
+++ b/app/(tabs)/social/_layout.tsx
@@ -0,0 +1,63 @@
+// app/(tabs)/social/_layout.tsx
+import React from 'react';
+import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
+import FollowingScreen from './following';
+import PowerScreen from './powr';
+import GlobalScreen from './global';
+import Header from '@/components/Header';
+import { useTheme } from '@react-navigation/native';
+import type { CustomTheme } from '@/lib/theme';
+import { TabScreen } from '@/components/layout/TabScreen';
+
+const Tab = createMaterialTopTabNavigator();
+
+export default function SocialLayout() {
+ const theme = useTheme() as CustomTheme;
+
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/(tabs)/social/following.tsx b/app/(tabs)/social/following.tsx
new file mode 100644
index 0000000..2068d52
--- /dev/null
+++ b/app/(tabs)/social/following.tsx
@@ -0,0 +1,90 @@
+// app/(tabs)/social/following.tsx
+import React from 'react';
+import { View, ScrollView, RefreshControl } from 'react-native';
+import { Text } from '@/components/ui/text';
+import { Button } from '@/components/ui/button';
+import SocialPost from '@/components/social/SocialPost';
+import { useNDKCurrentUser } from '@/lib/hooks/useNDK';
+import NostrLoginPrompt from '@/components/social/NostrLoginPrompt';
+import EmptyFeed from '@/components/social/EmptyFeed';
+
+// Sample mock data for posts
+const MOCK_POSTS = [
+ {
+ id: '1',
+ author: {
+ name: 'Jane Fitness',
+ handle: 'janefitness',
+ avatar: 'https://randomuser.me/api/portraits/women/32.jpg',
+ pubkey: 'npub1q8s7vw...'
+ },
+ content: 'Just crushed this leg workout! New PR on squat ๐ช #fitness #legday',
+ createdAt: new Date(Date.now() - 3600000 * 2), // 2 hours ago
+ metrics: {
+ likes: 24,
+ comments: 5,
+ reposts: 3
+ },
+ workout: {
+ title: 'Leg Day Destroyer',
+ exercises: ['Squats', 'Lunges', 'Leg Press'],
+ duration: 45
+ }
+ },
+ {
+ id: '2',
+ author: {
+ name: 'Mark Strong',
+ handle: 'markstrong',
+ avatar: 'https://randomuser.me/api/portraits/men/45.jpg',
+ pubkey: 'npub1z92r3...'
+ },
+ content: 'Morning cardio session complete! 5K run in 22 minutes. Starting the day right! #running #cardio',
+ createdAt: new Date(Date.now() - 3600000 * 5), // 5 hours ago
+ metrics: {
+ likes: 18,
+ comments: 2,
+ reposts: 1
+ },
+ workout: {
+ title: 'Morning Cardio',
+ exercises: ['Running'],
+ duration: 22
+ }
+ }
+];
+
+export default function FollowingScreen() {
+ const { isAuthenticated } = useNDKCurrentUser();
+ const [refreshing, setRefreshing] = React.useState(false);
+ const [posts, setPosts] = React.useState(MOCK_POSTS);
+
+ const onRefresh = React.useCallback(() => {
+ setRefreshing(true);
+ // Simulate fetch - in a real app, this would be a call to load posts
+ setTimeout(() => {
+ setRefreshing(false);
+ }, 1500);
+ }, []);
+
+ if (!isAuthenticated) {
+ return ;
+ }
+
+ if (posts.length === 0) {
+ return ;
+ }
+
+ return (
+
+ }
+ >
+ {posts.map(post => (
+
+ ))}
+
+ );
+}
\ No newline at end of file
diff --git a/app/(tabs)/social/global.tsx b/app/(tabs)/social/global.tsx
new file mode 100644
index 0000000..d97c719
--- /dev/null
+++ b/app/(tabs)/social/global.tsx
@@ -0,0 +1,114 @@
+// app/(tabs)/social/global.tsx
+import React from 'react';
+import { View, ScrollView, RefreshControl } from 'react-native';
+import { Text } from '@/components/ui/text';
+import SocialPost from '@/components/social/SocialPost';
+
+// Sample mock data for global feed - more diverse content
+const GLOBAL_POSTS = [
+ {
+ id: '1',
+ author: {
+ name: 'Strength Coach',
+ handle: 'strengthcoach',
+ avatar: 'https://randomuser.me/api/portraits/men/32.jpg',
+ pubkey: 'npub1q8s7vw...'
+ },
+ content: 'Form tip: When squatting, make sure your knees track in line with your toes. This helps protect your knees and ensures proper muscle engagement. #squatform #technique',
+ createdAt: new Date(Date.now() - 3600000 * 3), // 3 hours ago
+ metrics: {
+ likes: 132,
+ comments: 28,
+ reposts: 45
+ }
+ },
+ {
+ id: '2',
+ author: {
+ name: 'Marathon Runner',
+ handle: 'marathoner',
+ avatar: 'https://randomuser.me/api/portraits/women/28.jpg',
+ pubkey: 'npub1z92r3...'
+ },
+ content: 'Just finished my 10th marathon this year! Boston Marathon was an amazing experience. Thanks for all the support! #marathon #running #endurance',
+ createdAt: new Date(Date.now() - 3600000 * 14), // 14 hours ago
+ metrics: {
+ likes: 214,
+ comments: 38,
+ reposts: 22
+ },
+ workout: {
+ title: 'Boston Marathon',
+ exercises: ['Running'],
+ duration: 218 // 3:38 marathon
+ }
+ },
+ {
+ id: '3',
+ author: {
+ name: 'PowerLifter',
+ handle: 'liftsheavy',
+ avatar: 'https://randomuser.me/api/portraits/men/85.jpg',
+ pubkey: 'npub1xne8q...'
+ },
+ content: 'NEW PR ALERT! ๐ช Just hit 500lbs on deadlift after 3 years of consistent training. Proof that patience and consistency always win. #powerlifting #deadlift #pr',
+ createdAt: new Date(Date.now() - 3600000 * 36), // 36 hours ago
+ metrics: {
+ likes: 347,
+ comments: 72,
+ reposts: 41
+ },
+ workout: {
+ title: 'Deadlift Day',
+ exercises: ['Deadlifts', 'Back Accessories'],
+ duration: 65
+ }
+ },
+ {
+ id: '4',
+ author: {
+ name: 'Yoga Master',
+ handle: 'yogalife',
+ avatar: 'https://randomuser.me/api/portraits/women/50.jpg',
+ pubkey: 'npub1r72df...'
+ },
+ content: 'Morning yoga flow to start the day centered and grounded. Remember that flexibility isn\'t just physical - it\'s mental too. #yoga #morningroutine #wellness',
+ createdAt: new Date(Date.now() - 3600000 * 48), // 2 days ago
+ metrics: {
+ likes: 183,
+ comments: 12,
+ reposts: 25
+ },
+ workout: {
+ title: 'Morning Yoga Flow',
+ exercises: ['Yoga'],
+ duration: 30
+ }
+ }
+];
+
+export default function GlobalScreen() {
+ const [refreshing, setRefreshing] = React.useState(false);
+ const [posts, setPosts] = React.useState(GLOBAL_POSTS);
+
+ const onRefresh = React.useCallback(() => {
+ setRefreshing(true);
+ // Simulate fetch
+ setTimeout(() => {
+ setRefreshing(false);
+ }, 1500);
+ }, []);
+
+ return (
+
+ }
+ >
+ {posts.map(post => (
+
+ ))}
+
+ );
+}
\ No newline at end of file
diff --git a/app/(tabs)/social/powr.tsx b/app/(tabs)/social/powr.tsx
new file mode 100644
index 0000000..295d2ba
--- /dev/null
+++ b/app/(tabs)/social/powr.tsx
@@ -0,0 +1,105 @@
+// app/(tabs)/social/powr.tsx
+import React from 'react';
+import { View, ScrollView, RefreshControl } from 'react-native';
+import { Text } from '@/components/ui/text';
+import SocialPost from '@/components/social/SocialPost';
+import { Zap } from 'lucide-react-native';
+
+// Sample mock data for posts from POWR team/recommendations
+const POWR_POSTS = [
+ {
+ id: '1',
+ author: {
+ name: 'POWR Team',
+ handle: 'powrteam',
+ avatar: 'https://i.pravatar.cc/150?img=12',
+ pubkey: 'npub1q8s7vw...',
+ verified: true
+ },
+ content: 'Welcome to the new social feed in POWR! Share your workouts, follow friends and get inspired by the global fitness community. #powrapp',
+ createdAt: new Date(Date.now() - 3600000 * 48), // 2 days ago
+ metrics: {
+ likes: 158,
+ comments: 42,
+ reposts: 27
+ },
+ featured: true
+ },
+ {
+ id: '2',
+ author: {
+ name: 'Sarah Trainer',
+ handle: 'sarahfitness',
+ avatar: 'https://randomuser.me/api/portraits/women/44.jpg',
+ pubkey: 'npub1z92r3...'
+ },
+ content: 'Just released my new 30-day strength program! Check it out in my profile and let me know what you think. #strengthtraining #30daychallenge',
+ createdAt: new Date(Date.now() - 3600000 * 24), // 1 day ago
+ metrics: {
+ likes: 84,
+ comments: 15,
+ reposts: 12
+ },
+ workout: {
+ title: '30-Day Strength Builder',
+ exercises: ['Full Program'],
+ isProgramPreview: true
+ }
+ },
+ {
+ id: '3',
+ author: {
+ name: 'POWR Team',
+ handle: 'powrteam',
+ avatar: 'https://i.pravatar.cc/150?img=12',
+ pubkey: 'npub1q8s7vw...',
+ verified: true
+ },
+ content: 'New features alert! You can now track your rest periods automatically and share your PRs directly to your feed. Update to the latest version to try it out!',
+ createdAt: new Date(Date.now() - 3600000 * 72), // 3 days ago
+ metrics: {
+ likes: 207,
+ comments: 31,
+ reposts: 45
+ },
+ featured: true
+ }
+];
+
+export default function PowerScreen() {
+ const [refreshing, setRefreshing] = React.useState(false);
+ const [posts, setPosts] = React.useState(POWR_POSTS);
+
+ const onRefresh = React.useCallback(() => {
+ setRefreshing(true);
+ // Simulate fetch
+ setTimeout(() => {
+ setRefreshing(false);
+ }, 1500);
+ }, []);
+
+ return (
+
+ }
+ >
+ {/* POWR Welcome Section */}
+
+
+
+ POWR Community
+
+
+ Official updates, featured content, and community highlights from the POWR team.
+
+
+
+ {/* Posts */}
+ {posts.map(post => (
+
+ ))}
+
+ );
+}
\ No newline at end of file
diff --git a/app/(workout)/create.tsx b/app/(workout)/create.tsx
index 0990379..c7ec420 100644
--- a/app/(workout)/create.tsx
+++ b/app/(workout)/create.tsx
@@ -1,6 +1,6 @@
// app/(workout)/create.tsx
import React, { useState, useEffect } from 'react';
-import { View, ScrollView, StyleSheet, TextInput } from 'react-native';
+import { View, ScrollView, StyleSheet } from 'react-native';
import { router, useNavigation } from 'expo-router';
import { TabScreen } from '@/components/layout/TabScreen';
import { Text } from '@/components/ui/text';
@@ -16,7 +16,7 @@ import {
AlertDialogCancel
} from '@/components/ui/alert-dialog';
import { useWorkoutStore } from '@/stores/workoutStore';
-import { Plus, Pause, Play, MoreHorizontal, CheckCircle2, Dumbbell, ChevronLeft } from 'lucide-react-native';
+import { Plus, Pause, Play, MoreHorizontal, Dumbbell, ChevronLeft } from 'lucide-react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import EditableText from '@/components/EditableText';
import { cn } from '@/lib/utils';
@@ -25,6 +25,7 @@ import { WorkoutSet } from '@/types/workout';
import { formatTime } from '@/utils/formatTime';
import { ParamListBase } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
+import SetInput from '@/components/workout/SetInput';
// Define styles outside of component
const styles = StyleSheet.create({
@@ -116,19 +117,6 @@ export default function CreateWorkoutScreen() {
updateSet(exerciseIndex, exercise.sets.length, newSet);
};
- // Handler for completing a set
- const handleCompleteSet = (exerciseIndex: number, setIndex: number) => {
- if (!activeWorkout) return;
-
- const exercise = activeWorkout.exercises[exerciseIndex];
- const set = exercise.sets[setIndex];
-
- updateSet(exerciseIndex, setIndex, {
- ...set,
- isCompleted: !set.isCompleted
- });
- };
-
// Show empty state when no workout is active
if (!activeWorkout) {
return (
@@ -280,7 +268,7 @@ export default function CreateWorkoutScreen() {
>
{/* Exercise Header */}
-
+
{exercise.title}
);
}
\ No newline at end of file
diff --git a/docs/design/nostr-exercise-nip.md b/docs/design/nostr-exercise-nip.md
index 8cc9432..4ceb744 100644
--- a/docs/design/nostr-exercise-nip.md
+++ b/docs/design/nostr-exercise-nip.md
@@ -6,6 +6,22 @@ This specification defines workout events for fitness tracking. These workout ev
## Event Kinds
+### Event Kind Selection Rationale
+
+The event kinds in this NIP follow Nostr protocol conventions:
+
+- **Exercise and Workout Templates** (33401, 33402) use parameterized replaceable event kinds (30000+) because:
+ - They represent content that may be updated or improved over time
+ - The author may want to replace previous versions with improved ones
+ - They need the `d` parameter to distinguish between different templates by the same author
+ - Multiple versions shouldn't accumulate in clients' storage
+
+- **Workout Records** (1301) use a standard event kind (0-9999) because:
+ - They represent a chronological feed of activity that shouldn't replace previous records
+ - Each workout is a unique occurrence that adds to a user's history
+ - Users publish multiple records over time, creating a timeline
+ - They're conceptually similar to notes (kind 1) but with structured fitness data
+
### Exercise Template (kind: 33401)
Defines reusable exercise definitions. These should remain public to enable discovery and sharing. The `content` field contains detailed form instructions and notes.
@@ -37,7 +53,7 @@ Defines a complete workout plan. The `content` field contains workout notes and
* `rest_between_rounds` - Rest time between rounds in seconds
* `t` - Hashtags for categorization
-### Workout Record (kind: 33403)
+### Workout Record (kind: 1301)
Records a completed workout session. The `content` field contains notes about the workout.
#### Required Tags
@@ -52,6 +68,7 @@ Records a completed workout session. The `content` field contains notes about th
#### Optional Tags
* `rounds_completed` - Number of rounds completed
* `interval` - Duration of each exercise portion in seconds (for timed workouts)
+* `template` - Reference to the workout template used, if any. Format: ["template", "33402::", ""]
* `pr` - Personal Record achieved during workout. Format: "kind:pubkey:d-tag,metric,value". Used to track when a user achieves their best performance for a given exercise and metric (e.g., heaviest weight lifted, most reps completed, fastest time)
* `t` - Hashtags for categorization
@@ -154,7 +171,7 @@ Sets where technical failure was reached before completing prescribed reps. Thes
### Circuit Workout Record
```json
{
- "kind": 33403,
+ "kind": 1301,
"content": "Completed first round as prescribed. Second round showed form deterioration on deadlifts.",
"tags": [
["d", "workout-20250128"],