Round 3: rewrite in React Native

This commit is contained in:
Niklas Korz 2024-01-01 19:50:01 +01:00
parent 13d7ebb061
commit 8a74d3e2c9
50 changed files with 19136 additions and 5288 deletions

View file

@ -5,4 +5,3 @@
```
npx pocketbase-typegen --db ./pb_data/data.db --out app/src/lib/pocketbase-types.ts
```

View file

@ -1,13 +0,0 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock

View file

@ -1,31 +0,0 @@
/** @type { import("eslint").Linter.FlatConfig } */
module.exports = {
root: true,
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:svelte/recommended',
'prettier'
],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
parserOptions: {
sourceType: 'module',
ecmaVersion: 2020,
extraFileExtensions: ['.svelte']
},
env: {
browser: true,
es2017: true,
node: true
},
overrides: [
{
files: ['*.svelte'],
parser: 'svelte-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser'
}
}
]
};

43
app/.gitignore vendored
View file

@ -1,10 +1,35 @@
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
# dependencies
node_modules/
# Expo
.expo/
dist/
web-build/
# Native
*.orig.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
# Metro
.metro-health-check*
# debug
npm-debug.*
yarn-debug.*
yarn-error.*
# macOS
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
*.pem
# local env files
.env*.local
# typescript
*.tsbuildinfo

View file

@ -1 +0,0 @@
engine-strict=true

View file

@ -1,13 +0,0 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock

View file

@ -1,8 +0,0 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

51
app/App.tsx Normal file
View file

@ -0,0 +1,51 @@
import 'react-native-gesture-handler';
import React from "react";
import { StyleSheet, useWindowDimensions } from "react-native";
import { NavigationContainer } from "@react-navigation/native";
import { PaperProvider, Text } from "react-native-paper";
import { BottomNavigation } from "./src/BottomNavigation";
import { DrawerNavigation } from "./src/DrawerNavigation";
const linking = {
prefixes: [
/* your linking prefixes */
"musclecat://",
"https://musclecat.pi.korz.tech",
"https://musclecat.pi4.korz.tech",
],
config: {
/* configuration for matching screens with paths */
screens: {
Home: {
screens: {
List: "lists/:listId?",
Task: "tasks/:taskId"
}
},
Notifications: "notifications",
Profile: "profile",
},
},
};
export default function App() {
const dimensions = useWindowDimensions();
//const NavigationComponent = dimensions.width < 700 ? BottomNavigation : DrawerNavigation;
return (
<NavigationContainer linking={linking} fallback={<Text>Loading...</Text>}>
<PaperProvider>
<BottomNavigation />
</PaperProvider>
</NavigationContainer>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center",
},
});

View file

@ -1,38 +1,18 @@
# create-svelte
# Musclecat app
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
Routes:
## Creating a project
- `/`: show all root-level lists
- `/list/<id>`: show all direct sublists and tasks of list with `<id>`
- `/task/<id>`: show task info for `<id>`
- `/history`: show all completed tasks
- `/profile`: show profile information and settings
If you're seeing this, you've probably already done this step. Congrats!
on List view (including root):
```bash
# create a new project in the current directory
npm create svelte@latest
- Show action button for picking a random available task
- Gray-out scheduled tasks that are not yet available
# create a new project in my-app
npm create svelte@latest my-app
```
on Task view:
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```bash
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
- Show action button for starting task, if scheduled

30
app/app.json Normal file
View file

@ -0,0 +1,30 @@
{
"expo": {
"name": "app",
"slug": "app",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"assetBundlePatterns": [
"**/*"
],
"ios": {
"supportsTablet": true
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
}
},
"web": {
"favicon": "./assets/favicon.png"
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
app/assets/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
app/assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
app/assets/splash.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

10
app/babel.config.js Normal file
View file

@ -0,0 +1,10 @@
module.exports = function(api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
plugins: [
'@babel/plugin-proposal-export-namespace-from',
'react-native-reanimated/plugin',
],
};
};

View file

@ -1,22 +0,0 @@
import { CapacitorConfig } from '@capacitor/cli';
const appId = 'dev.korz.musclecat';
const appName = 'Musclecat';
const server = process.argv.includes('-hmr') ? {
'url': 'http://192.168.178.25:5173', // always have http:// in url
'cleartext': true
} : {};
const webDir = 'build';
const config: CapacitorConfig = {
appId,
appName,
webDir,
server
};
if (process.argv.includes('-hmr')) console.log('WARNING: running capacitor with livereload config', config);
export default config;

23096
app/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,42 +1,41 @@
{
"name": "app",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite dev --host",
"build": "vite build",
"preview": "vite preview",
"test": "npm run test:integration && npm run test:unit",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check . && eslint .",
"format": "prettier --write .",
"test:integration": "playwright test",
"test:unit": "vitest"
},
"devDependencies": {
"@capacitor/cli": "^5.5.1",
"@playwright/test": "^1.28.1",
"@sveltejs/adapter-static": "^2.0.3",
"@sveltejs/kit": "^1.27.4",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"eslint": "^8.28.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-svelte": "^2.30.0",
"prettier": "^3.0.0",
"prettier-plugin-svelte": "^3.0.0",
"svelte": "^4.2.7",
"svelte-check": "^3.6.0",
"svelte-preprocess": "^5.1.1",
"tslib": "^2.4.1",
"typescript": "^5.0.0",
"vite": "^4.5.1",
"vitest": "^0.32.2"
},
"type": "module",
"dependencies": {
"@capacitor/core": "^5.5.1",
"pocketbase": "^0.19.0"
}
"name": "app",
"version": "1.0.0",
"main": "node_modules/expo/AppEntry.js",
"scripts": {
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web"
},
"dependencies": {
"@babel/plugin-proposal-export-namespace-from": "^7.18.9",
"@expo/webpack-config": "^19.0.0",
"@react-native-masked-view/masked-view": "0.2.9",
"@react-navigation/drawer": "^6.6.6",
"@react-navigation/native": "^6.1.9",
"@react-navigation/stack": "^6.3.20",
"expo": "~49.0.15",
"expo-screen-orientation": "~6.0.6",
"expo-status-bar": "~1.6.0",
"pocketbase": "^0.20.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-native": "0.72.6",
"react-native-gesture-handler": "~2.12.0",
"react-native-paper": "^5.11.5",
"react-native-reanimated": "~3.3.0",
"react-native-render-html": "^6.3.4",
"react-native-safe-area-context": "4.6.3",
"react-native-screens": "~3.22.0",
"react-native-web": "~0.19.6",
"react-responsive": "^9.0.2",
"swr": "^2.2.4"
},
"devDependencies": {
"@babel/core": "^7.20.0",
"@types/react": "~18.2.14",
"typescript": "^5.1.3"
},
"private": true
}

View file

@ -1,12 +0,0 @@
import type { PlaywrightTestConfig } from '@playwright/test';
const config: PlaywrightTestConfig = {
webServer: {
command: 'npm run build && npm run preview',
port: 4173
},
testDir: 'tests',
testMatch: /(.+\.)?(test|spec)\.[jt]s/
};
export default config;

View file

@ -0,0 +1,42 @@
import { createMaterialBottomTabNavigator } from "react-native-paper/react-navigation";
import MaterialCommunityIcons from "@expo/vector-icons/MaterialCommunityIcons";
import { ScreenList, Notifications, Profile } from "./screens";
const Tab = createMaterialBottomTabNavigator();
export function BottomNavigation() {
return (
<Tab.Navigator initialRouteName="Home">
<Tab.Screen
name="Home"
component={ScreenList}
options={{
tabBarLabel: "Home",
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons name="home" color={color} size={26} />
),
}}
/>
<Tab.Screen
name="Notifications"
component={Notifications}
options={{
tabBarLabel: "History",
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons name="history" color={color} size={26} />
),
}}
/>
<Tab.Screen
name="Profile"
component={Profile}
options={{
tabBarLabel: "Profile",
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons name="account" color={color} size={26} />
),
}}
/>
</Tab.Navigator>
);
}

View file

@ -0,0 +1,20 @@
import * as React from "react";
import { createDrawerNavigator } from "@react-navigation/drawer";
import { ScreenList, Notifications, Profile } from "./screens";
const Drawer = createDrawerNavigator();
export function DrawerNavigation() {
return (
<Drawer.Navigator
initialRouteName="Feed"
screenOptions={{
drawerType: "permanent",
}}
>
<Drawer.Screen name="Feed" component={ScreenList} />
<Drawer.Screen name="Notifications" component={Notifications} />
<Drawer.Screen name="Profile" component={Profile} />
</Drawer.Navigator>
);
}

4
app/src/api/index.ts Normal file
View file

@ -0,0 +1,4 @@
import PocketBase from "pocketbase";
import type { TypedPocketBase } from "./pocketbase-types";
export const pb = new PocketBase("http://127.0.0.1:8090") as TypedPocketBase;

12
app/src/app.d.ts vendored
View file

@ -1,12 +0,0 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface Platform {}
}
}
export {};

View file

@ -1,16 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Segoe+UI:wght@400;600&display=swap"
/>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View file

@ -1,7 +0,0 @@
import { describe, it, expect } from 'vitest';
describe('sum test', () => {
it('adds 1 + 2 to equal 3', () => {
expect(1 + 2).toBe(3);
});
});

View file

@ -1,4 +0,0 @@
import PocketBase from "pocketbase";
import type { TypedPocketBase } from "./pocketbase-types"
export const pb = new PocketBase('http://127.0.0.1:8090') as TypedPocketBase

View file

@ -1 +0,0 @@
// place files you want to import through the `$lib` alias in this folder.

View file

@ -1,46 +0,0 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { pb } from '$lib/api';
import '../theme/global.css';
/* Theme variables */
import '../theme/variables.css';
async function goToRandomTask() {
const task = await pb
.collection('tasks')
.getFirstListItem('', { sort: '@random', fields: 'id' });
goto(`/tasks/${task.id}`);
}
let listsLoading = pb.collection("lists").getList(1, 50);
</script>
<div class="topbar">
<div class="app-name">
<img width="30" height="30" alt="Musclecat logo" src="/logo.webp" />
<h1>Musceclat</h1>
</div>
<div class="search">
<input type="search" placeholder="Search tasks" />
<button on:click={goToRandomTask}>Zufällig</button>
</div>
</div>
<div class="container">
<div class="sidebar">
<div class="sidebar-heading">Lists</div>
{#await listsLoading}
<div class="list-name selected">Loading...</div>
{:then lists}
{#each lists.items as list}
<div class="list-name">{list.name}</div>
{/each}
{/await}
<div class="progress-bar-container">
<div class="progress-bar">70%</div>
</div>
</div>
<div class="content">
<slot />
</div>
</div>

View file

@ -1 +0,0 @@
export const ssr = false;

View file

@ -1,18 +0,0 @@
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
let { tasks } = data;
</script>
<h2>Haushalt</h2>
<ul class="task-list">
{#each tasks.items as task}
<li>
{task.name}
<span>{task.expand?.list.name}</span>
</li>
{/each}
</ul>
<button>Neue Aufgabe</button>

View file

@ -1,30 +0,0 @@
import { pb } from '$lib/api';
import { ClientResponseError, type ListResult } from 'pocketbase';
import type { PageLoad } from './$types';
import { error } from '@sveltejs/kit';
import type { IconsResponse, ListsResponse, TasksResponse } from '$lib/pocketbase-types';
interface Expand {
icon?: IconsResponse;
list: ListsResponse;
}
export const load: PageLoad = async function () {
try {
const tasks: ListResult<TasksResponse<Expand>> = await pb.collection('tasks').getList(1, 50, {
expand: "icon,list"
});
/*const icon = task.expand?.icon;
const iconUrl =
icon &&
pb.files.getUrl(icon, icon.image, {
thumb: '100x100'
});*/
return { tasks };
} catch (ex) {
if (ex instanceof ClientResponseError) {
throw error(ex.status, ex.response.message);
}
throw ex;
}
};

View file

@ -1,51 +0,0 @@
<script lang="ts">
import type { ListsResponse } from '$lib/pocketbase-types';
import type { PageData } from './$types';
export let data: PageData;
const rootLists = data.lists.filter((l) => !l.parent);
const children = data.lists.reduce((acc, l) => {
if (acc.has(l.parent)) {
acc.get(l.parent)?.push(l);
} else {
acc.set(l.parent, [l]);
}
return acc;
}, new Map<string, ListsResponse[]>());
// TODO: Replace with navigation logic, such as using a Svelte store or a Router
const goToTasks = (taskId: string) => {
console.log(`Redirecting to tasks for list ${taskId}`);
};
</script>
<ion-header>
<ion-toolbar>
<ion-title>Task Lists</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list>
{#each rootLists as list}
<ion-item lines="full">
<ion-label>
<h2>{list.name}</h2>
<ion-list>
{#each children.get(list.id) as childList}
<ion-item lines="full">
<ion-label>
<h2>{childList.name}</h2>
</ion-label>
</ion-item>
{/each}
</ion-list>
</ion-label>
</ion-item>
{/each}
</ion-list>
</ion-content>
<style>
/* Additional styles if necessary */
</style>

View file

@ -1,7 +0,0 @@
import { pb } from '$lib/api';
import type { PageLoad } from './$types';
export const load: PageLoad = async function () {
const lists = await pb.collection('lists').getFullList();
return { lists };
};

View file

@ -1,99 +0,0 @@
<script lang="ts">
import { pb } from '$lib/api';
let email = '';
let password = '';
const handleLogin = async (event: Event) => {
event.preventDefault();
email = email.trim();
password = password.trim();
if (email && password) {
const resp = await pb.collection('users').authWithPassword(email, password);
console.log('Logged in:', resp);
}
};
</script>
<form class="login-container" on:submit={handleLogin}>
<h1>Musceclat Login</h1>
<div class="form-group">
<label for="email">E-Mail:</label>
<input
type="email"
id="email"
name="email"
placeholder="Enter your email"
required
bind:value={email}
/>
</div>
<div class="form-group">
<label for="password">Password:</label>
<input
type="password"
id="password"
name="password"
placeholder="Enter your password"
required
bind:value={password}
/>
</div>
<button type="submit">Login</button>
</form>
<style>
.login-container {
background-color: #ffffff;
padding: 40px;
border-radius: 8px;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2);
width: 100%;
max-width: 400px;
}
.login-container h1 {
font-size: 2em;
color: #202020;
margin-bottom: 20px;
text-align: center;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
color: #606060;
margin-bottom: 5px;
}
.form-group input {
width: 100%;
padding: 15px;
border: none;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
font-size: 16px;
}
button {
background-color: #0078d4;
color: white;
padding: 15px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
width: 100%;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
transition: background-color 0.3s;
}
button:hover {
background-color: #005ea6;
}
</style>

View file

@ -1,9 +0,0 @@
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
</script>
<h2>Task: {data.task.name}</h2>
<img width="100" height="100" alt="icon" src={data.iconUrl} />
{@html data.task.description}

View file

@ -1,27 +0,0 @@
import { pb } from '$lib/api';
import { ClientResponseError } from 'pocketbase';
import type { PageLoad } from './$types';
import { error } from '@sveltejs/kit';
import type { IconsResponse, TasksResponse } from '$lib/pocketbase-types';
interface Expand {
icon?: IconsResponse;
}
export const load: PageLoad = async function ({ params: { id } }) {
try {
const task: TasksResponse<Expand> = await pb.collection('tasks').getOne(id, { expand: 'icon' });
const icon = task.expand?.icon;
const iconUrl =
icon &&
pb.files.getUrl(icon, icon.image, {
thumb: '100x100'
});
return { task, iconUrl };
} catch (ex) {
if (ex instanceof ClientResponseError) {
throw error(ex.status, ex.response.message);
}
throw ex;
}
};

262
app/src/screens/List.tsx Normal file
View file

@ -0,0 +1,262 @@
import { StyleSheet, ScrollView, View } from "react-native";
import {
Appbar,
Text,
Avatar,
IconButton,
Card,
List,
useTheme,
} from "react-native-paper";
import { createStackNavigator } from "@react-navigation/stack";
import RenderHtml from "react-native-render-html";
import { HomeScreenNavigationProp } from "./types";
import useSWR from "swr";
import { pb } from "../api";
function ListItem(props: { name: string; onPress(): void }) {
return (
<Card
style={{ marginHorizontal: 8, marginBottom: 8 }}
onPress={props.onPress}
>
<Card.Title
title={props.name}
subtitle="A list"
left={(props) => <Avatar.Icon {...props} icon="clipboard-list" />}
right={(props) => (
<IconButton {...props} icon="dots-vertical" onPress={() => {}} />
)}
/>
</Card>
);
}
function TaskItem(props: { name: string; onPress(): void }) {
return (
<List.Item
title={props.name}
description="A task"
left={(props) => <List.Icon {...props} icon="clipboard-check-outline" />}
onPress={props.onPress}
/>
);
}
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">) {
const theme = useTheme();
const { listId } = route.params ?? {};
const { isLoading, data, error } = useSWR(listId || "all", listFetcher);
if (isLoading) {
return (
<View
style={[styles.padded, { backgroundColor: theme.colors.background }]}
>
<Text>Loading...</Text>
</View>
);
}
if (error || !data) {
return (
<View
style={[styles.padded, { backgroundColor: theme.colors.background }]}
>
<Text>Something went wrong: {error.toString()}</Text>
</View>
);
}
const { name, parentId, lists, tasks } = data;
return (
<>
<Appbar.Header elevated>
{Boolean(listId) && (
<Appbar.BackAction
onPress={() =>
navigation.canGoBack()
? navigation.pop()
: navigation.replace("List", { listId: parentId })
}
/>
)}
<Appbar.Content title={name} />
<Appbar.Action icon="dice-multiple-outline" onPress={() => {}} />
<Appbar.Action icon="magnify" onPress={() => {}} />
</Appbar.Header>
<ScrollView
style={[styles.container, { backgroundColor: theme.colors.background }]}
>
{lists.map((l) => (
<ListItem
key={l.id}
name={l.name}
onPress={() => navigation.push("List", { listId: l.id })}
/>
))}
{tasks.length > 0 && (
<List.Section>
<List.Subheader>Tasks</List.Subheader>
{tasks.map((t) => (
<TaskItem
key={t.id}
name={t.name}
onPress={() => navigation.push("Task", { taskId: t.id })}
/>
))}
</List.Section>
)}
</ScrollView>
</>
);
}
interface TaskResponse {
id: string;
name: string;
description: string;
listId: string;
}
async function taskFetcher(id: string): Promise<TaskResponse> {
const task = await pb.collection("tasks").getOne(id);
return {
id,
name: task.name,
description: task.description,
listId: task.list,
};
}
function TaskContent({ route, navigation }: HomeScreenNavigationProp<"Task">) {
const theme = useTheme();
const { taskId } = route.params;
const { isLoading, data, error } = useSWR(taskId, taskFetcher);
if (isLoading) {
return (
<View
style={[styles.padded, { backgroundColor: theme.colors.background }]}
>
<Text>Loading...</Text>
</View>
);
}
if (error || !data) {
return (
<View
style={[styles.padded, { backgroundColor: theme.colors.background }]}
>
<Text>Something went wrong: {error.toString()}</Text>
</View>
);
}
const { name, description, listId } = data;
return (
<>
<Appbar.Header elevated>
<Appbar.BackAction
onPress={() =>
navigation.canGoBack()
? navigation.pop()
: navigation.replace("List", { listId })
}
/>
<Appbar.Content title={name} />
</Appbar.Header>
<View
style={[styles.padded, { backgroundColor: theme.colors.background }]}
>
<Text variant="bodyLarge">
<RenderHtml source={{ html: description }} />
</Text>
</View>
</>
);
}
const Stack = createStackNavigator();
export function ScreenList() {
return (
<Stack.Navigator
initialRouteName="List"
screenOptions={{ headerShown: false }}
>
<Stack.Screen
name="List"
component={ListContent as any}
options={{ headerShown: false }}
/>
<Stack.Screen
name="Task"
component={TaskContent as any}
options={{ headerShown: false }}
/>
</Stack.Navigator>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
paddingVertical: 8,
//backgroundColor: "#fff",
/*alignItems: "center",
justifyContent: "center",*/
},
padded: {
padding: 16,
flex: 1,
},
});

View file

@ -0,0 +1,20 @@
import { StyleSheet, View } from "react-native";
import { Text } from "react-native-paper";
export function Notifications() {
return (
<View style={styles.container}>
<Text variant="headlineMedium">Updates!</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center",
},
});

View file

@ -0,0 +1,32 @@
import { StyleSheet, View } from "react-native";
import { useNavigation } from "@react-navigation/native";
import { Text } from "react-native-paper";
import { Appbar } from "react-native-paper";
export function Profile() {
const navigation = useNavigation();
return (
<>
<Appbar.Header>
{navigation.canGoBack() && <Appbar.BackAction onPress={() => navigation.goBack()} />}
<Appbar.Content title="Title" />
<Appbar.Action icon="calendar" onPress={() => navigation.navigate("Profile")} />
<Appbar.Action icon="magnify" onPress={() => {}} />
</Appbar.Header>
<View style={styles.container}>
<Text variant="headlineMedium">Profile!</Text>
</View>
</>
);
}
const styles = StyleSheet.create({
container: {
/*flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center",*/
},
});

3
app/src/screens/index.ts Normal file
View file

@ -0,0 +1,3 @@
export * from "./List";
export * from "./Notifications";
export * from "./Profile";

27
app/src/screens/types.ts Normal file
View file

@ -0,0 +1,27 @@
import type {
NavigatorScreenParams,
ParamListBase,
} from "@react-navigation/native";
import type { StackScreenProps } from "@react-navigation/stack";
export interface RootBottomParamList extends ParamListBase {
Home: NavigatorScreenParams<HomeStackParamList>;
Notifications: undefined;
Profile: undefined;
Task: { id: string };
}
interface HomeStackParamList extends ParamListBase {
List: { listId: string | null };
Task: { taskId: string | null };
}
export type HomeScreenNavigationProp<
RouteName extends keyof HomeStackParamList = keyof HomeStackParamList,
> = StackScreenProps<HomeStackParamList, RouteName>;
declare global {
namespace ReactNavigation {
interface RootParamList extends RootBottomParamList {}
}
}

View file

@ -1,178 +0,0 @@
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: "Segoe UI", sans-serif;
background-color: #f3f3f3;
color: #202020;
padding: 10px;
}
.container {
display: flex;
height: calc(100vh - 20px);
}
.sidebar {
flex: 0 0 300px;
background-color: #ffffff;
padding: 20px;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2);
margin-right: 20px;
border-radius: 8px;
overflow: hidden;
}
.sidebar-heading {
font-size: 24px;
color: #202020;
margin-bottom: 20px;
font-weight: 600;
}
.list-name {
padding: 10px 0;
cursor: pointer;
border-radius: 4px;
transition: background-color 0.3s;
}
.list-name:hover {
background-color: #f0f0f0;
}
.progress-bar-container {
width: 100%;
background-color: #e1e1e1;
border-radius: 10px;
margin: 20px 0;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2);
}
.progress-bar {
height: 20px;
background-color: #0078d4;
width: 70%;
/* Dynamic value here */
border-radius: 8px;
transition: width 0.5s ease-in-out;
display: flex;
align-items: center;
justify-content: center;
color: #ffffff;
font-size: 12px;
}
.topbar {
display: flex;
align-items: center;
margin-bottom: 20px;
}
.app-name {
display: flex;
align-items: center;
font-weight: 600;
font-size: 1.5em;
color: #202020;
margin-right: 20px;
}
h1 {
font-size: 1em;
}
.app-logo {
width: 36px;
height: 36px;
margin-right: 10px;
fill: currentColor;
}
.search {
display: flex;
flex: 1;
align-items: center;
}
.search input {
padding: 15px;
border: none;
border-radius: 8px;
font-size: 16px;
flex: 1;
margin-right: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.search button {
background-color: #0078d4;
border: none;
color: white;
padding: 15px;
font-size: 16px;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: background-color 0.3s;
}
.search button:hover {
background-color: #005ea6;
}
.content {
flex: 1;
padding: 20px;
background-color: #ffffff;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2);
border-radius: 8px;
}
.content h2 {
font-size: 24px;
color: #202020;
margin-bottom: 20px;
font-weight: 600;
}
.task-list li {
list-style: none;
background-color: #f9f9f9;
padding: 15px;
margin: 10px 0;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
transition: background-color 0.3s;
}
.task-list li:hover {
background-color: #f3f3f3;
}
.task-list li span {
display: block;
color: #606060;
margin-top: 5px;
font-size: 14px;
font-weight: 400;
}
.content button {
background-color: #0078d4;
border: none;
color: white;
padding: 15px;
font-size: 16px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
cursor: pointer;
transition: background-color 0.3s;
}
.content button:hover {
background-color: #005ea6;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 326 KiB

View file

@ -1,18 +0,0 @@
import adapter from '@sveltejs/adapter-static';
import preprocess from 'svelte-preprocess';
/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: preprocess(),
kit: {
adapter: adapter({
pages: 'build',
assets: 'build',
fallback: 'index.html',
precompress: false
})
}
};
export default config;

View file

@ -1,6 +0,0 @@
import { expect, test } from '@playwright/test';
test('index page has expected h1', async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('heading', { name: 'Welcome to SvelteKit' })).toBeVisible();
});

View file

@ -1,18 +1,7 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
"extends": "expo/tsconfig.base",
"compilerOptions": {
"strict": true
},
"include": ["src", "types"]
}

View file

@ -1,9 +0,0 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vitest/config';
export default defineConfig({
plugins: [sveltekit()],
test: {
include: ['src/**/*.{test,spec}.{js,ts}']
}
});