6 changed files with 69 additions and 23 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import { View, Text, Pressable, TextInput, useColorScheme, Alert } from 'react-native';
|
||||
import { View, Text, Pressable, TextInput, useColorScheme, Alert, RefreshControl } from 'react-native';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { Plus, ArrowUpDown, Filter, Download, Search, X } from 'lucide-react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
|
@ -44,6 +44,7 @@ export default function InboxScreen() {
|
|||
const theme = useSettingsStore((s) => s.theme);
|
||||
const isDark = (theme === 'system' ? systemScheme : theme) === 'dark';
|
||||
const isDraggingRef = useRef(false);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
const { sortBy, sortOrder, filterPriority, filterTag, filterCompleted, filterDueDate, hasActiveFilters } = useTaskStore();
|
||||
|
||||
|
|
@ -70,6 +71,12 @@ export default function InboxScreen() {
|
|||
return () => clearInterval(interval);
|
||||
}, [loadTasks]);
|
||||
|
||||
const handleRefresh = useCallback(async () => {
|
||||
setRefreshing(true);
|
||||
await loadTasks();
|
||||
setRefreshing(false);
|
||||
}, [loadTasks]);
|
||||
|
||||
const handleToggle = async (id: string) => {
|
||||
await toggleComplete(id);
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
|
|
@ -201,6 +208,14 @@ export default function InboxScreen() {
|
|||
onDragBegin={() => { isDraggingRef.current = true; }}
|
||||
onDragEnd={handleDragEnd}
|
||||
activationDistance={canDrag ? 0 : 10000}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={handleRefresh}
|
||||
tintColor={colors.bleu.DEFAULT}
|
||||
colors={[colors.bleu.DEFAULT]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</GestureHandlerRootView>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import { View, Text, Pressable, TextInput, useColorScheme, Alert } from 'react-native';
|
||||
import { View, Text, Pressable, TextInput, useColorScheme, Alert, RefreshControl } from 'react-native';
|
||||
import { useRouter, useLocalSearchParams } from 'expo-router';
|
||||
import {
|
||||
ArrowLeft, Plus, ArrowUpDown, Filter, Download, Search, X,
|
||||
|
|
@ -61,6 +61,7 @@ export default function ListDetailScreen() {
|
|||
const theme = useSettingsStore((s) => s.theme);
|
||||
const isDark = (theme === 'system' ? systemScheme : theme) === 'dark';
|
||||
const isDraggingRef = useRef(false);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
const { sortBy, sortOrder, filterPriority, filterTag, filterCompleted, filterDueDate, hasActiveFilters } = useTaskStore();
|
||||
|
||||
|
|
@ -95,6 +96,12 @@ export default function ListDetailScreen() {
|
|||
return () => clearInterval(interval);
|
||||
}, [loadData]);
|
||||
|
||||
const handleRefresh = useCallback(async () => {
|
||||
setRefreshing(true);
|
||||
await loadData();
|
||||
setRefreshing(false);
|
||||
}, [loadData]);
|
||||
|
||||
const handleToggle = async (taskId: string) => {
|
||||
await toggleComplete(taskId);
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
|
|
@ -249,6 +256,14 @@ export default function ListDetailScreen() {
|
|||
onDragBegin={() => { isDraggingRef.current = true; }}
|
||||
onDragEnd={handleDragEnd}
|
||||
activationDistance={canDrag ? 0 : 10000}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={handleRefresh}
|
||||
tintColor={colors.bleu.DEFAULT}
|
||||
colors={[colors.bleu.DEFAULT]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</GestureHandlerRootView>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -200,6 +200,16 @@ async function processOperation(op: SyncOperation, userId: string) {
|
|||
const d = (data as Record<string, unknown>) || {};
|
||||
const incomingIsInbox = d.isInbox as boolean | undefined;
|
||||
|
||||
const listValues = {
|
||||
id: entityId,
|
||||
userId,
|
||||
name: d.name as string || 'Untitled',
|
||||
color: d.color as string | undefined,
|
||||
icon: d.icon as string | undefined,
|
||||
position: d.position as number | undefined,
|
||||
isInbox: incomingIsInbox,
|
||||
};
|
||||
|
||||
// If the incoming list is an inbox, check for an existing inbox and merge
|
||||
if (incomingIsInbox) {
|
||||
await db.transaction(async (tx) => {
|
||||
|
|
@ -219,26 +229,10 @@ async function processOperation(op: SyncOperation, userId: string) {
|
|||
.where(eq(slLists.id, existingInbox.id));
|
||||
}
|
||||
|
||||
await tx.insert(slLists).values({
|
||||
id: entityId,
|
||||
userId,
|
||||
name: d.name as string || 'Untitled',
|
||||
color: d.color as string | undefined,
|
||||
icon: d.icon as string | undefined,
|
||||
position: d.position as number | undefined,
|
||||
isInbox: incomingIsInbox,
|
||||
}).onConflictDoNothing();
|
||||
await tx.insert(slLists).values(listValues).onConflictDoNothing();
|
||||
});
|
||||
} else {
|
||||
await db.insert(slLists).values({
|
||||
id: entityId,
|
||||
userId,
|
||||
name: d.name as string || 'Untitled',
|
||||
color: d.color as string | undefined,
|
||||
icon: d.icon as string | undefined,
|
||||
position: d.position as number | undefined,
|
||||
isInbox: incomingIsInbox,
|
||||
}).onConflictDoNothing();
|
||||
await db.insert(slLists).values(listValues).onConflictDoNothing();
|
||||
}
|
||||
} else if (action === 'update') {
|
||||
await verifyOwnership(slLists, entityId, userId);
|
||||
|
|
|
|||
|
|
@ -4,8 +4,9 @@ import type { Task } from "@/lib/types";
|
|||
import { TaskItem } from "./TaskItem";
|
||||
import { TaskForm } from "./TaskForm";
|
||||
import { FilterBar } from "./FilterBar";
|
||||
import { ClipboardList } from "lucide-react";
|
||||
import { Suspense } from "react";
|
||||
import { ClipboardList, RefreshCw } from "lucide-react";
|
||||
import { Suspense, useState, useCallback } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface TaskListProps {
|
||||
|
|
@ -17,12 +18,31 @@ interface TaskListProps {
|
|||
|
||||
export function TaskList({ tasks, subtasksMap, listId, listName }: TaskListProps) {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
const handleRefresh = useCallback(async () => {
|
||||
setRefreshing(true);
|
||||
router.refresh();
|
||||
// Brief visual feedback
|
||||
setTimeout(() => setRefreshing(false), 500);
|
||||
}, [router]);
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto w-full">
|
||||
{/* Header */}
|
||||
<div className="mb-6">
|
||||
<h2 className="text-xl font-semibold mb-3">{listName}</h2>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="text-xl font-semibold">{listName}</h2>
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
disabled={refreshing}
|
||||
className="p-1.5 text-foreground/40 hover:text-foreground transition-colors disabled:opacity-50"
|
||||
title={t("task.refresh")}
|
||||
>
|
||||
<RefreshCw size={18} className={refreshing ? "animate-spin" : ""} />
|
||||
</button>
|
||||
</div>
|
||||
<Suspense fallback={null}>
|
||||
<FilterBar />
|
||||
</Suspense>
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"subtaskPlaceholder": "New subtask...",
|
||||
"notesPlaceholder": "Notes...",
|
||||
"empty": "No tasks",
|
||||
"refresh": "Refresh",
|
||||
"edit": "Edit",
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"subtaskPlaceholder": "Nouvelle sous-tâche...",
|
||||
"notesPlaceholder": "Notes...",
|
||||
"empty": "Aucune tâche",
|
||||
"refresh": "Rafraîchir",
|
||||
"edit": "Modifier",
|
||||
"save": "Enregistrer",
|
||||
"cancel": "Annuler",
|
||||
|
|
|
|||
Loading…
Reference in a new issue