diff --git a/.opencode/architekt.md b/.opencode/architekt.md
new file mode 100644
index 0000000..7bcd2fd
--- /dev/null
+++ b/.opencode/architekt.md
@@ -0,0 +1,17 @@
+# Architekt Agent
+
+## Role
+System architecture design, technical strategy, and scalability planning.
+
+## Responsibilities
+- High-level system design
+- Technology stack selection
+- Performance optimization
+- Architecture documentation
+- Trade-off analysis
+
+## Key Metrics
+- System scalability
+- Performance benchmarks
+- Technical debt ratio
+- Architecture compliance
\ No newline at end of file
diff --git a/.opencode/config.json b/.opencode/config.json
new file mode 100644
index 0000000..c70ed7f
--- /dev/null
+++ b/.opencode/config.json
@@ -0,0 +1,13 @@
+{
+ "agents": {
+ "manager": ".opencode/manager.md",
+ "architekt": ".opencode/architekt.md",
+ "software_engineer": ".opencode/software_engineer.md",
+ "hardware_engineer": ".opencode/hardware_engineer.md",
+ "tester": ".opencode/tester.md",
+ "docs": ".opencode/docs.md",
+ "deployment": ".opencode/deployment.md",
+ "vla": ".opencode/vla.md"
+ },
+ "subagent_depth": 999999
+}
\ No newline at end of file
diff --git a/.opencode/deployment.md b/.opencode/deployment.md
new file mode 100644
index 0000000..ae79274
--- /dev/null
+++ b/.opencode/deployment.md
@@ -0,0 +1,17 @@
+# Deployment Agent
+
+## Role
+CI/CD, infrastructure, and release management.
+
+## Responsibilities
+- CI/CD pipeline management
+- Infrastructure as code
+- Release orchestration
+- Environment management
+- Monitoring and alerting
+
+## Key Metrics
+- Deployment frequency
+- Lead time for changes
+- Mean time to recovery
+- Change success rate
\ No newline at end of file
diff --git a/.opencode/docs.md b/.opencode/docs.md
new file mode 100644
index 0000000..def4029
--- /dev/null
+++ b/.opencode/docs.md
@@ -0,0 +1,17 @@
+# Docs Agent
+
+## Role
+Technical documentation, API docs, and knowledge management.
+
+## Responsibilities
+- API documentation
+- User guides
+- Architecture docs
+- Change logs
+- Knowledge base maintenance
+
+## Key Metrics
+- Documentation completeness
+- Update frequency
+- User satisfaction
+- Searchability
\ No newline at end of file
diff --git a/.opencode/hardware_engineer.md b/.opencode/hardware_engineer.md
new file mode 100644
index 0000000..f3c6e1a
--- /dev/null
+++ b/.opencode/hardware_engineer.md
@@ -0,0 +1,17 @@
+# Hardware Engineer Agent
+
+## Role
+Hardware integration, device management, and embedded systems.
+
+## Responsibilities
+- Hardware selection and integration
+- Device driver development
+- Embedded firmware
+- Prototyping and testing
+- Hardware-software interface
+
+## Key Metrics
+- Hardware compatibility rate
+- Power consumption
+- Latency measurements
+- Component reliability
\ No newline at end of file
diff --git a/.opencode/manager.md b/.opencode/manager.md
new file mode 100644
index 0000000..4bfc2ef
--- /dev/null
+++ b/.opencode/manager.md
@@ -0,0 +1,17 @@
+# Manager Agent
+
+## Role
+Project management, task coordination, prioritization, and team oversight.
+
+## Responsibilities
+- Task decomposition and assignment
+- Progress tracking and status reporting
+- Risk identification and mitigation
+- Resource allocation
+- Stakeholder communication
+
+## Key Metrics
+- Sprint velocity
+- Task completion rate
+- Blockers resolved
+- Timeline adherence
\ No newline at end of file
diff --git a/.opencode/project-summary.md b/.opencode/project-summary.md
new file mode 100644
index 0000000..9df136e
--- /dev/null
+++ b/.opencode/project-summary.md
@@ -0,0 +1,98 @@
+# Carry Your Live - Project Summary
+
+## Overview
+A minimalist, offline-first task management app built with Expo, React Native, and WatermelonDB. Features three primary screens: To-Do List, Add New Task, and Calendar with bottom navigation.
+
+## Core Features
+- **Offline-First Architecture**: Full CRUD operations work offline with WatermelonDB + SQLite
+- **Auto-Sync**: Background synchronization with custom Postgres API when online
+- **Cross-Platform**: iOS, Android, and Web from single codebase
+
+## Tech Stack
+| Layer | Technology |
+|-------|------------|
+| Framework | Expo (React Native) |
+| Language | TypeScript |
+| Navigation | Expo Router (file-based) |
+| Database | WatermelonDB + SQLite |
+| State | WatermelonDB (reactive queries, no Redux needed) |
+| Forms | React Hook Form + Zod validation |
+| UI Components | React Native Paper + Custom SVG icons |
+| Animations | React Native Reanimated |
+| Date/Time | @react-native-community/datetimepicker + date-fns |
+| Build | Gradle (local APK) |
+| CI/CD | Gitea Actions |
+
+## Project Structure
+```
+carry-your-live/
+├ app/
+│ ├── _layout.tsx # Root layout + DatabaseProvider
+│ ├── (tabs)/
+│ │ ├── _layout.tsx # Bottom tab navigator
+│ │ ├── index.tsx # Tasks screen
+│ │ ├── calendar.tsx # Calendar screen
+│ │ └── settings.tsx # Settings screen
+│ └── add-task.tsx # Add Task screen
+├ src/
+│ ├── components/ # Reusable UI components
+│ ├── database/
+│ │ ├── schema.ts # WatermelonDB schema
+│ │ ├── index.ts # Database instance + collections
+│ │ └── sync.ts # Custom API sync adapter
+│ ├── models/
+│ │ ├── Task.ts
+│ │ ├── Category.ts
+│ │ └── Subtask.ts
+│ ├── hooks/
+│ │ ├── useDatabase.tsx # Database context & providers
+│ │ └── useTasks.tsx # Task queries with reactive state
+│ ├── constants/
+│ │ └── index.ts # Categories, priorities, colors
+│ └── types/
+│ └── index.ts # TypeScript interfaces
+```
+
+## Data Models
+- **Task**: id, title, description, categoryId, priority, completed, dueDate, dueTime, createdAt, updatedAt, subtasks
+- **Category**: id, name, color, order
+- **Subtask**: id, taskId, title, completed, order, createdAt, updatedAt
+
+## Default Categories
+| Name | Color | Order |
+|------|-------|-------|
+| Work | #E53935 (Red) | 0 |
+| Personal | #43A047 (Green) | 1 |
+| School | #1E88E5 (Blue) | 2 |
+| Shopping | #FB8C00 (Orange) | 3 |
+| Health | #8E24AA (Purple) | 4 |
+| Finance | #FDD835 (Yellow) | 5 |
+
+## Priority Colors
+| Priority | Color |
+|----------|-------|
+| None | #9E9E9E |
+| Low | #43A047 |
+| Medium | #FB8C00 |
+| High | #E53935 |
+| Critical | #B71C1C |
+
+## Sync API
+Custom API at `http://localhost:3000/api` (configurable in `src/database/sync.ts`):
+- **Pull**: GET /sync?since={timestamp} - returns categories, tasks, subtasks, timestamp
+- **Push**: POST /sync/push - body with changes { categories, tasks, subtasks } + lastPulledAt
+
+## Getting Started
+```bash
+npm install
+npx expo start
+# Platform-specific:
+npx expo start --ios
+npx expo start --android
+npx expo start --web
+```
+
+## Build Commands
+- `cd android && ./gradlew assembleDebug` - Debug APK
+- `cd android && ./gradlew assembleRelease` - Release APK
+- `npm run desktop` - Electron desktop app
\ No newline at end of file
diff --git a/.opencode/software_engineer.md b/.opencode/software_engineer.md
new file mode 100644
index 0000000..9053e69
--- /dev/null
+++ b/.opencode/software_engineer.md
@@ -0,0 +1,17 @@
+# Software Engineer Agent
+
+## Role
+Code implementation, feature development, and software quality.
+
+## Responsibilities
+- Feature implementation
+- Code reviews
+- Bug fixing
+- Refactoring
+- Unit testing
+
+## Key Metrics
+- Code quality score
+- Test coverage
+- Pull request throughput
+- Bug resolution time
\ No newline at end of file
diff --git a/.opencode/tester.md b/.opencode/tester.md
new file mode 100644
index 0000000..795e03f
--- /dev/null
+++ b/.opencode/tester.md
@@ -0,0 +1,17 @@
+# Tester Agent
+
+## Role
+Quality assurance, test automation, and test strategy.
+
+## Responsibilities
+- Test case design
+- Automated test execution
+- Regression testing
+- Performance testing
+- Test coverage analysis
+
+## Key Metrics
+- Test pass rate
+- Defect detection rate
+- Test execution time
+- Bug leakage
\ No newline at end of file
diff --git a/.opencode/vla.md b/.opencode/vla.md
new file mode 100644
index 0000000..e59d818
--- /dev/null
+++ b/.opencode/vla.md
@@ -0,0 +1,17 @@
+# VLA Agent
+
+## Role
+Vision-Language-Action models and multimodal understanding.
+
+## Responsibilities
+- VLA model development
+- Multimodal perception
+- Action prediction
+- Policy learning
+- Sim-to-real transfer
+
+## Key Metrics
+- Task success rate
+- Sample efficiency
+- Generalization ability
+- Simulation-to-real transfer rate
\ No newline at end of file
diff --git a/carry-your-live/src/components/AnalogClock.tsx b/carry-your-live/src/components/AnalogClock.tsx
index 40a8dbd..bfc19c7 100644
--- a/carry-your-live/src/components/AnalogClock.tsx
+++ b/carry-your-live/src/components/AnalogClock.tsx
@@ -49,9 +49,9 @@ export function AnalogClock({ value, is24Hour = false, onChange }: AnalogClockPr
let hour: number;
if (is24Hour) {
const inner = distance < INNER_NUM_RADIUS + 14;
- hour = inner ? base + 12 : (base === 0 ? 12 : base);
+ hour = inner ? base + 12 : base === 0 ? 12 : base;
} else {
- hour = isPM ? base + 12 : (base === 0 ? 0 : base);
+ hour = isPM ? base + 12 : base === 0 ? 0 : base;
}
emit(hour, minutes);
setMode('minute');
@@ -135,23 +135,24 @@ export function AnalogClock({ value, is24Hour = false, onChange }: AnalogClockPr
);
});
- const minuteDots = mode === 'minute'
- ? Array.from({ length: 12 }, (_, i) => {
- const angle = (i * 30 * Math.PI) / 180;
- const x = CENTER + RADIUS * 0.82 * Math.sin(angle);
- const y = CENTER - RADIUS * 0.82 * Math.cos(angle);
- const active = Math.round(minutes / 5) % 12 === i;
- return (
-
- );
- })
- : null;
+ const minuteDots =
+ mode === 'minute'
+ ? Array.from({ length: 12 }, (_, i) => {
+ const angle = (i * 30 * Math.PI) / 180;
+ const x = CENTER + RADIUS * 0.82 * Math.sin(angle);
+ const y = CENTER - RADIUS * 0.82 * Math.cos(angle);
+ const active = Math.round(minutes / 5) % 12 === i;
+ return (
+
+ );
+ })
+ : null;
return (
@@ -181,7 +182,7 @@ export function AnalogClock({ value, is24Hour = false, onChange }: AnalogClockPr
{!is24Hour && (
- {(['AM', 'PM'] as const).map((period) => {
+ {(['AM', 'PM'] as const).map(period => {
const active = isPM === (period === 'PM');
return (
true}
onMoveShouldSetResponder={() => true}
- onResponderGrant={(e) => {
+ onResponderGrant={e => {
movedRef.current = false;
onLocation(e, false);
}}
- onResponderMove={(e) => {
+ onResponderMove={e => {
movedRef.current = true;
onLocation(e, true);
}}
- onResponderRelease={(e) => {
+ onResponderRelease={e => {
if (!movedRef.current) {
onLocation(e, false);
}
diff --git a/carry-your-live/src/components/AssigneeSelector.tsx b/carry-your-live/src/components/AssigneeSelector.tsx
index a3609db..832da54 100644
--- a/carry-your-live/src/components/AssigneeSelector.tsx
+++ b/carry-your-live/src/components/AssigneeSelector.tsx
@@ -1,5 +1,15 @@
import React from 'react';
-import { View, Text, TouchableOpacity, StyleSheet, Modal, FlatList, ActivityIndicator, TextInput, KeyboardAvoidingView } from 'react-native';
+import {
+ View,
+ Text,
+ TouchableOpacity,
+ StyleSheet,
+ Modal,
+ FlatList,
+ ActivityIndicator,
+ TextInput,
+ KeyboardAvoidingView,
+} from 'react-native';
import { useSettings } from '@/theme';
import { useFriends } from '@/hooks/useFriends';
@@ -82,19 +92,14 @@ export function AssigneeSelector({ value, onChange, disabled = false }: Assignee
👤
Assignee
-
+
{selectedFriend ? selectedFriend.username : 'Unassigned'}
{value && !disabled && (
{
+ onPress={e => {
e.stopPropagation();
handleClear();
}}
@@ -105,7 +110,12 @@ export function AssigneeSelector({ value, onChange, disabled = false }: Assignee
)}
- setShowModal(false)}>
+ setShowModal(false)}
+ >
Assign Task
- setShowModal(false)} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
+ setShowModal(false)}
+ hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
+ >
✕
@@ -122,7 +135,10 @@ export function AssigneeSelector({ value, onChange, disabled = false }: Assignee
handleClear()}
>
@@ -143,7 +159,9 @@ export function AssigneeSelector({ value, onChange, disabled = false }: Assignee
) : friends.length === 0 ? (
- No friends yet. Add friends to assign tasks.
+
+ No friends yet. Add friends to assign tasks.
+
) : (
handleSelectFriend(item.id)}
>
👤
- {item.username}
+
+ {item.username}
+
{value === item.id && (
✓
)}
@@ -172,7 +195,14 @@ export function AssigneeSelector({ value, onChange, disabled = false }: Assignee
➕
- {item.username}
- Add as friend & assign
+
+ {item.username}
+
+
+ Add as friend & assign
+
)}
/>
) : searchQuery.length >= 2 && searchResults.length === 0 && !searching ? (
No users found
) : searchQuery.length < 2 ? (
- Type at least 2 characters to search
+
+ Type at least 2 characters to search
+
) : null}
@@ -348,4 +384,4 @@ const styles = StyleSheet.create({
borderRadius: 10,
borderWidth: 1,
},
-});
\ No newline at end of file
+});
diff --git a/carry-your-live/src/components/CategoryEditorModal.tsx b/carry-your-live/src/components/CategoryEditorModal.tsx
index 79ba2f7..82fcbe1 100644
--- a/carry-your-live/src/components/CategoryEditorModal.tsx
+++ b/carry-your-live/src/components/CategoryEditorModal.tsx
@@ -1,5 +1,17 @@
import React, { useState, useEffect } from 'react';
-import { Modal, View, Text, StyleSheet, TextInput, TouchableOpacity, ScrollView, KeyboardAvoidingView, Platform, Pressable, useWindowDimensions } from 'react-native';
+import {
+ Modal,
+ View,
+ Text,
+ StyleSheet,
+ TextInput,
+ TouchableOpacity,
+ ScrollView,
+ KeyboardAvoidingView,
+ Platform,
+ Pressable,
+ useWindowDimensions,
+} from 'react-native';
import Category from '@/models/Category';
import { useSettings } from '@/theme';
import { CATEGORY_COLORS } from '@/constants';
@@ -15,7 +27,12 @@ interface CategoryEditorModalProps {
onClose: () => void;
}
-export function CategoryEditorModal({ visible, category, categoryCount, onClose }: CategoryEditorModalProps) {
+export function CategoryEditorModal({
+ visible,
+ category,
+ categoryCount,
+ onClose,
+}: CategoryEditorModalProps) {
const { theme } = useSettings();
const [name, setName] = useState('');
const [color, setColor] = useState(CATEGORY_COLORS[0]);
@@ -64,15 +81,8 @@ export function CategoryEditorModal({ visible, category, categoryCount, onClose
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={0}
>
-
- {}}
- >
+
+ {}}>
{category ? 'Edit Tag' : 'New Tag'}
-
+
@@ -94,7 +115,11 @@ export function CategoryEditorModal({ visible, category, categoryCount, onClose
Color
- {CATEGORY_COLORS.map((c) => (
+ {CATEGORY_COLORS.map(c => (
{color === c && (
)}
@@ -139,13 +171,22 @@ export function CategoryEditorModal({ visible, category, categoryCount, onClose
disabled={!canDelete}
activeOpacity={0.7}
>
-
+
Delete
)}
- {categories.map((category) => (
+ {categories.map(category => (
))}
router.push('/settings')}
activeOpacity={0.7}
accessibilityRole="button"
@@ -80,10 +83,22 @@ export function CategoryFilter({ selected, onSelect }: CategoryFilterProps) {
hitSlop={{ top: 8, bottom: 8, left: 4, right: 4 }}
>
Manage
@@ -104,7 +119,16 @@ interface CategoryButtonProps {
scaleAnim: Animated.Value;
}
-function CategoryButton({ color, selected, onPress, onPressIn, onPressOut, theme, isAllCategory, scaleAnim }: CategoryButtonProps) {
+function CategoryButton({
+ color,
+ selected,
+ onPress,
+ onPressIn,
+ onPressOut,
+ theme,
+ isAllCategory,
+ scaleAnim,
+}: CategoryButtonProps) {
const animatedStyle = {
transform: [{ scale: scaleAnim }],
shadowColor: selected ? theme.accent : '#000',
@@ -130,8 +154,24 @@ function CategoryButton({ color, selected, onPress, onPressIn, onPressOut, theme
>
{isAllCategory ? (
) : null}
@@ -187,4 +227,4 @@ const styles = StyleSheet.create({
fontSize: 12,
fontWeight: '600',
},
-});
\ No newline at end of file
+});
diff --git a/carry-your-live/src/components/CategorySelector.tsx b/carry-your-live/src/components/CategorySelector.tsx
index c3d35ee..6cf9978 100644
--- a/carry-your-live/src/components/CategorySelector.tsx
+++ b/carry-your-live/src/components/CategorySelector.tsx
@@ -1,5 +1,14 @@
import React, { useState } from 'react';
-import { View, Text, StyleSheet, TouchableOpacity, ScrollView, Modal, Pressable, KeyboardAvoidingView } from 'react-native';
+import {
+ View,
+ Text,
+ StyleSheet,
+ TouchableOpacity,
+ ScrollView,
+ Modal,
+ Pressable,
+ KeyboardAvoidingView,
+} from 'react-native';
import { useCategories } from '@/hooks/useDatabase';
import { useSettings } from '@/theme';
import Svg, { Path } from 'react-native-svg';
@@ -16,7 +25,7 @@ export function CategorySelector({ value, onChange, error }: CategorySelectorPro
const [showModal, setShowModal] = useState(false);
const selected = new Set(value);
- const selectedCategories = categories.filter((c) => selected.has(c.id));
+ const selectedCategories = categories.filter(c => selected.has(c.id));
const toggle = (id: string) => {
const next = new Set(selected);
@@ -33,7 +42,7 @@ export function CategorySelector({ value, onChange, error }: CategorySelectorPro
? 'None'
: selectedCategories
.slice(0, 2)
- .map((c) => c.name)
+ .map(c => c.name)
.join(', ') + (selectedCategories.length > 2 ? ` +${selectedCategories.length - 2}` : '');
return (
@@ -53,10 +62,18 @@ export function CategorySelector({ value, onChange, error }: CategorySelectorPro
{selectedCategories.length > 0 ? (
- {selectedCategories.slice(0, 3).map((c) => (
-
+ {selectedCategories.slice(0, 3).map(c => (
+
- {c.name}
+
+ {c.name}
+
))}
@@ -65,12 +82,24 @@ export function CategorySelector({ value, onChange, error }: CategorySelectorPro
)}
{error && {error}}
- setShowModal(false)}>
+ setShowModal(false)}
+ >
Select Tags
{ onChange([]); setShowModal(false); }}
+ onPress={() => {
+ onChange([]);
+ setShowModal(false);
+ }}
activeOpacity={0.7}
accessibilityRole="button"
accessibilityLabel="Clear all tags"
@@ -102,7 +134,7 @@ export function CategorySelector({ value, onChange, error }: CategorySelectorPro
A task can have multiple tags. Selected: {selected.size}
- {categories.map((category) => {
+ {categories.map(category => {
const isSelected = selected.has(category.id);
return (
toggle(category.id)}
activeOpacity={0.8}
@@ -118,17 +154,32 @@ export function CategorySelector({ value, onChange, error }: CategorySelectorPro
accessibilityLabel={`Tag ${category.name}`}
accessibilityState={{ checked: isSelected }}
>
-
-
+
+
{category.name}
{isSelected && (
)}
@@ -262,4 +313,4 @@ const styles = StyleSheet.create({
fontWeight: '500',
flex: 1,
},
-});
\ No newline at end of file
+});
diff --git a/carry-your-live/src/components/ColorPickerInput.native.tsx b/carry-your-live/src/components/ColorPickerInput.native.tsx
index 4648951..91a2c95 100644
--- a/carry-your-live/src/components/ColorPickerInput.native.tsx
+++ b/carry-your-live/src/components/ColorPickerInput.native.tsx
@@ -20,16 +20,21 @@ export default function NativeColorPickerInput({ value, onChange }: ColorPickerI
const valid = isValidHex(hex);
const handleChange = (raw: string) => {
- const cleaned = raw.replace(/[^0-9a-fA-F]/g, '').slice(0, 6).toUpperCase();
+ const cleaned = raw
+ .replace(/[^0-9a-fA-F]/g, '')
+ .slice(0, 6)
+ .toUpperCase();
onChange(`#${cleaned}`);
};
return (
-
+
#{/**/}
Math.round((n + m) * 255).toString(16).padStart(2, '0');
+ let r = 0,
+ g = 0,
+ b = 0;
+ if (h < 60) {
+ r = c;
+ g = x;
+ b = 0;
+ } else if (h < 120) {
+ r = x;
+ g = c;
+ b = 0;
+ } else if (h < 180) {
+ r = 0;
+ g = c;
+ b = x;
+ } else if (h < 240) {
+ r = 0;
+ g = x;
+ b = c;
+ } else if (h < 300) {
+ r = x;
+ g = 0;
+ b = c;
+ } else {
+ r = c;
+ g = 0;
+ b = x;
+ }
+ const toHex = (n: number) =>
+ Math.round((n + m) * 255)
+ .toString(16)
+ .padStart(2, '0');
return `#${toHex(r)}${toHex(g)}${toHex(b)}`.toUpperCase();
}
@@ -84,35 +108,49 @@ export default function ColorWheel({ color, onChange }: ColorWheelProps) {
setValue(1 - distance / radius);
};
- const wheelPanResponder = React.useMemo(() => PanResponder.create({
- onStartShouldSetPanResponder: () => true,
- onMoveShouldSetPanResponder: () => true,
- onPanResponderGrant: (event) => {
- updateFromWheel(event.nativeEvent.locationX, event.nativeEvent.locationY);
- },
- onPanResponderMove: (event) => {
- updateFromWheel(event.nativeEvent.locationX, event.nativeEvent.locationY);
- },
- }), []);
+ const wheelPanResponder = React.useMemo(
+ () =>
+ PanResponder.create({
+ onStartShouldSetPanResponder: () => true,
+ onMoveShouldSetPanResponder: () => true,
+ onPanResponderGrant: event => {
+ updateFromWheel(event.nativeEvent.locationX, event.nativeEvent.locationY);
+ },
+ onPanResponderMove: event => {
+ updateFromWheel(event.nativeEvent.locationX, event.nativeEvent.locationY);
+ },
+ }),
+ []
+ );
const setHueFromLocation = (locationX: number) => {
setHue(Math.min(360, Math.max(0, (locationX / WHEEL_SIZE) * 360)));
};
- const huePanResponder = React.useMemo(() => PanResponder.create({
- onStartShouldSetPanResponder: () => true,
- onMoveShouldSetPanResponder: () => true,
- onPanResponderGrant: (event) => {
- setHueFromLocation(event.nativeEvent.locationX);
- },
- onPanResponderMove: (event) => {
- setHueFromLocation(event.nativeEvent.locationX);
- },
- }), []);
+ const huePanResponder = React.useMemo(
+ () =>
+ PanResponder.create({
+ onStartShouldSetPanResponder: () => true,
+ onMoveShouldSetPanResponder: () => true,
+ onPanResponderGrant: event => {
+ setHueFromLocation(event.nativeEvent.locationX);
+ },
+ onPanResponderMove: event => {
+ setHueFromLocation(event.nativeEvent.locationX);
+ },
+ }),
+ []
+ );
- const thumbX = ((WHEEL_SIZE - THUMB_SIZE) / 2) * saturation * Math.cos((hue - 180) * Math.PI / 180) + (WHEEL_SIZE / 2) - (THUMB_SIZE / 2);
- const thumbY = ((WHEEL_SIZE - THUMB_SIZE) / 2) * saturation * Math.sin((hue - 180) * Math.PI / 180) + (WHEEL_SIZE / 2) - (THUMB_SIZE / 2);
- const hueThumbX = (hue / 360) * WHEEL_SIZE - (THUMB_SIZE / 2);
+ const thumbX =
+ ((WHEEL_SIZE - THUMB_SIZE) / 2) * saturation * Math.cos(((hue - 180) * Math.PI) / 180) +
+ WHEEL_SIZE / 2 -
+ THUMB_SIZE / 2;
+ const thumbY =
+ ((WHEEL_SIZE - THUMB_SIZE) / 2) * saturation * Math.sin(((hue - 180) * Math.PI) / 180) +
+ WHEEL_SIZE / 2 -
+ THUMB_SIZE / 2;
+ const hueThumbX = (hue / 360) * WHEEL_SIZE - THUMB_SIZE / 2;
return (
@@ -180,7 +218,9 @@ export default function ColorWheel({ color, onChange }: ColorWheelProps) {
- {hsvToHex(hue, saturation, value)}
+
+ {hsvToHex(hue, saturation, value)}
+
);
@@ -215,4 +255,4 @@ const styles = StyleSheet.create({
fontWeight: '500',
fontFamily: 'monospace',
},
-});
\ No newline at end of file
+});
diff --git a/carry-your-live/src/components/ColorWheel.tsx b/carry-your-live/src/components/ColorWheel.tsx
index 596c075..4d47062 100644
--- a/carry-your-live/src/components/ColorWheel.tsx
+++ b/carry-your-live/src/components/ColorWheel.tsx
@@ -1 +1 @@
-export { default } from './ColorWheel.native';
\ No newline at end of file
+export { default } from './ColorWheel.native';
diff --git a/carry-your-live/src/components/ColorWheel.web.tsx b/carry-your-live/src/components/ColorWheel.web.tsx
index 5ca6d4c..a094b15 100644
--- a/carry-your-live/src/components/ColorWheel.web.tsx
+++ b/carry-your-live/src/components/ColorWheel.web.tsx
@@ -32,14 +32,38 @@ function hsvToHex(h: number, s: number, v: number) {
const c = v * s;
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
const m = v - c;
- let r = 0, g = 0, b = 0;
- if (h < 60) { r = c; g = x; b = 0; }
- else if (h < 120) { r = x; g = c; b = 0; }
- else if (h < 180) { r = 0; g = c; b = x; }
- else if (h < 240) { r = 0; g = x; b = c; }
- else if (h < 300) { r = x; g = 0; b = c; }
- else { r = c; g = 0; b = x; }
- const toHex = (n: number) => Math.round((n + m) * 255).toString(16).padStart(2, '0');
+ let r = 0,
+ g = 0,
+ b = 0;
+ if (h < 60) {
+ r = c;
+ g = x;
+ b = 0;
+ } else if (h < 120) {
+ r = x;
+ g = c;
+ b = 0;
+ } else if (h < 180) {
+ r = 0;
+ g = c;
+ b = x;
+ } else if (h < 240) {
+ r = 0;
+ g = x;
+ b = c;
+ } else if (h < 300) {
+ r = x;
+ g = 0;
+ b = c;
+ } else {
+ r = c;
+ g = 0;
+ b = x;
+ }
+ const toHex = (n: number) =>
+ Math.round((n + m) * 255)
+ .toString(16)
+ .padStart(2, '0');
return `#${toHex(r)}${toHex(g)}${toHex(b)}`.toUpperCase();
}
@@ -108,15 +132,25 @@ export default function ColorWheel({ color, onChange }: ColorWheelProps) {
handleMove(e.nativeEvent);
};
- const thumbX = ((WHEEL_SIZE - THUMB_SIZE) / 2) * saturation * Math.cos((hue - 180) * Math.PI / 180) + (WHEEL_SIZE / 2) - (THUMB_SIZE / 2);
- const thumbY = ((WHEEL_SIZE - THUMB_SIZE) / 2) * saturation * Math.sin((hue - 180) * Math.PI / 180) + (WHEEL_SIZE / 2) - (THUMB_SIZE / 2);
- const hueThumbX = (hue / 360) * WHEEL_SIZE - (THUMB_SIZE / 2);
+ const thumbX =
+ ((WHEEL_SIZE - THUMB_SIZE) / 2) * saturation * Math.cos(((hue - 180) * Math.PI) / 180) +
+ WHEEL_SIZE / 2 -
+ THUMB_SIZE / 2;
+ const thumbY =
+ ((WHEEL_SIZE - THUMB_SIZE) / 2) * saturation * Math.sin(((hue - 180) * Math.PI) / 180) +
+ WHEEL_SIZE / 2 -
+ THUMB_SIZE / 2;
+ const hueThumbX = (hue / 360) * WHEEL_SIZE - THUMB_SIZE / 2;
const currentColor = hsvToHex(hue, saturation, value);
return (
-
-
+
Done
@@ -166,4 +170,4 @@ const styles = StyleSheet.create({
fontSize: 15,
fontWeight: '600',
},
-});
\ No newline at end of file
+});
diff --git a/carry-your-live/src/components/DateTimePicker.tsx b/carry-your-live/src/components/DateTimePicker.tsx
index c6fdfc8..95c9a3d 100644
--- a/carry-your-live/src/components/DateTimePicker.tsx
+++ b/carry-your-live/src/components/DateTimePicker.tsx
@@ -49,14 +49,30 @@ export function DateTimePickerComponent({ control }: DateTimePickerComponentProp
styles.pickerButton,
{ backgroundColor: theme.inputBg, borderColor: theme.borderStrong },
field.value && { borderColor: theme.accent, backgroundColor: theme.accentSoft },
- fieldName === 'endTime' && endTimeInvalid && { borderColor: theme.dangerStrong, backgroundColor: theme.accentSoft },
+ fieldName === 'endTime' &&
+ endTimeInvalid && {
+ borderColor: theme.dangerStrong,
+ backgroundColor: theme.accentSoft,
+ },
]}
onPress={onPress}
activeOpacity={0.8}
>
-
-
+
+
-
+
) : null}
@@ -142,7 +163,12 @@ export function DateTimePickerComponent({ control }: DateTimePickerComponentProp
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
>
-
+
) : null}
@@ -151,7 +177,10 @@ export function DateTimePickerComponent({ control }: DateTimePickerComponentProp
}}
/>
setPicker('date')}
activeOpacity={0.8}
accessibilityLabel="Open calendar"
@@ -184,7 +213,7 @@ export function DateTimePickerComponent({ control }: DateTimePickerComponentProp
thumbColor={field.value ? theme.accent : theme.textMuted}
ios_backgroundColor={theme.border}
value={field.value ?? false}
- onValueChange={(value) => {
+ onValueChange={value => {
field.onChange(value);
if (value) {
timeRef.current?.('');
@@ -203,10 +232,27 @@ export function DateTimePickerComponent({ control }: DateTimePickerComponentProp
{endTimeInvalid && (
-
+
-
-
+
+
{endTime === startTime
@@ -235,12 +281,8 @@ export function DateTimePickerComponent({ control }: DateTimePickerComponentProp
{
+ initialTime={picker === 'endTime' ? endTime || startTime : startTime}
+ onConfirm={time => {
if (picker === 'endTime') {
setEndTimeValue(time);
} else {
@@ -334,4 +376,4 @@ const styles = StyleSheet.create({
fontWeight: '500',
flex: 1,
},
-});
\ No newline at end of file
+});
diff --git a/carry-your-live/src/components/DescriptionInput.tsx b/carry-your-live/src/components/DescriptionInput.tsx
index 8d8e75e..58aae06 100644
--- a/carry-your-live/src/components/DescriptionInput.tsx
+++ b/carry-your-live/src/components/DescriptionInput.tsx
@@ -1,5 +1,5 @@
import React, { useMemo } from 'react';
-import { View, Text, StyleSheet, TextInput , TextInputProps } from 'react-native';
+import { View, Text, StyleSheet, TextInput, TextInputProps } from 'react-native';
import { useSettings, type ThemeColors } from '@/theme';
interface DescriptionInputProps extends TextInputProps {
@@ -36,33 +36,34 @@ export function DescriptionInput({ error, ...props }: DescriptionInputProps) {
);
}
-const createStyles = (theme: ThemeColors) => StyleSheet.create({
- container: {
- gap: 5,
- },
- label: {
- fontSize: 13,
- fontWeight: '600',
- },
- input: {
- minHeight: 88,
- padding: 14,
- borderRadius: 10,
- borderWidth: 1,
- fontSize: 14,
- },
- inputError: {
- borderColor: theme.dangerStrong,
- borderWidth: 1.5,
- },
- charCount: {
- fontSize: 10,
- textAlign: 'right',
- marginTop: -3,
- },
- errorText: {
- fontSize: 11,
- color: theme.dangerStrong,
- marginLeft: 4,
- },
-});
\ No newline at end of file
+const createStyles = (theme: ThemeColors) =>
+ StyleSheet.create({
+ container: {
+ gap: 5,
+ },
+ label: {
+ fontSize: 13,
+ fontWeight: '600',
+ },
+ input: {
+ minHeight: 88,
+ padding: 14,
+ borderRadius: 10,
+ borderWidth: 1,
+ fontSize: 14,
+ },
+ inputError: {
+ borderColor: theme.dangerStrong,
+ borderWidth: 1.5,
+ },
+ charCount: {
+ fontSize: 10,
+ textAlign: 'right',
+ marginTop: -3,
+ },
+ errorText: {
+ fontSize: 11,
+ color: theme.dangerStrong,
+ marginLeft: 4,
+ },
+ });
diff --git a/carry-your-live/src/components/FloatingActionButton.tsx b/carry-your-live/src/components/FloatingActionButton.tsx
index 1778a41..a6543af 100644
--- a/carry-your-live/src/components/FloatingActionButton.tsx
+++ b/carry-your-live/src/components/FloatingActionButton.tsx
@@ -47,12 +47,7 @@ export function FloatingActionButton() {
});
return (
-
+
setStep('enterResetCode') },
- ]);
+ Alert.alert(
+ 'Reset Token (Dev Mode)',
+ `Your reset token is: ${result.resetToken}\n\nEnter this token on the next screen.`,
+ [{ text: 'OK', onPress: () => setStep('enterResetCode') }]
+ );
} else {
- Alert.alert('Reset Email Sent', 'If the account exists, a password reset link has been sent.', [
- { text: 'OK', onPress: () => setStep('enterResetCode') },
- ]);
+ Alert.alert(
+ 'Reset Email Sent',
+ 'If the account exists, a password reset link has been sent.',
+ [{ text: 'OK', onPress: () => setStep('enterResetCode') }]
+ );
}
} else {
setError('Failed to request password reset');
@@ -112,7 +116,10 @@ export function ForgotPasswordModal({ visible, onClose }: ForgotPasswordModalPro
Enter your username to receive a password reset link.
) : (
- Send Reset Link
+
+ Send Reset Link
+
)}
@@ -142,7 +151,10 @@ export function ForgotPasswordModal({ visible, onClose }: ForgotPasswordModalPro
Enter the reset token and your new password.
) : (
- Reset Password
+
+ Reset Password
+
)}
@@ -184,10 +204,7 @@ export function ForgotPasswordModal({ visible, onClose }: ForgotPasswordModalPro
return (
-
+
@@ -276,4 +293,4 @@ const styles = StyleSheet.create({
fontSize: 13,
fontWeight: '500',
},
-});
\ No newline at end of file
+});
diff --git a/carry-your-live/src/components/FormButtons.tsx b/carry-your-live/src/components/FormButtons.tsx
index f13a7db..e7eee93 100644
--- a/carry-your-live/src/components/FormButtons.tsx
+++ b/carry-your-live/src/components/FormButtons.tsx
@@ -13,7 +13,10 @@ interface FormButtonsProps {
export function FormButtons({ onSubmit, submitLabel = 'Submit' }: FormButtonsProps) {
const router = useRouter();
const { theme } = useSettings();
- const { handleSubmit, formState: { isSubmitting } } = useFormContext();
+ const {
+ handleSubmit,
+ formState: { isSubmitting },
+ } = useFormContext();
const insets = useSafeAreaInsets();
const cancel = () => {
@@ -25,9 +28,21 @@ export function FormButtons({ onSubmit, submitLabel = 'Submit' }: FormButtonsPro
};
return (
-
+
Cancel
([]);
const [searching, setSearching] = useState(false);
- const [selectedTab, setSelectedTab] = useState<'friends' | 'incoming' | 'outgoing' | 'add'>('friends');
+ const [selectedTab, setSelectedTab] = useState<'friends' | 'incoming' | 'outgoing' | 'add'>(
+ 'friends'
+ );
const searchKey = `${selectedTab}:${searchQuery}`;
const [prevSearchKey, setPrevSearchKey] = useState(searchKey);
@@ -41,7 +53,11 @@ export function FriendsModal({ visible, onClose }: FriendsModalProps) {
setSearching(true);
try {
const results = await searchUsers(searchQuery);
- setSearchResults(results.filter(r => !friends.some(f => f.id === r.id) && !outgoing.some(o => o.id === r.id)));
+ setSearchResults(
+ results.filter(
+ r => !friends.some(f => f.id === r.id) && !outgoing.some(o => o.id === r.id)
+ )
+ );
} catch (err) {
console.error('Search failed:', err);
} finally {
@@ -71,13 +87,17 @@ export function FriendsModal({ visible, onClose }: FriendsModalProps) {
const handleRemove = async (friendId: string) => {
Alert.alert('Remove Friend', 'Are you sure you want to remove this friend?', [
{ text: 'Cancel', style: 'cancel' },
- { text: 'Remove', style: 'destructive', onPress: async () => {
- try {
- await removeFriend(friendId);
- } catch (err) {
- Alert.alert('Error', err instanceof Error ? err.message : 'Failed to remove friend');
- }
- }},
+ {
+ text: 'Remove',
+ style: 'destructive',
+ onPress: async () => {
+ try {
+ await removeFriend(friendId);
+ } catch (err) {
+ Alert.alert('Error', err instanceof Error ? err.message : 'Failed to remove friend');
+ }
+ },
+ },
]);
};
@@ -98,7 +118,7 @@ export function FriendsModal({ visible, onClose }: FriendsModalProps) {
- {(['friends', 'incoming', 'outgoing', 'add'] as const).map((tab) => (
+ {(['friends', 'incoming', 'outgoing', 'add'] as const).map(tab => (
{tab.charAt(0).toUpperCase() + tab.slice(1)}
{tab === 'friends' && friends.length > 0 && (
- {friends.length}
+
+ {friends.length}
+
)}
{tab === 'incoming' && incoming.length > 0 && (
- {incoming.length}
+
+ {incoming.length}
+
)}
@@ -135,7 +159,9 @@ export function FriendsModal({ visible, onClose }: FriendsModalProps) {
) : friends.length === 0 ? (
No friends yet
- {'Tap "Add" to find friends by username'}
+
+ {'Tap "Add" to find friends by username'}
+
) : (
f.id}
renderItem={({ item }) => (
- {item.username}
+
+ {item.username}
+
handleRemove(item.id)}
>
- Remove
+
+ Remove
+
)}
@@ -165,7 +198,9 @@ export function FriendsModal({ visible, onClose }: FriendsModalProps) {
) : incoming.length === 0 ? (
- No incoming requests
+
+ No incoming requests
+
) : (
r.requestId}
renderItem={({ item }) => (
- {item.username}
+
+ {item.username}
+
handleAccept(item.requestId)}
>
- Accept
+
+ Accept
+
handleDecline(item.requestId)}
>
- Decline
+
+ Decline
+
@@ -203,7 +251,9 @@ export function FriendsModal({ visible, onClose }: FriendsModalProps) {
) : outgoing.length === 0 ? (
- No outgoing requests
+
+ No outgoing requests
+
) : (
r.requestId}
renderItem={({ item }) => (
- {item.username}
+
+ {item.username}
+
Pending
handleDecline(item.requestId)}
>
- Cancel
+
+ Cancel
+
)}
@@ -230,7 +291,14 @@ export function FriendsModal({ visible, onClose }: FriendsModalProps) {
u.id}
renderItem={({ item }) => (
- {item.username}
+
+ {item.username}
+
sendRequest(item.username)
- .then(() => {
- setSearchQuery('');
- setSearchResults([]);
- setSelectedTab('friends');
- })
- .catch(err => Alert.alert('Error', err instanceof Error ? err.message : 'Failed to send request'))
+ onPress={() =>
+ sendRequest(item.username)
+ .then(() => {
+ setSearchQuery('');
+ setSearchResults([]);
+ setSelectedTab('friends');
+ })
+ .catch(err =>
+ Alert.alert(
+ 'Error',
+ err instanceof Error ? err.message : 'Failed to send request'
+ )
+ )
}
>
Add
@@ -272,7 +348,9 @@ export function FriendsModal({ visible, onClose }: FriendsModalProps) {
/>
) : searchQuery.length < 2 ? (
- Type at least 2 characters to search
+
+ Type at least 2 characters to search
+
) : null}
@@ -433,4 +511,4 @@ const styles = StyleSheet.create({
borderRadius: 12,
borderWidth: 1,
},
-});
\ No newline at end of file
+});
diff --git a/carry-your-live/src/components/Header.tsx b/carry-your-live/src/components/Header.tsx
index daed94f..bf16eab 100644
--- a/carry-your-live/src/components/Header.tsx
+++ b/carry-your-live/src/components/Header.tsx
@@ -20,7 +20,9 @@ export function Header({ title, showLogo, rightAction }: HeaderProps) {
✓
)}
- {title}
+
+ {title}
+
{rightAction}
@@ -67,4 +69,4 @@ const styles = StyleSheet.create({
width: 32,
alignItems: 'flex-end',
},
-});
\ No newline at end of file
+});
diff --git a/carry-your-live/src/components/LegalModal.tsx b/carry-your-live/src/components/LegalModal.tsx
index 0a6ad6d..d5f3891 100644
--- a/carry-your-live/src/components/LegalModal.tsx
+++ b/carry-your-live/src/components/LegalModal.tsx
@@ -136,7 +136,12 @@ export function LegalModal({ visible, type, onClose }: LegalModalProps) {
{title}
-
+
@@ -193,4 +198,4 @@ const styles = StyleSheet.create({
fontSize: 14,
lineHeight: 22,
},
-});
\ No newline at end of file
+});
diff --git a/carry-your-live/src/components/ListItem.tsx b/carry-your-live/src/components/ListItem.tsx
index ada97b0..e69de29 100644
--- a/carry-your-live/src/components/ListItem.tsx
+++ b/carry-your-live/src/components/ListItem.tsx
@@ -1,89 +0,0 @@
-import React from 'react';
-import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
-import { useSettings } from '@/theme';
-import Svg, { Path } from 'react-native-svg';
-
-interface ListItemProps {
- title: string;
- subtitle?: string;
- leftElement?: React.ReactNode;
- rightElement?: React.ReactNode;
- onPress?: () => void;
- showChevron?: boolean;
-}
-
-export function ListItem({ title, subtitle, leftElement, rightElement, onPress, showChevron }: ListItemProps) {
- const { theme } = useSettings();
- const isInteractive = onPress || showChevron;
-
- return (
-
- {leftElement && {leftElement}}
-
- {title}
- {subtitle && {subtitle}}
-
-
- {rightElement}
- {showChevron && !rightElement && (
-
-
-
- )}
-
-
- );
-}
-
-const styles = StyleSheet.create({
- container: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'space-between',
- paddingVertical: 14,
- paddingHorizontal: 16,
- borderRadius: 12,
- borderWidth: 1,
- marginBottom: 8,
- },
- interactive: {},
- leftElement: {
- marginRight: 12,
- },
- leftContent: {
- flex: 1,
- gap: 2,
- },
- title: {
- fontSize: 16,
- fontWeight: '500',
- },
- subtitle: {
- fontSize: 13,
- },
- rightContent: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 12,
- },
-});
\ No newline at end of file