diff --git a/app/src/api/fetcher.ts b/app/src/api/fetcher.ts new file mode 100644 index 0000000..3eecaa9 --- /dev/null +++ b/app/src/api/fetcher.ts @@ -0,0 +1,93 @@ +import { pb } from "./pb"; +import { ListsResponse, TasksResponse } from "./pocketbase-types"; + +export interface ListData { + id: string; + name: string; + parentId: string | null; + lists: Array<{ + id: string; + name: string; + listCount: number; + taskCount: number; + }>; + tasks: Array; +} + +export async function listFetcher(id: string): Promise { + type Expand = { "lists(parent)": unknown[]; "tasks(list)": unknown[] }; + type ListsResponseExpand = ListsResponse; + if (id === "all") { + const lists = await pb + .collection("lists") + .getList(0, 50, { + filter: pb.filter("parent = null"), + expand: "lists(parent),tasks(list)", + }); + const tasks = await pb.collection("tasks").getList(0, 50, { + filter: pb.filter("list = null"), + }); + return { + id, + name: "Lists", + parentId: null, + lists: lists.items.map((l) => ({ + id: l.id, + name: l.name, + listCount: l.expand?.["lists(parent)"]?.length || 0, + taskCount: l.expand?.["tasks(list)"]?.length || 0, + })), + tasks: tasks.items, + }; + } + + const list = await pb.collection("lists").getOne(id); + const lists = await pb + .collection("lists") + .getList(0, 50, { + filter: pb.filter("parent = {:id}", { id }), + expand: "lists(parent),tasks(list)", + }); + const tasks = await pb.collection("tasks").getList(0, 50, { + filter: pb.filter("list = {:id}", { id }), + }); + return { + id, + name: list.name, + parentId: list.parent, + lists: lists.items.map((l) => ({ + id: l.id, + name: l.name, + listCount: l.expand?.["lists(parent)"]?.length || 0, + taskCount: l.expand?.["tasks(list)"]?.length || 0, + })), + tasks: tasks.items, + }; +} + +export interface TaskData extends TasksResponse { + listId: string; + imageSource: string; +} + +export async function taskFetcher(id: string): Promise { + const task = await pb.collection("tasks").getOne(id); + const icon = await pb.collection("icons").getOne(task.icon); + const imageSource = pb.getFileUrl(icon, icon.image, { + thumb: "300x300", + }); + return { + ...task, + listId: task.list, + imageSource, + }; +} + +export interface RandomTaskData { + id: string; + name: string; +} + +export async function randomTask(parent: string | null) { + return await pb.send(`/api/extras/random/${parent}`, {}); +} diff --git a/app/src/api/index.ts b/app/src/api/index.ts deleted file mode 100644 index c07ccaf..0000000 --- a/app/src/api/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import PocketBase from "pocketbase"; -import type { TypedPocketBase } from "./pocketbase-types"; - -export const pb = new PocketBase("https://musclecat.pi.korz.tech") as TypedPocketBase; diff --git a/app/src/api/pb.ts b/app/src/api/pb.ts new file mode 100644 index 0000000..184abb8 --- /dev/null +++ b/app/src/api/pb.ts @@ -0,0 +1,5 @@ +import PocketBase from "pocketbase"; +import type { TypedPocketBase } from "./pocketbase-types"; + +//export const pb = new PocketBase("https://musclecat.pi.korz.tech") as TypedPocketBase; +export const pb = new PocketBase("http://localhost:8090") as TypedPocketBase; diff --git a/app/src/screens/List.tsx b/app/src/screens/List.tsx index 9d39d44..f470151 100644 --- a/app/src/screens/List.tsx +++ b/app/src/screens/List.tsx @@ -5,27 +5,42 @@ import { useWindowDimensions, Image, } from "react-native"; -import { Appbar, Text, Avatar, Card, List, useTheme } from "react-native-paper"; +import { Appbar, Text, Avatar, Card, List, useTheme, Button } from "react-native-paper"; import { StackHeaderProps, createStackNavigator, } from "@react-navigation/stack"; import RenderHtml from "react-native-render-html"; import { HomeScreenNavigationProp } from "./types"; -import useSWR from "swr"; -import { pb } from "../api"; +import useSWR, { preload } from "swr"; import { getHeaderTitle } from "@react-navigation/elements"; -import { useEffect, useLayoutEffect } from "react"; +import { useEffect } from "react"; +import { ListData, listFetcher, randomTask, taskFetcher } from "../api/fetcher"; + +function ListItem(props: { data: ListData["lists"][0]; onPress(): void }) { + const { name, listCount, taskCount } = props.data; + let subtitle = ""; + if (listCount) { + subtitle += `${listCount} lists`; + } + if (listCount && taskCount) { + subtitle += " • " + } + if (taskCount) { + subtitle += `${taskCount} tasks`; + } + if (!listCount && !taskCount) { + subtitle += "Empty"; + } -function ListItem(props: { name: string; onPress(): void }) { return ( } /*right={(props) => ( {}} /> @@ -35,64 +50,23 @@ function ListItem(props: { name: string; onPress(): void }) { ); } -function TaskItem(props: { name: string; onPress(): void }) { +function TaskItem(props: { data: ListData["tasks"][0]; onPress(): void }) { + const { id, name, schedule } = props.data; + const date = new Date(schedule); + const now = new Date(); + const ready = date <= now; + return ( } onPress={props.onPress} + onPressIn={() => preload(id, taskFetcher)} /> ); } -interface ListResponse { - id: string; - name: string; - parentId: string | null; - lists: Array<{ - id: string; - name: string; - }>; - tasks: Array<{ - id: string; - name: string; - }>; -} - -async function listFetcher(id: string): Promise { - if (id === "all") { - const lists = await pb.collection("lists").getList(0, 50, { - filter: pb.filter("parent = null"), - }); - const tasks = await pb.collection("tasks").getList(0, 50, { - filter: pb.filter("list = null"), - }); - return { - id, - name: "Lists", - parentId: null, - lists: lists.items.map((l) => ({ id: l.id, name: l.name })), - tasks: tasks.items.map((l) => ({ id: l.id, name: l.name })), - }; - } - - const list = await pb.collection("lists").getOne(id); - const lists = await pb.collection("lists").getList(0, 50, { - filter: pb.filter("parent = {:id}", { id }), - }); - const tasks = await pb.collection("tasks").getList(0, 50, { - filter: pb.filter("list = {:id}", { id }), - }); - return { - id, - name: list.name, - parentId: list.parent, - lists: lists.items.map((l) => ({ id: l.id, name: l.name })), - tasks: tasks.items.map((l) => ({ id: l.id, name: l.name })), - }; -} - function ListContent({ route, navigation }: HomeScreenNavigationProp<"List">) { const theme = useTheme(); const { listId, listName } = route.params ?? {}; @@ -137,8 +111,10 @@ function ListContent({ route, navigation }: HomeScreenNavigationProp<"List">) { {lists.map((l) => ( navigation.push("List", { listId: l.id, listName: l.name })} + data={l} + onPress={() => + navigation.push("List", { listId: l.id, listName: l.name }) + } /> ))} {tasks.length > 0 && ( @@ -147,8 +123,10 @@ function ListContent({ route, navigation }: HomeScreenNavigationProp<"List">) { {tasks.map((t) => ( navigation.push("Task", { taskId: t.id, taskName: t.name })} + data={t} + onPress={() => + navigation.push("Task", { taskId: t.id, taskName: t.name }) + } /> ))} @@ -157,29 +135,6 @@ function ListContent({ route, navigation }: HomeScreenNavigationProp<"List">) { ); } -interface TaskResponse { - id: string; - name: string; - description: string; - listId: string; - imageSource: string; -} - -async function taskFetcher(id: string): Promise { - const task = await pb.collection("tasks").getOne(id); - const icon = await pb.collection("icons").getOne(task.icon); - const imageSource = pb.getFileUrl(icon, icon.image, { - thumb: "300x300", - }); - return { - id, - name: task.name, - description: task.description, - listId: task.list, - imageSource, - }; -} - function TaskContent({ route, navigation }: HomeScreenNavigationProp<"Task">) { const dimensions = useWindowDimensions(); const theme = useTheme(); @@ -211,12 +166,15 @@ function TaskContent({ route, navigation }: HomeScreenNavigationProp<"Task">) { - Something went wrong: {error.toString()} + Something went wrong: {error?.toString()} ); } - const { name, description, imageSource } = data; + const { name, description, schedule, imageSource } = data; + const date = new Date(schedule); + const now = new Date(); + const ready = date <= now; return ( ) { fontSize: "1.1em", }} /> + ); } @@ -245,14 +204,24 @@ function ListNavigationBar({ options, back, }: StackHeaderProps) { - const title = getHeaderTitle(options, (route.params as any)?.listName || route.name); + const params = route.params as any | undefined; + const title = getHeaderTitle(options, params?.listName || route.name); return ( - {back ? navigation.pop()} /> : null} + {(back || params?.listId) ? (back ? navigation.pop() : navigation.replace("List"))} + /> : null} - {}} /> - {}} /> + + randomTask(params?.listId || null).then((data) => + navigation.push("Task", { taskId: data.id, taskName: data.name }), + ) + } + /> + {/* {}} />*/} ); } @@ -263,7 +232,10 @@ function TaskNavigationBar({ options, back, }: StackHeaderProps) { - const title = getHeaderTitle(options, (route.params as any)?.taskName || route.name); + const title = getHeaderTitle( + options, + (route.params as any)?.taskName || route.name, + ); return ( @@ -271,8 +243,8 @@ function TaskNavigationBar({ onPress={() => (back ? navigation.pop() : navigation.replace("List"))} /> - {}} /> - {}} /> + {/* {}} />*/} + {/* {}} />*/} ); } @@ -281,9 +253,7 @@ const Stack = createStackNavigator(); export function ScreenList() { return ( - + { + const result = new DynamicModel({ + "id": "", + "name": "", + }); + + const query = `WITH RECURSIVE descendants(id) AS ( + VALUES({:parent}) UNION ALL + SELECT l.id FROM lists l JOIN descendants d ON d.id = l.parent + ) SELECT t.id, t.name FROM tasks t JOIN descendants d ON t.list = d.id WHERE t.schedule <= DATE('now') ORDER BY RANDOM() LIMIT 1`; + const queryNull = `SELECT id, name FROM tasks WHERE schedule <= DATE('now') ORDER BY RANDOM() LIMIT 1`; + const parent = c.pathParam("parent"); + + $app.dao().db() + .newQuery(parent === "null" ? queryNull : query) + .bind({ parent }) + .one(result) + + return c.json(200, result) +}) + diff --git a/pb_migrations/1704322685_updated_tasks.js b/pb_migrations/1704322685_updated_tasks.js new file mode 100644 index 0000000..dc052df --- /dev/null +++ b/pb_migrations/1704322685_updated_tasks.js @@ -0,0 +1,61 @@ +/// +migrate((db) => { + const dao = new Dao(db) + const collection = dao.findCollectionByNameOrId("pf13z4hs1iubw41") + + // add + collection.schema.addField(new SchemaField({ + "system": false, + "id": "mxuameue", + "name": "reward", + "type": "number", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": 0, + "max": 10, + "noDecimal": false + } + })) + + // update + collection.schema.addField(new SchemaField({ + "system": false, + "id": "qjz9b0tm", + "name": "index4groupdefaultschedule", + "type": "date", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": "", + "max": "" + } + })) + + return dao.saveCollection(collection) +}, (db) => { + const dao = new Dao(db) + const collection = dao.findCollectionByNameOrId("pf13z4hs1iubw41") + + // remove + collection.schema.removeField("mxuameue") + + // update + collection.schema.addField(new SchemaField({ + "system": false, + "id": "qjz9b0tm", + "name": "schedule", + "type": "date", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": "", + "max": "" + } + })) + + return dao.saveCollection(collection) +}) diff --git a/pb_migrations/1704322700_updated_tasks.js b/pb_migrations/1704322700_updated_tasks.js new file mode 100644 index 0000000..b9e37ed --- /dev/null +++ b/pb_migrations/1704322700_updated_tasks.js @@ -0,0 +1,42 @@ +/// +migrate((db) => { + const dao = new Dao(db) + const collection = dao.findCollectionByNameOrId("pf13z4hs1iubw41") + + // update + collection.schema.addField(new SchemaField({ + "system": false, + "id": "qjz9b0tm", + "name": "schedule", + "type": "date", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": "", + "max": "" + } + })) + + return dao.saveCollection(collection) +}, (db) => { + const dao = new Dao(db) + const collection = dao.findCollectionByNameOrId("pf13z4hs1iubw41") + + // update + collection.schema.addField(new SchemaField({ + "system": false, + "id": "qjz9b0tm", + "name": "index4groupdefaultschedule", + "type": "date", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": "", + "max": "" + } + })) + + return dao.saveCollection(collection) +})