From 4b6e87c97969e428947963fd17539fb8c5aefa69 Mon Sep 17 00:00:00 2001 From: tech08mag Date: Fri, 7 Aug 2026 22:30:00 +0200 Subject: [PATCH] docker compose for backend +frontend --- carry-your-live/.dockerignore | 10 + carry-your-live/Dockerfile | 18 ++ carry-your-live/app/(tabs)/calendar.tsx | 4 +- carry-your-live/app/(tabs)/settings.tsx | 28 ++- carry-your-live/app/add-task.tsx | 2 +- carry-your-live/app/subtask-detail.tsx | 2 +- carry-your-live/app/task-detail.tsx | 2 +- carry-your-live/electron/main.js | 44 ++++ carry-your-live/nginx.conf | 21 ++ .../src/components/CategoryEditorModal.tsx | 5 +- .../src/components/CategoryFilter.tsx | 87 +------ .../src/components/CategorySelector.tsx | 11 +- .../src/components/ColorWheel.native.tsx | 205 +++++++++++++++++ carry-your-live/src/components/ColorWheel.tsx | 1 + .../src/components/ColorWheel.web.tsx | 216 ++++++++++++++++++ .../src/components/FloatingActionButton.tsx | 3 + .../src/components/FormButtons.tsx | 6 + carry-your-live/src/components/Header.tsx | 7 +- carry-your-live/src/components/ListItem.tsx | 4 + .../src/components/OptionPickerModal.tsx | 5 + .../src/components/PrioritySelector.tsx | 5 +- .../src/components/QuickAddBar.tsx | 6 +- .../src/components/ReminderSelector.tsx | 4 + .../src/components/RepeatSelector.tsx | 20 +- .../src/components/SubtaskItem.tsx | 6 +- carry-your-live/src/components/TaskItem.tsx | 49 ++-- carry-your-live/src/components/TaskList.tsx | 202 +++------------- .../src/components/TaskNameInput.tsx | 2 + .../src/components/TaskOverflowMenu.tsx | 2 + carry-your-live/src/hooks/useTasks.tsx | 72 ++---- carry-your-live/src/theme.tsx | 10 + docker-compose.yml | 48 ++++ todo.md | 68 +++--- 33 files changed, 793 insertions(+), 382 deletions(-) create mode 100644 carry-your-live/.dockerignore create mode 100644 carry-your-live/Dockerfile create mode 100644 carry-your-live/electron/main.js create mode 100644 carry-your-live/nginx.conf create mode 100644 carry-your-live/src/components/ColorWheel.native.tsx create mode 100644 carry-your-live/src/components/ColorWheel.tsx create mode 100644 carry-your-live/src/components/ColorWheel.web.tsx create mode 100644 docker-compose.yml diff --git a/carry-your-live/.dockerignore b/carry-your-live/.dockerignore new file mode 100644 index 0000000..b230635 --- /dev/null +++ b/carry-your-live/.dockerignore @@ -0,0 +1,10 @@ +node_modules +dist +.expo +.git +*.log +.DS_Store +android +ios +coverage +*.local \ No newline at end of file diff --git a/carry-your-live/Dockerfile b/carry-your-live/Dockerfile new file mode 100644 index 0000000..edc3618 --- /dev/null +++ b/carry-your-live/Dockerfile @@ -0,0 +1,18 @@ +FROM node:20-alpine AS builder + +WORKDIR /app + +COPY package*.json ./ +RUN npm ci + +COPY . . +RUN npx expo export -p web + +FROM nginx:alpine AS runner + +COPY --from=builder /app/dist /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 8081 + +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/carry-your-live/app/(tabs)/calendar.tsx b/carry-your-live/app/(tabs)/calendar.tsx index 4bc56bc..3c760ad 100644 --- a/carry-your-live/app/(tabs)/calendar.tsx +++ b/carry-your-live/app/(tabs)/calendar.tsx @@ -30,7 +30,7 @@ export default function CalendarScreen() { const { theme } = useSettings(); const { collections } = useDatabase(); const categories = useCategories(); - const { modals, openTaskMenu } = useTaskModals(); + const { modals, openTaskEdit } = useTaskModals(); const [visibleMonth, setVisibleMonth] = useState(() => new Date()); const [selectedDate, setSelectedDate] = useState(() => new Date()); @@ -301,7 +301,7 @@ export default function CalendarScreen() { - openTaskMenu(task)} activeOpacity={0.7}> + openTaskEdit(task.id)} activeOpacity={0.7} accessibilityRole="button" accessibilityLabel={`Edit ${task.title}`}> diff --git a/carry-your-live/app/(tabs)/settings.tsx b/carry-your-live/app/(tabs)/settings.tsx index 591b25d..16c4981 100644 --- a/carry-your-live/app/(tabs)/settings.tsx +++ b/carry-your-live/app/(tabs)/settings.tsx @@ -19,9 +19,9 @@ import Svg, { Path } from 'react-native-svg'; import ColorWheel from '@/components/ColorWheel'; export default function SettingsScreen() { - const { theme, notifications, setNotifications, defaultCategoryId, setDefaultCategoryId, sortBy, setSortBy, reminderPreference, setReminderPreference, apiUrl, accentColor, setAccentColor } = useSettings(); + const { theme, notifications, setNotifications, defaultCategoryId, setDefaultCategoryId, sortBy, setSortBy, reminderPreference, setReminderPreference, apiUrl, accentColor, setAccentColor, todoAheadDays, setTodoAheadDays } = useSettings(); const categories = useCategories(); - const [picker, setPicker] = useState(null); + const [picker, setPicker] = useState(null); const [editingCategory, setEditingCategory] = useState(null); const [syncVisible, setSyncVisible] = useState(false); const [friendsVisible, setFriendsVisible] = useState(false); @@ -92,6 +92,10 @@ export default function SettingsScreen() { setReminderPreference((Array.isArray(value) ? value[0] : value) as typeof reminderPreference); }; + const handleTodoAheadDays = (value: string | string[]) => { + setTodoAheadDays(parseInt(Array.isArray(value) ? value[0] : value, 10)); + }; + const editorVisible = editingCategory !== null; const editorCategory = editingCategory === 'new' ? null : editingCategory; @@ -156,6 +160,12 @@ export default function SettingsScreen() { onPress={() => setPicker('sort')} showChevron /> + setPicker('todoAhead')} + showChevron + /> Appearance Accent color @@ -256,6 +266,15 @@ export default function SettingsScreen() { onClose={() => setPicker(null)} /> + setPicker(null)} + /> + ({ + value: String(i), + label: i === 0 ? 'Only today' : `${i} day${i === 1 ? '' : 's'}`, +})); + function formatSyncTime(timestamp: number): string { const seconds = Math.floor((Date.now() - timestamp) / 1000); if (seconds < 60) return 'just now'; diff --git a/carry-your-live/app/add-task.tsx b/carry-your-live/app/add-task.tsx index 13904b9..747f599 100644 --- a/carry-your-live/app/add-task.tsx +++ b/carry-your-live/app/add-task.tsx @@ -176,7 +176,7 @@ export default function AddTaskScreen() { { + win.show() + }) + + win.on('closed', () => { + app.quit() + }) +} + +app.whenReady().then(createWindow) + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit() + } +}) + +app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow() + } +}) \ No newline at end of file diff --git a/carry-your-live/nginx.conf b/carry-your-live/nginx.conf new file mode 100644 index 0000000..415e564 --- /dev/null +++ b/carry-your-live/nginx.conf @@ -0,0 +1,21 @@ +server { + listen 8081; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } + + location /api { + proxy_pass http://backend:3000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } +} \ 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 45d3dce..85bb332 100644 --- a/carry-your-live/src/components/CategoryEditorModal.tsx +++ b/carry-your-live/src/components/CategoryEditorModal.tsx @@ -68,7 +68,7 @@ export function CategoryEditorModal({ visible, category, categoryCount, onClose {category ? 'Edit Category' : 'New Category'} - + @@ -102,6 +102,9 @@ export function CategoryEditorModal({ visible, category, categoryCount, onClose ]} onPress={() => setColor(c)} activeOpacity={0.8} + accessibilityRole="radio" + accessibilityLabel={`Color ${c}`} + accessibilityState={{ selected: color === c }} > {color === c && ( diff --git a/carry-your-live/src/components/CategoryFilter.tsx b/carry-your-live/src/components/CategoryFilter.tsx index 79a33a4..8d65860 100644 --- a/carry-your-live/src/components/CategoryFilter.tsx +++ b/carry-your-live/src/components/CategoryFilter.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { View, Text, StyleSheet, ScrollView, TouchableOpacity, Animated, Easing } from 'react-native'; +import { View, Text, StyleSheet, ScrollView, TouchableOpacity } from 'react-native'; import { useUniqueCategories } from '@/hooks/useDatabase'; import { useSettings, ThemeColors } from '@/theme'; import Category from '@/models/Category'; @@ -33,9 +33,11 @@ export function CategoryFilter({ selected, onSelect }: CategoryFilterProps) { theme={theme} /> {categories.map((category) => ( - onSelect(category.id)} theme={theme} @@ -54,7 +56,7 @@ interface CategoryButtonProps { theme: ThemeColors; } -function CategoryButton({ id, name, color, selected, onPress, theme }: CategoryButtonProps) { +function CategoryButton({ name, color, selected, onPress, theme }: CategoryButtonProps) { return ( void; - theme: ThemeColors; -} - -function AnimatedCategoryButton({ category, selected, onPress, theme }: AnimatedCategoryButtonProps) { - const [scaleAnim] = React.useState(() => new Animated.Value(selected ? 1.05 : 1)); - const [borderWidthAnim] = React.useState(() => new Animated.Value(selected ? 2 : 1)); - const [shadowOpacityAnim] = React.useState(() => new Animated.Value(selected ? 0.15 : 0)); - - React.useEffect(() => { - Animated.timing(scaleAnim, { - toValue: selected ? 1.05 : 1, - duration: 150, - easing: Easing.out(Easing.cubic), - useNativeDriver: false, - }).start(); - - Animated.timing(borderWidthAnim, { - toValue: selected ? 2 : 1, - duration: 150, - useNativeDriver: false, - }).start(); - - Animated.timing(shadowOpacityAnim, { - toValue: selected ? 0.15 : 0, - duration: 150, - useNativeDriver: false, - }).start(); - }, [selected, scaleAnim, borderWidthAnim, shadowOpacityAnim]); - - const animatedStyle = { - transform: [{ scale: scaleAnim }], - borderWidth: borderWidthAnim, - shadowOpacity: shadowOpacityAnim, - }; - - return ( - - - - - {category.name} - - - - ); -} - const styles = StyleSheet.create({ scrollView: { paddingVertical: 0, @@ -169,16 +106,6 @@ const styles = StyleSheet.create({ borderWidth: 1, minWidth: 64, justifyContent: 'center', - }, - animatedButton: { - shadowColor: '#000', - shadowOffset: { width: 0, height: 1 }, - shadowRadius: 4, - elevation: 2, - }, - buttonInner: { - flexDirection: 'row', - alignItems: 'center', gap: 6, }, colorDot: { @@ -195,4 +122,4 @@ const styles = StyleSheet.create({ fontSize: 12, fontWeight: '500', }, -}); \ 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 5ce84e7..6504c2b 100644 --- a/carry-your-live/src/components/CategorySelector.tsx +++ b/carry-your-live/src/components/CategorySelector.tsx @@ -26,6 +26,9 @@ export function CategorySelector({ value, onChange, error }: CategorySelectorPro ]} onPress={() => setShowModal(true)} activeOpacity={0.8} + accessibilityRole="button" + accessibilityLabel="Select category" + accessibilityHint="Opens a list of categories to choose from" > @@ -48,7 +51,7 @@ export function CategorySelector({ value, onChange, error }: CategorySelectorPro Select Category - setShowModal(false)} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}> + setShowModal(false)} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }} accessibilityRole="button" accessibilityLabel="Close category picker"> @@ -61,6 +64,9 @@ export function CategorySelector({ value, onChange, error }: CategorySelectorPro ]} onPress={() => { onChange(''); setShowModal(false); }} activeOpacity={0.8} + accessibilityRole="radio" + accessibilityLabel="No category" + accessibilityState={{ selected: !value }} > { onChange(category.id); setShowModal(false); }} activeOpacity={0.8} + accessibilityRole="radio" + accessibilityLabel={`Category ${category.name}`} + accessibilityState={{ selected: value === category.id }} > ; + +const { width: SCREEN_WIDTH } = Dimensions.get('window'); +const WHEEL_SIZE = Math.min(SCREEN_WIDTH - 64, 280); +const THUMB_SIZE = 24; + +function hexToHsv(hex: string) { + const r = parseInt(hex.slice(1, 3), 16) / 255; + const g = parseInt(hex.slice(3, 5), 16) / 255; + const b = parseInt(hex.slice(5, 7), 16) / 255; + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + const delta = max - min; + let h = 0; + if (delta !== 0) { + if (max === r) h = ((g - b) / delta) % 6; + else if (max === g) h = (b - r) / delta + 2; + else h = (r - g) / delta + 4; + h = Math.round(h * 60); + if (h < 0) h += 360; + } + const s = max === 0 ? 0 : delta / max; + const v = max; + return { h, s, v }; +} + +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'); + return `#${toHex(r)}${toHex(g)}${toHex(b)}`.toUpperCase(); +} + +interface ColorWheelProps { + color: string; + onChange: (color: string) => void; +} + +export default function ColorWheel({ color, onChange }: ColorWheelProps) { + const { theme } = useSettings(); + const initialHsv = useMemo(() => hexToHsv(color), [color]); + const [hue, setHue] = useState(initialHsv.h); + const [saturation, setSaturation] = useState(initialHsv.s); + const [value, setValue] = useState(initialHsv.v); + const [prevColor, setPrevColor] = useState(color); + + if (prevColor !== color) { + setPrevColor(color); + setHue(initialHsv.h); + setSaturation(initialHsv.s); + setValue(initialHsv.v); + } + + useEffect(() => { + const newColor = hsvToHex(hue, saturation, value); + onChange(newColor); + }, [hue, saturation, value, onChange]); + + const wheelPanResponder = React.useMemo(() => PanResponder.create({ + onStartShouldSetPanResponder: () => true, + onMoveShouldSetPanResponder: () => true, + onPanResponderMove: (_, gesture) => { + const center = WHEEL_SIZE / 2; + const dx = gesture.moveX - center; + const dy = gesture.moveY - center; + const distance = Math.sqrt(dx * dx + dy * dy); + const radius = (WHEEL_SIZE - THUMB_SIZE) / 2; + if (distance > radius) return; + const angle = Math.atan2(dy, dx) * (180 / Math.PI); + let h = angle + 180; + if (h >= 360) h -= 360; + setHue(h); + setSaturation(distance / radius); + setValue(1 - distance / radius); + }, + }), []); + + const huePanResponder = React.useMemo(() => PanResponder.create({ + onStartShouldSetPanResponder: () => true, + onMoveShouldSetPanResponder: () => true, + onPanResponderMove: (_, gesture) => { + const h = (gesture.moveX / WHEEL_SIZE) * 360; + setHue(Math.min(360, Math.max(0, h))); + }, + }), []); + + 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 ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {hsvToHex(hue, saturation, value)} + + + ); +} + +const styles = StyleSheet.create({ + container: { + alignItems: 'center', + gap: 16, + }, + wheelContainer: { + position: 'relative', + }, + hueContainer: { + position: 'relative', + width: WHEEL_SIZE, + }, + previewContainer: { + flexDirection: 'row', + alignItems: 'center', + gap: 12, + }, + preview: { + width: 44, + height: 44, + borderRadius: 12, + borderWidth: 1, + borderColor: '#00000020', + }, + previewText: { + fontSize: 15, + 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 new file mode 100644 index 0000000..596c075 --- /dev/null +++ b/carry-your-live/src/components/ColorWheel.tsx @@ -0,0 +1 @@ +export { default } from './ColorWheel.native'; \ No newline at end of file diff --git a/carry-your-live/src/components/ColorWheel.web.tsx b/carry-your-live/src/components/ColorWheel.web.tsx new file mode 100644 index 0000000..91fc68e --- /dev/null +++ b/carry-your-live/src/components/ColorWheel.web.tsx @@ -0,0 +1,216 @@ +import React, { useState, useEffect, useMemo, useRef } from 'react'; +import { View, Text, StyleSheet } from 'react-native'; +import { useSettings } from '@/theme'; +import Svg, { Rect, Circle, Defs, LinearGradient, Stop } from 'react-native-svg'; + +const DefsElement = Defs as unknown as React.ComponentType<{ children?: React.ReactNode }>; + +const WHEEL_SIZE = 280; +const THUMB_SIZE = 24; + +function hexToHsv(hex: string) { + const r = parseInt(hex.slice(1, 3), 16) / 255; + const g = parseInt(hex.slice(3, 5), 16) / 255; + const b = parseInt(hex.slice(5, 7), 16) / 255; + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + const delta = max - min; + let h = 0; + if (delta !== 0) { + if (max === r) h = ((g - b) / delta) % 6; + else if (max === g) h = (b - r) / delta + 2; + else h = (r - g) / delta + 4; + h = Math.round(h * 60); + if (h < 0) h += 360; + } + const s = max === 0 ? 0 : delta / max; + const v = max; + return { h, s, v }; +} + +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'); + return `#${toHex(r)}${toHex(g)}${toHex(b)}`.toUpperCase(); +} + +interface ColorWheelProps { + color: string; + onChange: (color: string) => void; +} + +export default function ColorWheel({ color, onChange }: ColorWheelProps) { + const { theme } = useSettings(); + const initialHsv = useMemo(() => hexToHsv(color), [color]); + const [hue, setHue] = useState(initialHsv.h); + const [saturation, setSaturation] = useState(initialHsv.s); + const [value, setValue] = useState(initialHsv.v); + const [prevColor, setPrevColor] = useState(color); + const wheelRef = useRef(null); + const hueRef = useRef(null); + + if (prevColor !== color) { + setPrevColor(color); + setHue(initialHsv.h); + setSaturation(initialHsv.s); + setValue(initialHsv.v); + } + + useEffect(() => { + const newColor = hsvToHex(hue, saturation, value); + onChange(newColor); + }, [hue, saturation, value, onChange]); + + const handleWheelMouseDown = (e: React.MouseEvent) => { + const handleMove = (moveEvent: MouseEvent) => { + if (!wheelRef.current) return; + const rect = (wheelRef.current as unknown as HTMLElement).getBoundingClientRect(); + const center = WHEEL_SIZE / 2; + const dx = moveEvent.clientX - rect.left - center; + const dy = moveEvent.clientY - rect.top - center; + const distance = Math.sqrt(dx * dx + dy * dy); + const radius = (WHEEL_SIZE - THUMB_SIZE) / 2; + if (distance > radius) return; + const angle = Math.atan2(dy, dx) * (180 / Math.PI); + let h = angle + 180; + if (h >= 360) h -= 360; + setHue(h); + setSaturation(distance / radius); + setValue(1 - distance / radius); + }; + const handleUp = () => { + window.removeEventListener('mousemove', handleMove); + window.removeEventListener('mouseup', handleUp); + }; + window.addEventListener('mousemove', handleMove); + window.addEventListener('mouseup', handleUp); + handleMove(e.nativeEvent); + }; + + const handleHueMouseDown = (e: React.MouseEvent) => { + const handleMove = (moveEvent: MouseEvent) => { + if (!hueRef.current) return; + const rect = (hueRef.current as unknown as HTMLElement).getBoundingClientRect(); + const h = ((moveEvent.clientX - rect.left) / WHEEL_SIZE) * 360; + setHue(Math.min(360, Math.max(0, h))); + }; + const handleUp = () => { + window.removeEventListener('mousemove', handleMove); + window.removeEventListener('mouseup', handleUp); + }; + window.addEventListener('mousemove', handleMove); + window.addEventListener('mouseup', handleUp); + 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 currentColor = hsvToHex(hue, saturation, value); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {currentColor} + + + ); +} + +const styles = StyleSheet.create({ + container: { + alignItems: 'center', + gap: 16, + }, + wheelContainer: {}, + hueContainer: { + width: WHEEL_SIZE, + }, + previewContainer: { + flexDirection: 'row', + alignItems: 'center', + gap: 12, + }, + preview: { + width: 44, + height: 44, + borderRadius: 12, + borderWidth: 1, + borderColor: '#00000020', + }, + previewText: { + fontSize: 15, + fontWeight: '500', + fontFamily: 'monospace', + }, +}); \ No newline at end of file diff --git a/carry-your-live/src/components/FloatingActionButton.tsx b/carry-your-live/src/components/FloatingActionButton.tsx index 18ab7bd..81b6b19 100644 --- a/carry-your-live/src/components/FloatingActionButton.tsx +++ b/carry-your-live/src/components/FloatingActionButton.tsx @@ -57,6 +57,9 @@ export function FloatingActionButton() { onPressIn={handlePressIn} onPressOut={handlePressOut} activeOpacity={1} + accessibilityRole="button" + accessibilityLabel="Add new task" + hitSlop={12} > Cancel @@ -39,6 +42,9 @@ export function FormButtons({ onSubmit, submitLabel = 'Submit' }: FormButtonsPro onPress={handleSubmit(onSubmit)} disabled={isSubmitting} activeOpacity={0.8} + accessibilityRole="button" + accessibilityLabel={isSubmitting ? 'Saving' : submitLabel} + accessibilityState={{ disabled: isSubmitting, busy: isSubmitting }} > {isSubmitting ? 'Saving...' : submitLabel} diff --git a/carry-your-live/src/components/Header.tsx b/carry-your-live/src/components/Header.tsx index 403ad51..b12efa6 100644 --- a/carry-your-live/src/components/Header.tsx +++ b/carry-your-live/src/components/Header.tsx @@ -20,7 +20,7 @@ export function Header({ title, showLogo, rightAction }: HeaderProps) { )} - {title} + {title} {rightAction} @@ -61,8 +61,9 @@ const styles = StyleSheet.create({ fontSize: 20, fontWeight: '700', position: 'absolute', - left: '50%', - marginLeft: -30, + left: 0, + right: 0, + textAlign: 'center', }, spacer: { width: 32, diff --git a/carry-your-live/src/components/ListItem.tsx b/carry-your-live/src/components/ListItem.tsx index 70a79f3..742885f 100644 --- a/carry-your-live/src/components/ListItem.tsx +++ b/carry-your-live/src/components/ListItem.tsx @@ -26,6 +26,10 @@ export function ListItem({ title, subtitle, leftElement, rightElement, onPress, onPress={onPress} activeOpacity={0.7} disabled={!isInteractive} + accessibilityRole={isInteractive ? 'button' : 'none'} + accessibilityLabel={title} + accessibilityHint={subtitle ? subtitle : undefined} + accessibilityState={{ disabled: !isInteractive }} > {leftElement && {leftElement}} diff --git a/carry-your-live/src/components/OptionPickerModal.tsx b/carry-your-live/src/components/OptionPickerModal.tsx index 360a078..e8878df 100644 --- a/carry-your-live/src/components/OptionPickerModal.tsx +++ b/carry-your-live/src/components/OptionPickerModal.tsx @@ -53,6 +53,9 @@ export function OptionPickerModal({ visible, title, options, selectedValue, onSe } }} activeOpacity={0.7} + accessibilityRole={multiSelect ? 'checkbox' : 'button'} + accessibilityLabel={item.label} + accessibilityState={multiSelect ? { checked: selected } : { selected }} > {item.color && ( @@ -81,6 +84,8 @@ export function OptionPickerModal({ visible, title, options, selectedValue, onSe style={[styles.cancelButton, { borderColor: theme.borderStrong }]} onPress={onClose} activeOpacity={0.7} + accessibilityRole="button" + accessibilityLabel="Cancel" > Cancel diff --git a/carry-your-live/src/components/PrioritySelector.tsx b/carry-your-live/src/components/PrioritySelector.tsx index 4b0d417..4f2c7d3 100644 --- a/carry-your-live/src/components/PrioritySelector.tsx +++ b/carry-your-live/src/components/PrioritySelector.tsx @@ -23,7 +23,7 @@ export function PrioritySelector({ value, onChange }: PrioritySelectorProps) { return ( Priority - + {priorities.map((priority) => ( onChange(priority.value)} activeOpacity={0.8} + accessibilityRole="radio" + accessibilityLabel={`${priority.label} priority`} + accessibilityState={{ selected: value === priority.value }} > setShowPicker(true)} disabled={disabled} activeOpacity={0.8} + accessibilityRole="button" + accessibilityLabel="Reminders" + accessibilityHint="Opens a list of reminder options" + accessibilityState={{ disabled }} > - applyProfile(profile)} activeOpacity={0.8}> + applyProfile(profile)} activeOpacity={0.8} accessibilityRole="button" accessibilityLabel={`Apply repeat profile ${profile.name}`}> {profile.name} handleDeleteProfile(profile.id, profile.name)} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }} + accessibilityRole="button" + accessibilityLabel={`Delete repeat profile ${profile.name}`} > × @@ -144,7 +148,7 @@ export function RepeatSelector({ value, interval, days, onChange }: RepeatSelect )} - + {REPEAT_OPTIONS.map((option) => ( selectRepeat(option.value)} activeOpacity={0.8} + accessibilityRole="radio" + accessibilityLabel={`Repeat ${option.label.replace('No Repeat', 'none')}`} + accessibilityState={{ selected: value === option.value }} > bumpInterval(-1)} disabled={interval <= 1} activeOpacity={0.7} + accessibilityRole="button" + accessibilityLabel="Decrease repeat interval" + accessibilityState={{ disabled: interval <= 1 }} > @@ -188,6 +198,9 @@ export function RepeatSelector({ value, interval, days, onChange }: RepeatSelect onPress={() => bumpInterval(1)} disabled={interval >= 30} activeOpacity={0.7} + accessibilityRole="button" + accessibilityLabel="Increase repeat interval" + accessibilityState={{ disabled: interval >= 30 }} > = 30 ? theme.textMuted : theme.text }]}>+ @@ -210,6 +223,9 @@ export function RepeatSelector({ value, interval, days, onChange }: RepeatSelect ]} onPress={() => toggleDay(day)} activeOpacity={0.8} + accessibilityRole="checkbox" + accessibilityLabel={WEEKDAY_NAMES[day]} + accessibilityState={{ checked: days.includes(day) }} > void; onDragEnd?: (absoluteY: number) => void; depth?: number; + categoryColor?: string; } export const SubtaskItem = React.memo(function SubtaskItem({ @@ -33,7 +34,8 @@ export const SubtaskItem = React.memo(function SubtaskItem({ onDragStart, onDragUpdate, onDragEnd, - depth = 1 + depth = 1, + categoryColor }: SubtaskItemProps) { const { theme } = useSettings(); const [expanded, setExpanded] = useState(true); @@ -59,6 +61,7 @@ export const SubtaskItem = React.memo(function SubtaskItem({ onDragStart={onDragStart} onDragUpdate={onDragUpdate} onDragEnd={onDragEnd} + categoryColor={categoryColor} /> {hasChildren && expanded && ( @@ -81,6 +84,7 @@ export const SubtaskItem = React.memo(function SubtaskItem({ onDragUpdate={onDragUpdate} onDragEnd={onDragEnd} depth={depth + 1} + categoryColor={categoryColor} /> ))} diff --git a/carry-your-live/src/components/TaskItem.tsx b/carry-your-live/src/components/TaskItem.tsx index a0ca61b..7a42d5d 100644 --- a/carry-your-live/src/components/TaskItem.tsx +++ b/carry-your-live/src/components/TaskItem.tsx @@ -41,9 +41,10 @@ interface TaskItemProps { expanded?: boolean; indented?: boolean; depth?: number; + categoryColor?: string; } -export const TaskItem = React.memo(function TaskItem({ task, onToggle, onDelete, onPress, onLongPress, onMenuOpen, selected, selectionMode, completedSection, draggable, hovered, onDragStart, onDragUpdate, onDragEnd, onReorderStart, onReorderUpdate, onReorderEnd, expanded, indented, depth = 0 }: TaskItemProps) { +export const TaskItem = React.memo(function TaskItem({ task, onToggle, onDelete, onPress, onLongPress, onMenuOpen, selected, selectionMode, completedSection, draggable, hovered, onDragStart, onDragUpdate, onDragEnd, onReorderStart, onReorderUpdate, onReorderEnd, expanded, indented, depth = 0, categoryColor }: TaskItemProps) { const { theme } = useSettings(); const [opacityAnim] = React.useState(new Animated.Value(task.completed ? 0.5 : 1)); const [dragTranslateX] = React.useState(new Animated.Value(0)); @@ -95,7 +96,6 @@ export const TaskItem = React.memo(function TaskItem({ task, onToggle, onDelete, Gesture.Pan() .activateAfterLongPress(400) .minDistance(2) - .maxDistance(12) .runOnJS(true) .onStart(() => { setDragging(true); @@ -221,32 +221,25 @@ const renderLeftActions = (progress: Animated.AnimatedInterpolation) => onLongPress={onLongPress} delayLongPress={350} activeOpacity={0.8} + accessibilityRole={selectionMode ? 'checkbox' : 'button'} + accessibilityLabel={selectionMode ? `Select ${task.title}` : task.title} + accessibilityState={selectionMode ? { checked: selected } : { expanded }} + accessibilityHint={selectionMode ? undefined : 'Expands the task to show subtasks'} > - {}} - onPressOut={() => {}} - > - - - - - - - - - - - - + + {categoryColor ? ( + + ) : null} + {task.completed ? ( @@ -369,6 +362,10 @@ const renderLeftActions = (progress: Animated.AnimatedInterpolation) => style={styles.menuButton} onPress={onMenuOpen} activeOpacity={0.7} + accessibilityRole="button" + accessibilityLabel={`Edit ${task.title}`} + accessibilityHint="Opens the task editor" + hitSlop={8} > @@ -477,6 +474,18 @@ const styles = StyleSheet.create({ justifyContent: 'center', marginRight: 8, }, + categoryDotSlot: { + width: 28, + height: 28, + alignItems: 'center', + justifyContent: 'center', + marginRight: 8, + }, + categoryDot: { + width: 10, + height: 10, + borderRadius: 5, + }, title: { fontSize: 17, fontWeight: '500', diff --git a/carry-your-live/src/components/TaskList.tsx b/carry-your-live/src/components/TaskList.tsx index b36a5c0..652af58 100644 --- a/carry-your-live/src/components/TaskList.tsx +++ b/carry-your-live/src/components/TaskList.tsx @@ -1,6 +1,7 @@ import React, { useState, useCallback, useMemo, useRef, useEffect } from 'react'; import { View, Text, StyleSheet, Animated, RefreshControl, Alert, TouchableOpacity } from 'react-native'; import { useTasks } from '@/hooks/useTasks'; +import { useCategories } from '@/hooks/useDatabase'; import { useTaskModals } from '@/hooks/useTaskModals'; import { TaskItem } from './TaskItem'; import { SubtaskItem } from './SubtaskItem'; @@ -38,17 +39,24 @@ const DropIndicator = ({ theme }: { theme: any }) => ( ); export function TaskList({ categoryId = 'all', onSelectionChange }: TaskListProps) { - const { theme, sortBy } = useSettings(); + const { theme, sortBy, todoAheadDays } = useSettings(); - const { tasks, loading } = useTasks(categoryId, false); - const { tasks: completedTasks } = useTasks(categoryId, true); + const { tasks, loading } = useTasks(categoryId, 'all', todoAheadDays); + + const categories = useCategories(); + const categoryColors = useMemo(() => { + const map = new Map(); + for (const c of categories) { + map.set(c.id, c.color); + } + return map; + }, [categories]); const [refreshing, setRefreshing] = useState(false); const [selectionMode, setSelectionMode] = useState(false); const [selectedIds, setSelectedIds] = useState>(new Set()); const [hoverTaskId, setHoverTaskId] = useState(null); const [expandedTasks, setExpandedTasks] = useState>(new Set()); - const [completedShown, setCompletedShown] = useState(false); const [subtasksMap, setSubtasksMap] = useState>(new Map()); const [dropIndicator, setDropIndicator] = useState<{ targetId: string | null; position: 'above' | 'below' } | null>(null); const itemRefs = useRef>(new Map()); @@ -56,11 +64,10 @@ export function TaskList({ categoryId = 'all', onSelectionChange }: TaskListProp const subtaskDragRef = useRef<{ subtaskId: string; parentTaskId: string; positions: Record } | null>(null); const { modals, - openTaskMenu, openTaskDelete, - openSubtaskMenu, openSubtaskDelete, openSubtaskEdit, + openTaskEdit, } = useTaskModals(); const registerRef = useCallback((taskId: string, ref: View | null) => { @@ -135,9 +142,8 @@ export function TaskList({ categoryId = 'all', onSelectionChange }: TaskListProp } }, [expandedTasks, fetchSubtasks]); - const handleToggle = useCallback(async (taskId: string, showCompleted = true) => { + const handleToggle = useCallback(async (taskId: string) => { await toggleTaskComplete(taskId); - if (showCompleted) setCompletedShown(true); refreshAll(); }, [refreshAll]); @@ -186,7 +192,6 @@ export function TaskList({ categoryId = 'all', onSelectionChange }: TaskListProp const handleBulkComplete = useCallback(async () => { await Promise.all(Array.from(selectedIds).map(taskId => setTaskCompleted(taskId, true))); - setCompletedShown(true); exitSelection(); refreshAll(); }, [selectedIds, exitSelection, refreshAll]); @@ -315,11 +320,11 @@ export function TaskList({ categoryId = 'all', onSelectionChange }: TaskListProp onExpand={toggleExpand} onSelect={toggleSelect} onEnterSelection={enterSelection} - onMenuOpen={openTaskMenu} + onMenuOpen={(task) => openTaskEdit(task.id)} onSubtaskToggle={handleSubtaskToggle} onSubtaskDelete={openSubtaskDelete} onSubtaskEdit={openSubtaskEdit} - onSubtaskMenuOpen={openSubtaskMenu} + onSubtaskMenuOpen={(subtask) => openSubtaskEdit(subtask.id)} onDragStart={handleDragStart} onDragUpdate={handleDragUpdate} onDragEnd={handleDragEnd} @@ -330,6 +335,7 @@ export function TaskList({ categoryId = 'all', onSelectionChange }: TaskListProp onReorderUpdate={handleDragUpdate} onReorderEnd={handleDragEnd} selectedIds={selectedIds} + categoryColor={categoryColors.get(item.categoryId)} /> {showDropBelow && } @@ -349,79 +355,34 @@ export function TaskList({ categoryId = 'all', onSelectionChange }: TaskListProp toggleExpand, toggleSelect, enterSelection, - openTaskMenu, + openTaskEdit, handleSubtaskToggle, openSubtaskDelete, openSubtaskEdit, - openSubtaskMenu, handleDragStart, handleDragUpdate, handleDragEnd, handleSubtaskDragStart, handleSubtaskDragUpdate, handleSubtaskDragEnd, + categoryColors, ] ); const listHeader = useMemo(() => { - if (sortedTasks.length > 0 || completedTasks.length > 0) return null; + if (sortedTasks.length > 0) return null; return ( No tasks yet Tap + to add your first task ); - }, [sortedTasks.length, completedTasks.length, theme.textSecondary, theme.textMuted]); + }, [sortedTasks.length, theme.textSecondary, theme.textMuted]); const listFooter = useMemo(() => { - const footerContent = completedTasks.length === 0 ? null : ( - handleToggle(task.id, false)} - onDelete={openTaskDelete} - onMenuOpen={openTaskMenu} - onLongPress={(task) => enterSelection(task.id)} - selectionMode={selectionMode} - selectedIds={selectedIds} - onSelect={toggleSelect} - onFetchSubtasks={fetchSubtasks} - subtasksMap={subtasksMap} - onSubtaskToggle={handleSubtaskToggle} - onSubtaskDelete={openSubtaskDelete} - onSubtaskEdit={openSubtaskEdit} - onSubtaskMenuOpen={openSubtaskMenu} - /> - ); - const showDropAtEnd = dropIndicator && dropIndicator.targetId === null; - - return ( - - {footerContent} - {showDropAtEnd && } - - ); - }, [ - completedTasks, - completedShown, - handleToggle, - openTaskDelete, - openTaskMenu, - enterSelection, - selectionMode, - selectedIds, - toggleSelect, - fetchSubtasks, - subtasksMap, - handleSubtaskToggle, - openSubtaskDelete, - openSubtaskEdit, - openSubtaskMenu, - dropIndicator, - theme, - ]); + return showDropAtEnd ? : null; + }, [dropIndicator, theme]); if (loading && !refreshing) { return ( @@ -514,6 +475,7 @@ interface TaskRowProps { onReorderUpdate: (absoluteY: number) => void; onReorderEnd: (absoluteY: number, translationY: number) => void; selectedIds: Set; + categoryColor?: string; } const TaskRow = React.memo(function TaskRow({ @@ -544,6 +506,7 @@ const TaskRow = React.memo(function TaskRow({ onReorderUpdate, onReorderEnd, selectedIds, + categoryColor, }: TaskRowProps) { const sortedSubtasks = useMemo( () => subtasks.slice().sort((a, b) => a.order - b.order), @@ -572,6 +535,7 @@ const TaskRow = React.memo(function TaskRow({ onReorderStart={onReorderStart} onReorderUpdate={onReorderUpdate} onReorderEnd={onReorderEnd} + categoryColor={categoryColor} /> {expanded && subtasks.length > 0 && ( @@ -589,6 +553,7 @@ const TaskRow = React.memo(function TaskRow({ onDragStart={() => onSubtaskDragStart(sub.id, task.id)} onDragUpdate={onSubtaskDragUpdate} onDragEnd={onSubtaskDragEnd} + categoryColor={categoryColor} /> ))} @@ -597,119 +562,6 @@ const TaskRow = React.memo(function TaskRow({ ); }); -interface CompletedSectionProps { - tasks: TaskData[]; - shown: boolean; - onShownChange: (shown: boolean) => void; - onToggle: (task: TaskData) => void; - onDelete: (task: TaskData) => void; - onMenuOpen: (task: TaskData) => void; - onLongPress: (task: TaskData) => void; - selectionMode: boolean; - selectedIds: Set; - onSelect: (taskId: string) => void; - onFetchSubtasks: (taskId: string) => Promise; - subtasksMap: Map; - onSubtaskToggle: (subtaskId: string, taskId: string) => void; - onSubtaskDelete: (subtask: SubtaskData) => void; - onSubtaskEdit: (subtaskId: string) => void; - onSubtaskMenuOpen: (subtask: SubtaskData) => void; -} - -const CompletedSection = React.memo(function CompletedSection({ tasks, shown, onShownChange, onToggle, onDelete, onMenuOpen, onLongPress, selectionMode, selectedIds, onSelect, onFetchSubtasks, subtasksMap, onSubtaskToggle, onSubtaskDelete, onSubtaskEdit, onSubtaskMenuOpen }: CompletedSectionProps) { - const { theme } = useSettings(); - const [openTasks, setOpenTasks] = useState>(new Set(tasks.map((t) => t.id))); - const fetchedRef = useRef>(new Set()); - - useEffect(() => { - setOpenTasks((prev) => { - const next = new Set(prev); - let changed = false; - for (const t of tasks) { - if (!next.has(t.id)) { - next.add(t.id); - changed = true; - } - } - return changed ? next : prev; - }); - for (const t of tasks) { - if (!fetchedRef.current.has(t.id)) { - fetchedRef.current.add(t.id); - onFetchSubtasks(t.id); - } - } - }, [tasks, onFetchSubtasks]); - - const toggleOpen = useCallback((taskId: string) => { - setOpenTasks((prev) => { - const next = new Set(prev); - if (next.has(taskId)) { - next.delete(taskId); - } else { - next.add(taskId); - } - return next; - }); - }, []); - - return ( - - onShownChange(!shown)} - > - - Completed ({tasks.length}) - - - {shown ? 'Hide' : 'Show'} - - - {shown && ( - - {tasks.map((task) => { - const isOpen = openTasks.has(task.id); - const subtasks = (subtasksMap.get(task.id) ?? []) - .slice() - .sort((a, b) => a.order - b.order); - return ( - - onToggle(task)} - onDelete={() => onDelete(task)} - onPress={() => (selectionMode ? onSelect(task.id) : toggleOpen(task.id))} - onLongPress={() => onLongPress(task)} - onMenuOpen={() => onMenuOpen(task)} - selected={selectedIds.has(task.id)} - selectionMode={selectionMode} - completedSection - expanded={isOpen} - /> - {isOpen && subtasks.length > 0 && ( - - {subtasks.map((sub) => ( - onSubtaskToggle(sub.id, task.id)} - onDelete={() => onSubtaskDelete(sub)} - onPress={() => onSubtaskEdit(sub.id)} - onMenuOpen={() => onSubtaskMenuOpen(sub)} - /> - ))} - - )} - - ); - })} - - )} - - ); -}); - const styles = StyleSheet.create({ container: { flex: 1, diff --git a/carry-your-live/src/components/TaskNameInput.tsx b/carry-your-live/src/components/TaskNameInput.tsx index d9010db..668663f 100644 --- a/carry-your-live/src/components/TaskNameInput.tsx +++ b/carry-your-live/src/components/TaskNameInput.tsx @@ -25,6 +25,8 @@ export function TaskNameInput({ error, ...props }: TaskNameInputProps) { placeholderTextColor={theme.textMuted} maxLength={100} autoCapitalize="sentences" + accessibilityLabel="Task name" + accessibilityHint="Required field. Enter a name for the task" {...props} /> {error && {error}} diff --git a/carry-your-live/src/components/TaskOverflowMenu.tsx b/carry-your-live/src/components/TaskOverflowMenu.tsx index fc14deb..100ae0b 100644 --- a/carry-your-live/src/components/TaskOverflowMenu.tsx +++ b/carry-your-live/src/components/TaskOverflowMenu.tsx @@ -119,6 +119,8 @@ export function TaskOverflowMenu({ action.onPress(); }} activeOpacity={0.7} + accessibilityRole="button" + accessibilityLabel={`${action.label}${action.destructive ? ' (dangerous)' : ''}`} > {action.icon} diff --git a/carry-your-live/src/hooks/useTasks.tsx b/carry-your-live/src/hooks/useTasks.tsx index 7f4065f..9e66113 100644 --- a/carry-your-live/src/hooks/useTasks.tsx +++ b/carry-your-live/src/hooks/useTasks.tsx @@ -58,11 +58,19 @@ export function useTasksInMonth(monthDate: Date) { return { tasks, byDay, loading }; } -export function useTasks(categoryId?: string, showCompleted = false) { +export function useTasks(categoryId?: string, showCompleted: boolean | 'all' = false, maxAheadDays?: number) { const { collections } = useDatabase(); const [tasks, setTasks] = useState([]); const [loading, setLoading] = useState(true); + const cutoff = useMemo(() => { + if (maxAheadDays === undefined) return null; + const d = new Date(); + d.setHours(23, 59, 59, 999); + d.setDate(d.getDate() + maxAheadDays); + return d.getTime(); + }, [maxAheadDays]); + useEffect(() => { let mounted = true; const conditions: any[] = []; @@ -71,12 +79,16 @@ export function useTasks(categoryId?: string, showCompleted = false) { conditions.push(Q.where('category_id', categoryId)); } - if (showCompleted) { + if (showCompleted === true) { conditions.push(Q.where('completed', true)); - } else { + } else if (showCompleted === false) { conditions.push(Q.where('completed', false)); } + if (cutoff !== null) { + conditions.push(Q.where('due_date', Q.lte(cutoff))); + } + const query = conditions.length > 0 ? collections.tasks.query(Q.and(...conditions)) : collections.tasks.query(); @@ -99,7 +111,7 @@ export function useTasks(categoryId?: string, showCompleted = false) { mounted = false; subscription.unsubscribe(); }; - }, [collections, categoryId, showCompleted]); + }, [collections, categoryId, showCompleted, cutoff]); return { tasks, loading }; } @@ -148,55 +160,3 @@ export function useTasksByDate(date: Date) { return { tasks, loading }; } - -export function useTasksInMonth(month: Date) { - const { collections } = useDatabase(); - const [tasks, setTasks] = useState([]); - const [loading, setLoading] = useState(true); - - const range = useMemo(() => { - const start = startOfMonth(month); - start.setHours(0, 0, 0, 0); - const end = endOfMonth(month); - end.setHours(23, 59, 59, 999); - return { start: start.getTime(), end: end.getTime() }; - }, [month]); - - useEffect(() => { - let mounted = true; - const subscription = collections.tasks - .query( - Q.where('due_date', Q.between(range.start, range.end)), - Q.sortBy('due_date', 'asc') - ) - .observe() - .subscribe({ - next: (result) => { - if (mounted) { - setTasks(result); - setLoading(false); - } - }, - error: () => { - if (mounted) setLoading(false); - }, - }); - - return () => { - mounted = false; - subscription.unsubscribe(); - }; - }, [collections, range]); - - const byDay = useMemo(() => { - const map: Record = {}; - for (const task of tasks) { - const day = new Date(task.dueDate).getDate(); - if (!map[day]) map[day] = []; - map[day].push(task); - } - return map; - }, [tasks]); - - return { tasks, byDay, loading }; -} diff --git a/carry-your-live/src/theme.tsx b/carry-your-live/src/theme.tsx index 4bd2798..27c88eb 100644 --- a/carry-your-live/src/theme.tsx +++ b/carry-your-live/src/theme.tsx @@ -116,15 +116,20 @@ interface SettingsContextType { setApiUrl: (value: string) => void; accentColor: string; setAccentColor: (value: string) => void; + todoAheadDays: number; + setTodoAheadDays: (value: number) => void; theme: ThemeColors; } +export const DEFAULT_TODO_AHEAD_DAYS = 7; + const STORAGE_KEYS = { notifications: 'settings:notifications', defaultCategoryId: 'settings:defaultCategoryId', sortBy: 'settings:sortBy', reminderPreference: 'settings:reminderPreference', accentColor: 'settings:accentColor', + todoAheadDays: 'settings:todoAheadDays', }; const SettingsContext = createContext(null); @@ -165,6 +170,7 @@ export function SettingsProvider({ children }: { children: ReactNode }) { ); const [apiUrl, setApiUrl] = useStoredSetting(API_URL_KEY, DEFAULT_API_BASE_URL); const [accentColor, setAccentColor] = useStoredSetting(STORAGE_KEYS.accentColor, DEFAULT_ACCENT); + const [todoAheadDays, setTodoAheadDays] = useStoredSetting(STORAGE_KEYS.todoAheadDays, DEFAULT_TODO_AHEAD_DAYS); const theme = useMemo(() => colors(accentColor), [accentColor]); @@ -182,6 +188,8 @@ export function SettingsProvider({ children }: { children: ReactNode }) { setApiUrl, accentColor, setAccentColor, + todoAheadDays, + setTodoAheadDays, theme, }), [ @@ -197,6 +205,8 @@ export function SettingsProvider({ children }: { children: ReactNode }) { setApiUrl, accentColor, setAccentColor, + todoAheadDays, + setTodoAheadDays, theme, ] ); diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ff2ac35 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,48 @@ +version: '3.8' + +services: + postgres: + image: postgres:16-alpine + container_name: carry-your-live-db + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: carry_your_live + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 + + backend: + build: + context: ./backend + target: prod + container_name: carry-your-live-api + depends_on: + postgres: + condition: service_healthy + environment: + DATABASE_URL: postgresql://postgres:postgres@postgres:5432/carry_your_live + JWT_SECRET: your-super-secret-jwt-key-change-in-production-min-32-chars + PORT: 3000 + NODE_ENV: production + FRONTEND_URL: http://localhost:8081 + ports: + - "3000:3000" + + frontend: + build: + context: ./carry-your-live + container_name: carry-your-live-web + depends_on: + - backend + ports: + - "8081:8081" + +volumes: + postgres_data: \ No newline at end of file diff --git a/todo.md b/todo.md index e0984d3..b74c35c 100644 --- a/todo.md +++ b/todo.md @@ -1,34 +1,34 @@ -add a circle bevor the tasks so they can be checked ✓ - -change behavior on click ✓ - -apply swipe gestures ✓ - - -test server stuff ✓ - -tell the user the sync status - -commit the app - -build an android app and notify it for updates on the app - -add an all-day option to tasks ✓ -fix the calendar layout - -implemennt that just task for today and the past can be checked ✓ - - -implement stats - - -implemnt an android widget for quickadd tasks - - -fix spacing for todo cards more space for todos - - -fix edit todo form layout so it fitts nicely on phone screen - -fix buggy scroll for time selection - +- [x] Tasks should be renamed to ToDo +- [x] top of ToDo page should say ToDo instead of TODO +- [ ] Calendar rework +- [x] when task with subtasks marked as done it hides the whole task and the subtask are gone and can't be seen anymore (subtask should be visible after completion) +- [x] when dragging task to become subtask user should hold the task and not tap + because now it's very hard to scroll through tasks without randomly creating subtasks +- [ ] when creating new task there is a huge gap between the create task box and the keyboard +- [ ] after creating task keyboard should automatically be hidden again + now it's hard to get rid of the keyboard without clicking on other tasks +- [ ] ToDo page button for category: hitbox very weird hard to click + when clicked should be framed in accent color and not get bigger like the all + category now +- [ ] category all button doesn't have the correct spacing between the dot and the text +- [ ] no way to tell what category the task has -> should be a colored dot in the left of the task box replacing the nine random dots that don't anything +- [ ] task from the calendar page should only show up in the ToDo page if it is that day or x days in advance (x can be set in settings) +- [ ] clicking the three dots should open the "add new task" window instead of the window with the limited options it opens currently +- [ ] swiping task for deleting or completing +- [x] no category required by creating task +- [ ] calendar task arrow not centered +- [X] page names (up top) are not centered +- [X] tick icon in the top left needs to be removed/ replaced/ given a function +- [ ] settings -> categories -> new / change category window needs to be centered and fully visible + exit window button not visible or not existent + user should be able to click outside of the window to close it +- [ ] pressing the go back button android: + in settings go to previous page task/ calendar/ stats + in other (task/ calendar/ stats) stay on that page + at the moment calendar and stats go to tasks +- [ ] ToDo -> select category -> scroll to the right most -> button to go to categories in settings +- [x] accent color should be changeable +- [ ] bottom page buttons need to be reworked looking ass currently. +- [ ] add a task button should say "Add a Task" and (the button to add have an arrow pointing upwards instead of a cross) could look better +- [ ] space between add task button and bottom bar should be zero of the task below should be blurred currently task visible between the two looks weird +- [ ] Stats page rework (when time is right) \ No newline at end of file