Compare commits

..

No commits in common. "e373deb347c715ef2c2ba208b3949e92e768d487" and "9187461ed4e4ac5bd21d3872af7d177db0a60362" have entirely different histories.

6 changed files with 96 additions and 284 deletions

View file

@ -1,93 +0,0 @@
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<TasksResponse>;
}
export async function listFetcher(id: string): Promise<ListData> {
type Expand = { "lists(parent)": unknown[]; "tasks(list)": unknown[] };
type ListsResponseExpand = ListsResponse<Expand>;
if (id === "all") {
const lists = await pb
.collection<ListsResponseExpand>("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<ListsResponseExpand>("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<TaskData> {
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<RandomTaskData>(`/api/extras/random/${parent}`, {});
}

View file

@ -2,4 +2,3 @@ import PocketBase from "pocketbase";
import type { TypedPocketBase } from "./pocketbase-types"; import type { TypedPocketBase } from "./pocketbase-types";
export const pb = new PocketBase("https://musclecat.pi.korz.tech") as TypedPocketBase; export const pb = new PocketBase("https://musclecat.pi.korz.tech") as TypedPocketBase;
//export const pb = new PocketBase("http://localhost:8090") as TypedPocketBase;

View file

@ -5,42 +5,27 @@ import {
useWindowDimensions, useWindowDimensions,
Image, Image,
} from "react-native"; } from "react-native";
import { Appbar, Text, Avatar, Card, List, useTheme, Button } from "react-native-paper"; import { Appbar, Text, Avatar, Card, List, useTheme } from "react-native-paper";
import { import {
StackHeaderProps, StackHeaderProps,
createStackNavigator, createStackNavigator,
} from "@react-navigation/stack"; } from "@react-navigation/stack";
import RenderHtml from "react-native-render-html"; import RenderHtml from "react-native-render-html";
import { HomeScreenNavigationProp } from "./types"; import { HomeScreenNavigationProp } from "./types";
import useSWR, { preload } from "swr"; import useSWR from "swr";
import { pb } from "../api";
import { getHeaderTitle } from "@react-navigation/elements"; import { getHeaderTitle } from "@react-navigation/elements";
import { useEffect } from "react"; import { useEffect, useLayoutEffect } 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 ( return (
<Card <Card
style={{ marginHorizontal: 8, marginBottom: 8 }} style={{ marginHorizontal: 8, marginBottom: 8 }}
onPress={props.onPress} onPress={props.onPress}
> >
<Card.Title <Card.Title
title={name} title={props.name}
subtitle={subtitle} //subtitle="A list"
left={(props) => <Avatar.Icon {...props} icon="clipboard-list" />} left={(props) => <Avatar.Icon {...props} icon="clipboard-list" />}
/*right={(props) => ( /*right={(props) => (
<IconButton {...props} icon="dots-vertical" onPress={() => {}} /> <IconButton {...props} icon="dots-vertical" onPress={() => {}} />
@ -50,23 +35,64 @@ function ListItem(props: { data: ListData["lists"][0]; onPress(): void }) {
); );
} }
function TaskItem(props: { data: ListData["tasks"][0]; onPress(): void }) { function TaskItem(props: { name: string; onPress(): void }) {
const { id, name, schedule } = props.data;
const date = new Date(schedule);
const now = new Date();
const ready = date <= now;
return ( return (
<List.Item <List.Item
title={name} title={props.name}
description={ready ? "Ready" : "Not ready"} //description="A task"
left={(props) => <List.Icon {...props} icon="clipboard-check-outline" />} left={(props) => <List.Icon {...props} icon="clipboard-check-outline" />}
onPress={props.onPress} 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<ListResponse> {
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">) { function ListContent({ route, navigation }: HomeScreenNavigationProp<"List">) {
const theme = useTheme(); const theme = useTheme();
const { listId, listName } = route.params ?? {}; const { listId, listName } = route.params ?? {};
@ -111,10 +137,8 @@ function ListContent({ route, navigation }: HomeScreenNavigationProp<"List">) {
{lists.map((l) => ( {lists.map((l) => (
<ListItem <ListItem
key={l.id} key={l.id}
data={l} name={l.name}
onPress={() => onPress={() => navigation.push("List", { listId: l.id, listName: l.name })}
navigation.push("List", { listId: l.id, listName: l.name })
}
/> />
))} ))}
{tasks.length > 0 && ( {tasks.length > 0 && (
@ -123,10 +147,8 @@ function ListContent({ route, navigation }: HomeScreenNavigationProp<"List">) {
{tasks.map((t) => ( {tasks.map((t) => (
<TaskItem <TaskItem
key={t.id} key={t.id}
data={t} name={t.name}
onPress={() => onPress={() => navigation.push("Task", { taskId: t.id, taskName: t.name })}
navigation.push("Task", { taskId: t.id, taskName: t.name })
}
/> />
))} ))}
</List.Section> </List.Section>
@ -135,6 +157,29 @@ function ListContent({ route, navigation }: HomeScreenNavigationProp<"List">) {
); );
} }
interface TaskResponse {
id: string;
name: string;
description: string;
listId: string;
imageSource: string;
}
async function taskFetcher(id: string): Promise<TaskResponse> {
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">) { function TaskContent({ route, navigation }: HomeScreenNavigationProp<"Task">) {
const dimensions = useWindowDimensions(); const dimensions = useWindowDimensions();
const theme = useTheme(); const theme = useTheme();
@ -166,15 +211,12 @@ function TaskContent({ route, navigation }: HomeScreenNavigationProp<"Task">) {
<View <View
style={[styles.padded, { backgroundColor: theme.colors.background }]} style={[styles.padded, { backgroundColor: theme.colors.background }]}
> >
<Text>Something went wrong: {error?.toString()}</Text> <Text>Something went wrong: {error.toString()}</Text>
</View> </View>
); );
} }
const { name, description, schedule, imageSource } = data; const { name, description, imageSource } = data;
const date = new Date(schedule);
const now = new Date();
const ready = date <= now;
return ( return (
<View <View
@ -193,7 +235,6 @@ function TaskContent({ route, navigation }: HomeScreenNavigationProp<"Task">) {
fontSize: "1.1em", fontSize: "1.1em",
}} }}
/> />
<Button disabled={!ready} mode="contained">{ready ? "Start" : "Not ready"}</Button>
</View> </View>
); );
} }
@ -204,24 +245,14 @@ function ListNavigationBar({
options, options,
back, back,
}: StackHeaderProps) { }: StackHeaderProps) {
const params = route.params as any | undefined; const title = getHeaderTitle(options, (route.params as any)?.listName || route.name);
const title = getHeaderTitle(options, params?.listName || route.name);
return ( return (
<Appbar.Header elevated> <Appbar.Header elevated>
{(back || params?.listId) ? <Appbar.BackAction {back ? <Appbar.BackAction onPress={() => navigation.pop()} /> : null}
onPress={() => (back ? navigation.pop() : navigation.replace("List"))}
/> : null}
<Appbar.Content title={title} /> <Appbar.Content title={title} />
<Appbar.Action <Appbar.Action icon="dice-multiple-outline" onPress={() => {}} />
icon="dice-multiple-outline" <Appbar.Action icon="magnify" onPress={() => {}} />
onPress={() =>
randomTask(params?.listId || null).then((data) =>
navigation.push("Task", { taskId: data.id, taskName: data.name }),
)
}
/>
{/*<Appbar.Action icon="magnify" onPress={() => {}} />*/}
</Appbar.Header> </Appbar.Header>
); );
} }
@ -232,10 +263,7 @@ function TaskNavigationBar({
options, options,
back, back,
}: StackHeaderProps) { }: StackHeaderProps) {
const title = getHeaderTitle( const title = getHeaderTitle(options, (route.params as any)?.taskName || route.name);
options,
(route.params as any)?.taskName || route.name,
);
return ( return (
<Appbar.Header elevated> <Appbar.Header elevated>
@ -243,8 +271,8 @@ function TaskNavigationBar({
onPress={() => (back ? navigation.pop() : navigation.replace("List"))} onPress={() => (back ? navigation.pop() : navigation.replace("List"))}
/> />
<Appbar.Content title={title} /> <Appbar.Content title={title} />
{/*<Appbar.Action icon="run-fast" onPress={() => {}} />*/} <Appbar.Action icon="run-fast" onPress={() => {}} />
{/*<Appbar.Action icon="square-edit-outline" onPress={() => {}} />*/} <Appbar.Action icon="square-edit-outline" onPress={() => {}} />
</Appbar.Header> </Appbar.Header>
); );
} }
@ -253,7 +281,9 @@ const Stack = createStackNavigator();
export function ScreenList() { export function ScreenList() {
return ( return (
<Stack.Navigator initialRouteName="List"> <Stack.Navigator
initialRouteName="List"
>
<Stack.Screen <Stack.Screen
name="List" name="List"
component={ListContent as any} component={ListContent as any}

View file

@ -1,21 +0,0 @@
routerAdd("GET", "/api/extras/random/:parent", (c) => {
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)
})

View file

@ -1,61 +0,0 @@
/// <reference path="../pb_data/types.d.ts" />
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)
})

View file

@ -1,42 +0,0 @@
/// <reference path="../pb_data/types.d.ts" />
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)
})