Skip to content

Commit

Permalink
Merge pull request #47 from richardguerre/mobile-pwa
Browse files Browse the repository at this point in the history
Add app/mobile-pwa + full integration into server
  • Loading branch information
richardguerre authored Aug 25, 2024
2 parents 9243593 + 0909c60 commit b75ea70
Show file tree
Hide file tree
Showing 95 changed files with 6,913 additions and 2,095 deletions.
6 changes: 4 additions & 2 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,12 @@ apps/server/plugins/*
!apps/server/plugins/.gitkeep

# ignoring schema.graphql as it's genrated from the server and doesn't need to be formatted
apps/web/src/relay/schema.graphql
packages/relay/schema.graphql

# ignoring generated files from relay-compiler (copied from apps/web/.gitignore)
apps/web/src/relay/__generated__
apps/mobile-pwa/src/relay/__generated__

# ignoring uno.css to keep the bundle size small
apps/web/uno.css
apps/web/uno.css
apps/mobile-pwa/uno.css
25 changes: 25 additions & 0 deletions apps/mobile-pwa/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
dev-dist
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
30 changes: 30 additions & 0 deletions apps/mobile-pwa/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# React + TypeScript + Vite

This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.

Currently, two official plugins are available:

- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh

## Expanding the ESLint configuration

If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:

- Configure the top-level `parserOptions` property like this:

```js
export default {
// other rules...
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: ['./tsconfig.json', './tsconfig.node.json'],
tsconfigRootDir: __dirname,
},
}
```

- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
86 changes: 86 additions & 0 deletions apps/mobile-pwa/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Set theme color to background-200 in hex format. -->
<meta name="theme-color" content="#e5e7eb" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@unocss/reset/tailwind.min.css" />
<title>Flow</title>
<!-- the follow 2 style tags are manual copies of what unocss generated in the Kanban View. It is missing some classes, but already prevents a lot of jerkiness coming from missing styles when loading the page. -->
<style>
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
</style>
<link rel="stylesheet" href="./uno.css" />
</head>
<body class="bg-background-200 text-foreground-900">
<div id="root">
<!-- Initial loading indicator shown to avoid white screen. Copy-pasted from Loading.tsx. -->
<div class="flex h-screen w-full items-center justify-center">
<div class="ring-primary-500/10 ring-5 rounded-full p-1">
<div class="relative h-24 w-24 overflow-clip rounded-full">
<svg
width="246"
height="113"
viewBox="0 0 246 113"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="absolute right-0 transform translate-y-24"
>
<path
d="M41 14.6066C27.3331 14.6066 13.6669 0.0610556 0 0.0205112V113H246V0.0205112L245.998 0.0214844C232.332 8.44216 218.666 16.8624 205 14.6066C191.333 12.3498 177.667 -0.58383 164 0.0205112C150.333 0.624852 136.667 14.7672 123 14.6066C109.333 14.4451 95.6669 -0.0200333 82 0.0205112C68.3331 0.0610556 54.6669 14.6066 41 14.6066Z"
fill="url(#bg-wave-gradient)"
/>
<defs>
<linearGradient
id="bg-wave-gradient"
x1="0"
y1="0"
x2="0"
y2="50"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="currentColor" class="text-primary-300" />
<stop offset="1" stopColor="currentColor" class="text-primary-400" />
</linearGradient>
</defs>
</svg>
<svg
width="246"
height="113"
viewBox="0 0 246 113"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="absolute left-0 transform translate-y-24"
>
<path
d="M41 14.6066C27.3331 14.6066 13.6669 0.0610556 0 0.0205112V113H246V0.0205112L245.998 0.0214844C232.332 8.44216 218.666 16.8624 205 14.6066C191.333 12.3498 177.667 -0.58383 164 0.0205112C150.333 0.624852 136.667 14.7672 123 14.6066C109.333 14.4451 95.6669 -0.0200333 82 0.0205112C68.3331 0.0610556 54.6669 14.6066 41 14.6066Z"
fill="url(#fg-wave-gradient)"
/>
<defs>
<linearGradient
id="fg-wave-gradient"
x1="0"
y1="0"
x2="0"
y2="113"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="currentColor" class="text-primary-200" />
<stop offset="1" stopColor="currentColor" class="text-primary-400" />
</linearGradient>
</defs>
</svg>
</div>
</div>
</div>
</div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>
50 changes: 50 additions & 0 deletions apps/mobile-pwa/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"name": "@flowdev/mobile-pwa",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"setup:relay": "mkdir -p src/relay/__generated__",
"relay": "cd ../.. && relay-compiler && echo '✅ Relay compiled!'",
"relay:watch": "cd ../.. && relay-compiler --watch",
"dev": "bun run relay && bunx --bun vite --port 5173",
"print-schema": "cd ../server && DATABASE_URL=this_makes_it_run_wo_server_envs ORIGIN=same_as_db_url bun run print-schema",
"prebuild": "bun run print-schema && bun run setup:relay && bun run relay",
"build": "bunx vite -v && bunx rollup -v && NODE_ENV=production bunx --bun vite build",
"preview": "vite preview",
"env:example": "cp -n .env.example .env || true",
"env:codespaces": "cp .env.codespaces .env"
},
"dependencies": {
"@flowdev/error-boundary": "workspace:*",
"@flowdev/icons": "workspace:*",
"@flowdev/plugin": "workspace:*",
"@flowdev/relay": "workspace:*",
"@flowdev/tiptap": "workspace:*",
"@flowdev/ui": "workspace:*",
"@flowdev/unocss": "workspace:*",
"@unocss/runtime": "0.60.3",
"dayjs": "1.11.7",
"framer-motion": "10.12.10",
"graphql-sse": "2.3.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.52.1",
"react-router-dom": "6.6.2",
"react-sortablejs": "6.1.4",
"sortablejs": "1.15.0"
},
"devDependencies": {
"@types/react": "^18.3.1",
"@types/react-dom": "^18.3.0",
"@vite-pwa/assets-generator": "^0.2.4",
"@vitejs/plugin-react": "^4.2.1",
"eslint": "^8.57.0",
"sharp": "0.33.5",
"sharp-ico": "0.1.5",
"typescript": "^5.2.2",
"vite": "^5.2.10",
"vite-plugin-pwa": "^0.20.0",
"workbox-core": "^7.1.0"
}
}
5 changes: 5 additions & 0 deletions apps/mobile-pwa/public/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions apps/mobile-pwa/pwa-assets.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { defineConfig, minimal2023Preset as preset } from "@vite-pwa/assets-generator/config";

export default defineConfig({
headLinkOptions: {
preset: "2023",
},
preset,
images: ["public/favicon.svg"],
});
1 change: 1 addition & 0 deletions apps/mobile-pwa/src/assets/react.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
168 changes: 168 additions & 0 deletions apps/mobile-pwa/src/components/Day.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { useEffect, useState } from "react";
import { graphql, useFragment, useMutation } from "@flowdev/relay";
import { Day_day$key } from "@flowdev/mobile-pwa/relay/__generated__/Day_day.graphql";
import { TaskCard } from "./TaskCard";
import { dayjs } from "@flowdev/mobile-pwa/dayjs";
import { ReactSortable, Sortable } from "react-sortablejs";
import { DayContent_day$key } from "@flowdev/mobile-pwa/relay/__generated__/DayContent_day.graphql";
import { DayUpdateTaskDateMutation } from "@flowdev/mobile-pwa/relay/__generated__/DayUpdateTaskDateMutation.graphql";
import { environment } from "@flowdev/mobile-pwa/relay/environment";

type DayProps = {
day: Day_day$key;
};

export const Day = (props: DayProps) => {
const day = useFragment(
graphql`
fragment Day_day on Day {
date
...DayContent_day
}
`,
props.day,
);

return (
<div className="flex h-full flex-col shrink-0">
<div className="px-4 pt-4">
<div className="active:text-primary-600 text-3xl font-semibold">
{dayOfWeekArr[dayjs(day.date).day()]}
</div>
<div className="text-foreground-700 text-base">{dayjs(day.date).format("MMMM D")}</div>
</div>
<DayContent day={day} />
</div>
);
};

type UpdateTaskDateInfo = {
movedTaskId: string;
htmlParent: HTMLElement;
} | null;

type DayContentProps = {
day: DayContent_day$key;
};

export const DayContent = (props: DayContentProps) => {
const day = useFragment(
graphql`
fragment DayContent_day on Day {
date
tasks {
__typename
id
...TaskCard_task
}
}
`,
props.day,
);

const [udpateTaskDate] = useMutation<DayUpdateTaskDateMutation>(graphql`
mutation DayUpdateTaskDateMutation($input: MutationUpdateTaskDateInput!) {
updateTaskDate(input: $input) {
...Day_day
}
}
`);

const [tasks, setTasks] = useState(structuredClone(Array.from(day.tasks)));
const [updateTaskDateInfo, setUpdateTaskDateInfo] = useState<UpdateTaskDateInfo>(null);

const handleTaskMove = (e: Sortable.SortableEvent) => {
setUpdateTaskDateInfo({
htmlParent: e.to,
movedTaskId: e.item.id,
});
};

const setList = async (newList: typeof tasks) => {
setTasks(newList.filter((task) => task.__typename === "Task")); // ignore item(s) that were dropped
return;
};

useEffect(() => {
setTasks(structuredClone(Array.from(day.tasks)));
}, [day.tasks]);

useEffect(() => {
if (!updateTaskDateInfo) return; // as the Lists component may be super-imposed on top of the Day component (in the IndexView), the Day component is still a drop target but needs to be ignored as the user is trying to move the task to the Lists component
const newTasksOrder = Array.from(updateTaskDateInfo.htmlParent.children).map((task) => task.id);

udpateTaskDate({
variables: {
input: {
id: updateTaskDateInfo.movedTaskId,
date: updateTaskDateInfo.htmlParent.id,
newTasksOrder,
},
},
});

setUpdateTaskDateInfo(null);
}, [updateTaskDateInfo]);

return (
<ReactSortable
id={day.date}
className="no-scrollbar mt-4 flex flex-auto flex-col gap-4 overflow-y-scroll px-4 pb-4"
list={tasks}
setList={setList}
animation={150}
delayOnTouchOnly
delay={100}
group="shared"
onEnd={handleTaskMove}
>
{tasks.map((task) => (
<TaskCard key={task.id} task={task} />
))}
</ReactSortable>
);
};

const dayOfWeekArr = [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
] as const;

export const createVirtualTask = (props: { date: string }) => {
const tempId = `Task_${Math.random()}`;
environment.commitUpdate((store) => {
const createdTask = store
.create(tempId, "Task")
.setValue(tempId, "id")
.setValue("", "title")
.setValue(new Date().toISOString(), "createdAt")
.setValue("TODO", "status")
.setValue(null, "completedAt")
.setValue(props.date, "date")
.setValue(null, "item")
.setValue(null, "durationInMinutes")
.setLinkedRecords([], "pluginDatas")
.setLinkedRecords([], "subtasks");

const dayRecord = store.get(`Day_${props.date}`);
const dayTasks = dayRecord?.getLinkedRecords("tasks");
// This adds the new task to the top of the list
dayRecord?.setLinkedRecords([createdTask, ...(dayTasks ?? [])], "tasks");
});
};

export const deleteVirtualTask = (tempId: string) => {
environment.commitUpdate((store) => {
const task = store.get(tempId);
if (!task) return;
const day = store.get(`Day_${task.getValue("date")}`);
const dayTasks = day?.getLinkedRecords("tasks");
day?.setLinkedRecords(dayTasks?.filter((task) => task.getDataID() !== tempId) ?? [], "tasks");
// store.delete(tempId); this causes a render issue so for now I'm not including it.
});
};
Loading

0 comments on commit b75ea70

Please sign in to comment.