From 7f8a7641319f9ea516426a062c9c46075ff6aa24 Mon Sep 17 00:00:00 2001 From: tech08mag Date: Wed, 26 Aug 2026 22:15:28 +0200 Subject: [PATCH] refactor: update component styles and layouts --- .opencode/architekt.md | 17 ++ .opencode/config.json | 13 ++ .opencode/deployment.md | 17 ++ .opencode/docs.md | 17 ++ .opencode/hardware_engineer.md | 17 ++ .opencode/manager.md | 17 ++ .opencode/project-summary.md | 98 +++++++++++ .opencode/software_engineer.md | 17 ++ .opencode/tester.md | 17 ++ .opencode/vla.md | 17 ++ .../src/components/AnalogClock.tsx | 47 +++--- .../src/components/AssigneeSelector.tsx | 74 ++++++--- .../src/components/CategoryEditorModal.tsx | 77 ++++++--- .../src/components/CategoryFilter.tsx | 56 ++++++- .../src/components/CategorySelector.tsx | 89 +++++++--- .../components/ColorPickerInput.native.tsx | 15 +- .../src/components/ColorWheel.native.tsx | 106 ++++++++---- carry-your-live/src/components/ColorWheel.tsx | 2 +- .../src/components/ColorWheel.web.tsx | 60 +++++-- .../src/components/DateTimeInput.native.tsx | 2 +- .../src/components/DateTimeInput.web.tsx | 8 +- .../src/components/DateTimePicker.tsx | 76 +++++++-- .../src/components/DescriptionInput.tsx | 63 ++++---- .../src/components/FloatingActionButton.tsx | 9 +- .../src/components/ForgotPasswordModal.tsx | 51 ++++-- .../src/components/FormButtons.tsx | 27 +++- .../src/components/FriendsModal.tsx | 152 +++++++++++++----- carry-your-live/src/components/Header.tsx | 6 +- carry-your-live/src/components/LegalModal.tsx | 9 +- carry-your-live/src/components/ListItem.tsx | 89 ---------- 30 files changed, 917 insertions(+), 348 deletions(-) create mode 100644 .opencode/architekt.md create mode 100644 .opencode/config.json create mode 100644 .opencode/deployment.md create mode 100644 .opencode/docs.md create mode 100644 .opencode/hardware_engineer.md create mode 100644 .opencode/manager.md create mode 100644 .opencode/project-summary.md create mode 100644 .opencode/software_engineer.md create mode 100644 .opencode/tester.md create mode 100644 .opencode/vla.md 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 ( - + @@ -209,4 +243,4 @@ const styles = StyleSheet.create({ fontWeight: '500', fontFamily: 'monospace', }, -}); \ No newline at end of file +}); diff --git a/carry-your-live/src/components/DateTimeInput.native.tsx b/carry-your-live/src/components/DateTimeInput.native.tsx index 87f1048..d5df809 100644 --- a/carry-your-live/src/components/DateTimeInput.native.tsx +++ b/carry-your-live/src/components/DateTimeInput.native.tsx @@ -48,4 +48,4 @@ const styles = StyleSheet.create({ width: 0, height: 0, }, -}); \ No newline at end of file +}); diff --git a/carry-your-live/src/components/DateTimeInput.web.tsx b/carry-your-live/src/components/DateTimeInput.web.tsx index bc57735..f03f855 100644 --- a/carry-your-live/src/components/DateTimeInput.web.tsx +++ b/carry-your-live/src/components/DateTimeInput.web.tsx @@ -109,7 +109,11 @@ export default function WebDateTimeInput({ > Clear - + 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