zenstack 0.1.42 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/.vscode/extensions.json +7 -0
  2. package/.vscode/launch.json +49 -0
  3. package/.vscode/settings.json +4 -0
  4. package/README.md +1 -0
  5. package/package.json +8 -90
  6. package/packages/internal/jest.config.ts +32 -0
  7. package/packages/internal/package.json +42 -0
  8. package/packages/internal/src/constants.ts +1 -0
  9. package/packages/internal/src/handler/data/guard-utils.ts +7 -0
  10. package/packages/internal/src/handler/data/handler.ts +415 -0
  11. package/packages/internal/src/handler/data/query-processor.ts +504 -0
  12. package/packages/internal/src/handler/index.ts +1 -0
  13. package/packages/internal/src/handler/types.ts +20 -0
  14. package/packages/internal/src/index.ts +3 -0
  15. package/packages/internal/src/request-handler.ts +27 -0
  16. package/packages/internal/src/request.ts +101 -0
  17. package/packages/internal/src/types.ts +40 -0
  18. package/packages/internal/tests/query-processor.test.ts +172 -0
  19. package/{out/cli/tsconfig.template.json → packages/internal/tsconfig.json} +7 -3
  20. package/packages/runtime/auth.d.ts +1 -0
  21. package/packages/runtime/auth.js +3 -0
  22. package/packages/runtime/hooks.d.ts +10 -0
  23. package/packages/runtime/hooks.js +3 -0
  24. package/packages/runtime/index.d.ts +3 -0
  25. package/packages/runtime/index.js +1 -0
  26. package/packages/runtime/package-lock.json +512 -0
  27. package/packages/runtime/package.json +16 -0
  28. package/packages/runtime/server.d.ts +1 -0
  29. package/packages/runtime/server.js +3 -0
  30. package/packages/runtime/types.d.ts +1 -0
  31. package/packages/runtime/types.js +3 -0
  32. package/packages/schema/.eslintrc.json +13 -0
  33. package/packages/schema/.vscodeignore +4 -0
  34. package/packages/schema/asset/logo-dark.png +0 -0
  35. package/packages/schema/asset/logo-light.png +0 -0
  36. package/{bin → packages/schema/bin}/cli +0 -0
  37. package/packages/schema/jest.config.ts +32 -0
  38. package/packages/schema/langium-config.json +14 -0
  39. package/packages/schema/langium-quickstart.md +41 -0
  40. package/packages/schema/language-configuration.json +30 -0
  41. package/packages/schema/package.json +96 -0
  42. package/packages/schema/src/cli/cli-util.ts +80 -0
  43. package/packages/schema/src/cli/index.ts +64 -0
  44. package/packages/schema/src/extension.ts +76 -0
  45. package/packages/schema/src/generator/constants.ts +5 -0
  46. package/packages/schema/src/generator/index.ts +92 -0
  47. package/{out/generator/next-auth/index.js → packages/schema/src/generator/next-auth/index.ts} +46 -58
  48. package/{out → packages/schema/src}/generator/package.template.json +0 -0
  49. package/packages/schema/src/generator/prisma/expression-writer.ts +352 -0
  50. package/packages/schema/src/generator/prisma/index.ts +32 -0
  51. package/packages/schema/src/generator/prisma/plain-expression-builder.ts +91 -0
  52. package/packages/schema/src/generator/prisma/prisma-builder.ts +366 -0
  53. package/packages/schema/src/generator/prisma/query-gard-generator.ts +208 -0
  54. package/packages/schema/src/generator/prisma/schema-generator.ts +300 -0
  55. package/packages/schema/src/generator/react-hooks/index.ts +181 -0
  56. package/packages/schema/src/generator/service/index.ts +107 -0
  57. package/{out → packages/schema/src}/generator/tsconfig.template.json +0 -0
  58. package/packages/schema/src/generator/types.ts +17 -0
  59. package/packages/schema/src/generator/utils.ts +9 -0
  60. package/packages/schema/src/language-server/generated/ast.ts +603 -0
  61. package/{out/language-server/generated/grammar.js → packages/schema/src/language-server/generated/grammar.ts} +5 -8
  62. package/packages/schema/src/language-server/generated/module.ts +24 -0
  63. package/packages/schema/src/language-server/main.ts +12 -0
  64. package/{out → packages/schema/src}/language-server/stdlib.zmodel +0 -0
  65. package/packages/schema/src/language-server/types.ts +9 -0
  66. package/packages/schema/src/language-server/zmodel-index.ts +33 -0
  67. package/packages/schema/src/language-server/zmodel-linker.ts +409 -0
  68. package/packages/schema/src/language-server/zmodel-module.ts +90 -0
  69. package/packages/schema/src/language-server/zmodel-scope.ts +21 -0
  70. package/packages/schema/src/language-server/zmodel-validator.ts +35 -0
  71. package/packages/schema/src/language-server/zmodel.langium +186 -0
  72. package/packages/schema/src/utils/exec-utils.ts +5 -0
  73. package/packages/schema/src/utils/indent-string.ts +6 -0
  74. package/packages/schema/syntaxes/zmodel.json +57 -0
  75. package/packages/schema/syntaxes/zmodel.tmLanguage.json +57 -0
  76. package/packages/schema/tests/generator/expression-writer.test.ts +676 -0
  77. package/packages/schema/tests/generator/prisma-builder.test.ts +138 -0
  78. package/packages/schema/tests/schema/parser.test.ts +423 -0
  79. package/packages/schema/tests/schema/sample-todo.test.ts +14 -0
  80. package/packages/schema/tests/utils.ts +38 -0
  81. package/packages/schema/tsconfig.json +23 -0
  82. package/pnpm-workspace.yaml +3 -0
  83. package/samples/todo/.env +2 -0
  84. package/samples/todo/.eslintrc.json +3 -0
  85. package/samples/todo/.vscode/launch.json +11 -0
  86. package/samples/todo/README.md +34 -0
  87. package/samples/todo/components/AuthGuard.tsx +17 -0
  88. package/samples/todo/components/Avatar.tsx +22 -0
  89. package/samples/todo/components/BreadCrumb.tsx +44 -0
  90. package/samples/todo/components/ManageMembers.tsx +134 -0
  91. package/samples/todo/components/NavBar.tsx +57 -0
  92. package/samples/todo/components/SpaceMembers.tsx +76 -0
  93. package/samples/todo/components/Spaces.tsx +28 -0
  94. package/samples/todo/components/TimeInfo.tsx +17 -0
  95. package/samples/todo/components/Todo.tsx +72 -0
  96. package/samples/todo/components/TodoList.tsx +77 -0
  97. package/samples/todo/lib/context.ts +31 -0
  98. package/samples/todo/next.config.js +10 -0
  99. package/samples/todo/package-lock.json +7527 -0
  100. package/samples/todo/package.json +45 -0
  101. package/samples/todo/pages/_app.tsx +50 -0
  102. package/samples/todo/pages/api/auth/[...nextauth].ts +83 -0
  103. package/samples/todo/pages/api/zenstack/[...path].ts +16 -0
  104. package/samples/todo/pages/create-space.tsx +114 -0
  105. package/samples/todo/pages/index.tsx +32 -0
  106. package/samples/todo/pages/space/[slug]/[listId]/index.tsx +88 -0
  107. package/samples/todo/pages/space/[slug]/index.tsx +169 -0
  108. package/samples/todo/postcss.config.js +6 -0
  109. package/samples/todo/public/avatar.jpg +0 -0
  110. package/samples/todo/public/favicon.ico +0 -0
  111. package/samples/todo/public/logo.png +0 -0
  112. package/samples/todo/public/vercel.svg +4 -0
  113. package/samples/todo/styles/globals.css +7 -0
  114. package/samples/todo/tailwind.config.js +11 -0
  115. package/samples/todo/tsconfig.json +28 -0
  116. package/samples/todo/types/next-auth.d.ts +14 -0
  117. package/samples/todo/types/next.d.ts +16 -0
  118. package/samples/todo/zenstack/migrations/20221014084317_init/migration.sql +153 -0
  119. package/samples/todo/zenstack/migrations/20221020094651_upate_cli/migration.sql +23 -0
  120. package/samples/todo/zenstack/migrations/migration_lock.toml +3 -0
  121. package/samples/todo/zenstack/schema.prisma +126 -0
  122. package/samples/todo/zenstack/schema.zmodel +161 -0
  123. package/tests/integration/jest.config.ts +16 -0
  124. package/tests/integration/package-lock.json +1081 -0
  125. package/tests/integration/package.json +27 -0
  126. package/tests/integration/tests/operation-coverate.test.ts +563 -0
  127. package/tests/integration/tests/operations.zmodel +69 -0
  128. package/tests/integration/tests/todo-e2e.test.ts +577 -0
  129. package/tests/integration/tests/todo.zmodel +123 -0
  130. package/tests/integration/tests/tsconfig.template.json +10 -0
  131. package/tests/integration/tests/utils.ts +133 -0
  132. package/tests/integration/tsconfig.json +10 -0
  133. package/out/cli/cli-util.js +0 -64
  134. package/out/cli/cli-util.js.map +0 -1
  135. package/out/cli/generator.js +0 -1
  136. package/out/cli/generator.js.map +0 -1
  137. package/out/cli/index.js +0 -46
  138. package/out/cli/index.js.map +0 -1
  139. package/out/cli/package.template.json +0 -10
  140. package/out/extension.js +0 -81
  141. package/out/extension.js.map +0 -1
  142. package/out/generator/constants.js +0 -9
  143. package/out/generator/constants.js.map +0 -1
  144. package/out/generator/data-server/index.js +0 -1
  145. package/out/generator/data-server/index.js.map +0 -1
  146. package/out/generator/index.js +0 -98
  147. package/out/generator/index.js.map +0 -1
  148. package/out/generator/next-auth/index.js.map +0 -1
  149. package/out/generator/prisma/expression-writer.js +0 -287
  150. package/out/generator/prisma/expression-writer.js.map +0 -1
  151. package/out/generator/prisma/index.js +0 -38
  152. package/out/generator/prisma/index.js.map +0 -1
  153. package/out/generator/prisma/plain-expression-builder.js +0 -69
  154. package/out/generator/prisma/plain-expression-builder.js.map +0 -1
  155. package/out/generator/prisma/prisma-builder.js +0 -307
  156. package/out/generator/prisma/prisma-builder.js.map +0 -1
  157. package/out/generator/prisma/query-gard-generator.js +0 -159
  158. package/out/generator/prisma/query-gard-generator.js.map +0 -1
  159. package/out/generator/prisma/schema-generator.js +0 -201
  160. package/out/generator/prisma/schema-generator.js.map +0 -1
  161. package/out/generator/query-guard/index.js +0 -2
  162. package/out/generator/query-guard/index.js.map +0 -1
  163. package/out/generator/react-hooks/index.js +0 -179
  164. package/out/generator/react-hooks/index.js.map +0 -1
  165. package/out/generator/server/data/data-generator.js +0 -376
  166. package/out/generator/server/data/data-generator.js.map +0 -1
  167. package/out/generator/server/data/expression-writer.js +0 -287
  168. package/out/generator/server/data/expression-writer.js.map +0 -1
  169. package/out/generator/server/data/plain-expression-builder.js +0 -69
  170. package/out/generator/server/data/plain-expression-builder.js.map +0 -1
  171. package/out/generator/server/data-generator.js +0 -82
  172. package/out/generator/server/data-generator.js.map +0 -1
  173. package/out/generator/server/expression-writer.js +0 -1
  174. package/out/generator/server/expression-writer.js.map +0 -1
  175. package/out/generator/server/function/function-generator.js +0 -50
  176. package/out/generator/server/function/function-generator.js.map +0 -1
  177. package/out/generator/server/function-generator.js +0 -13
  178. package/out/generator/server/function-generator.js.map +0 -1
  179. package/out/generator/server/index.js +0 -88
  180. package/out/generator/server/index.js.map +0 -1
  181. package/out/generator/server/js-expression-builder.js +0 -1
  182. package/out/generator/server/js-expression-builder.js.map +0 -1
  183. package/out/generator/server/plain-expression-builder.js +0 -1
  184. package/out/generator/server/plain-expression-builder.js.map +0 -1
  185. package/out/generator/server/server-code-generator.js +0 -3
  186. package/out/generator/server/server-code-generator.js.map +0 -1
  187. package/out/generator/server/server-code-writer.js +0 -1
  188. package/out/generator/server/server-code-writer.js.map +0 -1
  189. package/out/generator/service/index.js +0 -133
  190. package/out/generator/service/index.js.map +0 -1
  191. package/out/generator/types.js +0 -10
  192. package/out/generator/types.js.map +0 -1
  193. package/out/generator/utils.js +0 -10
  194. package/out/generator/utils.js.map +0 -1
  195. package/out/language-server/generated/ast.js +0 -386
  196. package/out/language-server/generated/ast.js.map +0 -1
  197. package/out/language-server/generated/grammar.js.map +0 -1
  198. package/out/language-server/generated/module.js +0 -23
  199. package/out/language-server/generated/module.js.map +0 -1
  200. package/out/language-server/main.js +0 -12
  201. package/out/language-server/main.js.map +0 -1
  202. package/out/language-server/types.js +0 -3
  203. package/out/language-server/types.js.map +0 -1
  204. package/out/language-server/zmodel-index.js +0 -38
  205. package/out/language-server/zmodel-index.js.map +0 -1
  206. package/out/language-server/zmodel-linker.js +0 -241
  207. package/out/language-server/zmodel-linker.js.map +0 -1
  208. package/out/language-server/zmodel-module.js +0 -51
  209. package/out/language-server/zmodel-module.js.map +0 -1
  210. package/out/language-server/zmodel-scope.js +0 -30
  211. package/out/language-server/zmodel-scope.js.map +0 -1
  212. package/out/language-server/zmodel-validator.js +0 -25
  213. package/out/language-server/zmodel-validator.js.map +0 -1
  214. package/out/utils/indent-string.js +0 -9
  215. package/out/utils/indent-string.js.map +0 -1
@@ -0,0 +1,17 @@
1
+ import { signIn, useSession } from 'next-auth/react';
2
+
3
+ type Props = {
4
+ children: JSX.Element | JSX.Element[];
5
+ };
6
+
7
+ export default function AuthGuard({ children }: Props) {
8
+ const { status } = useSession();
9
+ if (status === 'loading') {
10
+ return <p>Loading...</p>;
11
+ } else if (status === 'unauthenticated') {
12
+ signIn();
13
+ return <></>;
14
+ } else {
15
+ return <>{children}</>;
16
+ }
17
+ }
@@ -0,0 +1,22 @@
1
+ import { UserIcon } from '@heroicons/react/24/outline';
2
+ import { User } from 'next-auth';
3
+ import Image from 'next/image';
4
+
5
+ type Props = {
6
+ user: User;
7
+ size?: number;
8
+ };
9
+
10
+ export default function Avatar({ user, size }: Props) {
11
+ return (
12
+ <div className="tooltip" data-tip={user.name || user.email}>
13
+ <Image
14
+ src={user.image || '/avatar.jpg'}
15
+ alt="avatar"
16
+ width={size || 32}
17
+ height={size || 32}
18
+ className="rounded-full"
19
+ />
20
+ </div>
21
+ );
22
+ }
@@ -0,0 +1,44 @@
1
+ import { useList } from '@zenstackhq/runtime/hooks';
2
+ import Link from 'next/link';
3
+ import { useRouter } from 'next/router';
4
+ import { useCurrentSpace } from '@lib/context';
5
+
6
+ export default function BreadCrumb() {
7
+ const router = useRouter();
8
+ const space = useCurrentSpace();
9
+ const { get: getList } = useList();
10
+
11
+ const parts = router.asPath.split('/').filter((p) => p);
12
+
13
+ const [base, slug, listId] = parts;
14
+ if (base !== 'space') {
15
+ return <></>;
16
+ }
17
+
18
+ const items: Array<{ text: string; link: string }> = [];
19
+
20
+ items.push({ text: 'Home', link: '/' });
21
+ items.push({ text: space?.name || '', link: `/space/${slug}` });
22
+
23
+ if (listId) {
24
+ const { data } = getList(listId);
25
+ items.push({
26
+ text: data?.title || '',
27
+ link: `/space/${slug}/${listId}`,
28
+ });
29
+ }
30
+
31
+ return (
32
+ <div className="text-sm text-gray-600 breadcrumbs">
33
+ <ul>
34
+ {items.map((item, i) => (
35
+ <li key={i}>
36
+ <Link href={item.link}>
37
+ <a>{item.text}</a>
38
+ </Link>
39
+ </li>
40
+ ))}
41
+ </ul>
42
+ </div>
43
+ );
44
+ }
@@ -0,0 +1,134 @@
1
+ import { PlusIcon, TrashIcon } from '@heroicons/react/24/outline';
2
+ import { useCurrentUser } from '@lib/context';
3
+ import { ServerErrorCode } from '@zenstackhq/internal';
4
+ import { HooksError, useSpaceUser } from '@zenstackhq/runtime/hooks';
5
+ import { Space, SpaceUserRole } from '@zenstackhq/runtime/types';
6
+ import { ChangeEvent, KeyboardEvent, useState } from 'react';
7
+ import { toast } from 'react-toastify';
8
+ import Avatar from './Avatar';
9
+
10
+ type Props = {
11
+ space: Space;
12
+ };
13
+
14
+ export default function ManageMembers({ space }: Props) {
15
+ const [email, setEmail] = useState('');
16
+ const [role, setRole] = useState<SpaceUserRole>(SpaceUserRole.USER);
17
+ const user = useCurrentUser();
18
+
19
+ const { find, create: addMember, del: delMember } = useSpaceUser();
20
+ const { data: members } = find({
21
+ where: {
22
+ spaceId: space.id,
23
+ },
24
+ include: {
25
+ user: true,
26
+ },
27
+ });
28
+
29
+ const inviteUser = async () => {
30
+ try {
31
+ const r = await addMember({
32
+ data: {
33
+ user: {
34
+ connect: {
35
+ email,
36
+ },
37
+ },
38
+ space: {
39
+ connect: {
40
+ id: space.id,
41
+ },
42
+ },
43
+ role,
44
+ },
45
+ });
46
+ console.log('SpaceUser created:', r);
47
+ } catch (err: any) {
48
+ console.error(JSON.stringify(err));
49
+ if (err.info?.code) {
50
+ const { info } = err as HooksError;
51
+ if (info.code === ServerErrorCode.UNIQUE_CONSTRAINT_VIOLATION) {
52
+ toast.error('User is already a member of the space');
53
+ } else if (
54
+ info.code === ServerErrorCode.REFERENCE_CONSTRAINT_VIOLATION
55
+ ) {
56
+ toast.error('User is not found for this email');
57
+ }
58
+ } else {
59
+ toast.error(`Error occurred: ${err}`);
60
+ }
61
+ }
62
+ };
63
+
64
+ const removeMember = async (id: string) => {
65
+ if (confirm(`Are you sure to remove this member from space?`)) {
66
+ await delMember(id);
67
+ }
68
+ };
69
+
70
+ return (
71
+ <div>
72
+ <div className="flex flex-wrap gap-2 items-center mb-8 w-full">
73
+ <input
74
+ type="text"
75
+ placeholder="Type user email and enter to invite"
76
+ className="input input-sm input-bordered flex-grow mr-2"
77
+ value={email}
78
+ onChange={(e: ChangeEvent<HTMLInputElement>) => {
79
+ setEmail(e.currentTarget.value);
80
+ }}
81
+ onKeyUp={(e: KeyboardEvent<HTMLInputElement>) => {
82
+ if (e.key === 'Enter') {
83
+ inviteUser();
84
+ }
85
+ }}
86
+ />
87
+
88
+ <select
89
+ className="select select-sm mr-2"
90
+ value={role}
91
+ onChange={(e: ChangeEvent<HTMLSelectElement>) => {
92
+ setRole(e.currentTarget.value as SpaceUserRole);
93
+ }}
94
+ >
95
+ <option value={SpaceUserRole.USER}>USER</option>
96
+ <option value={SpaceUserRole.ADMIN}>ADMIN</option>
97
+ </select>
98
+
99
+ <button>
100
+ <PlusIcon className="w-6 h-6 text-gray-500" />
101
+ </button>
102
+ </div>
103
+
104
+ <ul className="space-y-2">
105
+ {members?.map((member) => (
106
+ <li
107
+ key={member.id}
108
+ className="flex flex-wrap w-full justify-between"
109
+ >
110
+ <div className="flex items-center">
111
+ <div className="hidden md:block mr-2">
112
+ <Avatar user={member.user} size={32} />
113
+ </div>
114
+ <p className="w-36 md:w-48 line-clamp-1 mr-2">
115
+ {member.user.name || member.user.email}
116
+ </p>
117
+ <p>{member.role}</p>
118
+ </div>
119
+ <div className="flex items-center">
120
+ {user?.id !== member.user.id && (
121
+ <TrashIcon
122
+ className="w-4 h-4 text-gray-500"
123
+ onClick={() => {
124
+ removeMember(member.id);
125
+ }}
126
+ />
127
+ )}
128
+ </div>
129
+ </li>
130
+ ))}
131
+ </ul>
132
+ </div>
133
+ );
134
+ }
@@ -0,0 +1,57 @@
1
+ import Image from 'next/image';
2
+ import Avatar from './Avatar';
3
+ import { signOut } from 'next-auth/react';
4
+ import Link from 'next/link';
5
+ import { Space } from '@zenstackhq/runtime/types';
6
+ import { User } from 'next-auth';
7
+
8
+ type Props = {
9
+ space: Space | undefined;
10
+ user: User | undefined;
11
+ };
12
+
13
+ export default function NavBar({ user, space }: Props) {
14
+ return (
15
+ <div className="navbar bg-base-100 px-8 py-2 border-b">
16
+ <div className="flex-1">
17
+ <Link href="/">
18
+ <a className="flex items-center">
19
+ <Image
20
+ src="/logo.png"
21
+ alt="Logo"
22
+ width={32}
23
+ height={32}
24
+ />
25
+ <div className="text-xl font-semibold ml-2 text-slate-700 hidden md:inline-block">
26
+ {space?.name || 'Welcome Todo App'}
27
+ </div>
28
+ <p className="text-xs ml-2 text-gray-500 self-end">
29
+ Powered by ZenStack
30
+ </p>
31
+ </a>
32
+ </Link>
33
+ </div>
34
+ <div className="flex-none">
35
+ <div className="dropdown dropdown-end">
36
+ <label
37
+ tabIndex={0}
38
+ className="btn btn-ghost btn-circle avatar"
39
+ >
40
+ {user && <Avatar user={user!} />}
41
+ </label>
42
+ <ul
43
+ tabIndex={0}
44
+ className="menu menu-compact dropdown-content mt-3 p-2 shadow bg-base-100 rounded-box w-52"
45
+ >
46
+ <li className="border-b border-gray-200">
47
+ {user && <div>{user.name || user.email}</div>}
48
+ </li>
49
+ <li>
50
+ <a onClick={() => signOut()}>Logout</a>
51
+ </li>
52
+ </ul>
53
+ </div>
54
+ </div>
55
+ </div>
56
+ );
57
+ }
@@ -0,0 +1,76 @@
1
+ import { useSpaceUser } from '@zenstackhq/runtime/hooks';
2
+ import { useCurrentSpace } from '@lib/context';
3
+ import { PlusIcon } from '@heroicons/react/24/outline';
4
+ import Avatar from './Avatar';
5
+ import ManageMembers from './ManageMembers';
6
+ import { Space } from '@zenstackhq/runtime/types';
7
+
8
+ function ManagementDialog(space?: Space) {
9
+ if (!space) return undefined;
10
+ return (
11
+ <>
12
+ <label htmlFor="management-modal" className="modal-button">
13
+ <PlusIcon className="w-6 h-6 text-gray-500 cursor-pointer mr-1" />
14
+ </label>
15
+
16
+ <input
17
+ type="checkbox"
18
+ id="management-modal"
19
+ className="modal-toggle"
20
+ />
21
+ <div className="modal">
22
+ <div className="modal-box">
23
+ <h3 className="font-bold text-base md:text-lg">
24
+ Manage Members of {space.name}
25
+ </h3>
26
+
27
+ <div className="p-4 mt-4">
28
+ <ManageMembers space={space} />
29
+ </div>
30
+
31
+ <div className="modal-action">
32
+ <label
33
+ htmlFor="management-modal"
34
+ className="btn btn-outline"
35
+ >
36
+ Close
37
+ </label>
38
+ </div>
39
+ </div>
40
+ </div>
41
+ </>
42
+ );
43
+ }
44
+
45
+ export default function SpaceMembers() {
46
+ const space = useCurrentSpace();
47
+
48
+ const { find: findMembers } = useSpaceUser();
49
+ const { data: members } = findMembers({
50
+ where: {
51
+ spaceId: space?.id,
52
+ },
53
+ include: {
54
+ user: true,
55
+ },
56
+ orderBy: {
57
+ role: 'desc',
58
+ },
59
+ });
60
+
61
+ return (
62
+ <div className="flex items-center">
63
+ {ManagementDialog(space)}
64
+ {members && (
65
+ <label
66
+ className="mr-1 modal-button cursor-pointer"
67
+ htmlFor="management-modal"
68
+ >
69
+ {members?.map((member) => (
70
+ <Avatar key={member.id} user={member.user} size={24} />
71
+ ))}
72
+ </label>
73
+ )}
74
+ </div>
75
+ );
76
+ }
@@ -0,0 +1,28 @@
1
+ import { useSpace } from '@zenstackhq/runtime/hooks';
2
+ import Link from 'next/link';
3
+
4
+ export default function Spaces() {
5
+ const { find } = useSpace();
6
+ const spaces = find();
7
+
8
+ return (
9
+ <ul className="flex flex-wrap gap-4">
10
+ {spaces.data?.map((space) => (
11
+ <li
12
+ className="card w-80 h-32 shadow-xl text-gray-600 cursor-pointer hover:bg-gray-50 border"
13
+ key={space.id}
14
+ >
15
+ <Link href={`/space/${space.slug}`}>
16
+ <a>
17
+ <div className="card-body" title={space.name}>
18
+ <h2 className="card-title line-clamp-1">
19
+ {space.name}
20
+ </h2>
21
+ </div>
22
+ </a>
23
+ </Link>
24
+ </li>
25
+ ))}
26
+ </ul>
27
+ );
28
+ }
@@ -0,0 +1,17 @@
1
+ import moment from 'moment';
2
+
3
+ type Props = {
4
+ value: { createdAt: Date; updatedAt: Date; completedAt?: Date | null };
5
+ };
6
+
7
+ export default function TimeInfo({ value }: Props) {
8
+ return (
9
+ <p className="text-sm text-gray-500">
10
+ {value.completedAt
11
+ ? `Completed ${moment(value.completedAt).fromNow()}`
12
+ : value.createdAt === value.updatedAt
13
+ ? `Created ${moment(value.createdAt).fromNow()}`
14
+ : `Updated ${moment(value.updatedAt).fromNow()}`}
15
+ </p>
16
+ );
17
+ }
@@ -0,0 +1,72 @@
1
+ import { TrashIcon } from '@heroicons/react/24/outline';
2
+ import { useTodo } from '@zenstackhq/runtime/hooks';
3
+ import { Todo, User } from '@zenstackhq/runtime/types';
4
+ import { ChangeEvent, useEffect, useState } from 'react';
5
+ import Avatar from './Avatar';
6
+ import TimeInfo from './TimeInfo';
7
+
8
+ type Props = {
9
+ value: Todo & { owner: User };
10
+ updated?: (value: Todo) => any;
11
+ deleted?: (value: Todo) => any;
12
+ };
13
+
14
+ export default function Component({ value, updated, deleted }: Props) {
15
+ const [completed, setCompleted] = useState(!!value.completedAt);
16
+ const { update, del } = useTodo();
17
+
18
+ useEffect(() => {
19
+ if (!!value.completedAt !== completed) {
20
+ update(value.id, {
21
+ data: { completedAt: completed ? new Date() : null },
22
+ }).then((newValue) => {
23
+ if (updated) {
24
+ updated(newValue);
25
+ }
26
+ });
27
+ }
28
+ });
29
+
30
+ const deleteTodo = async () => {
31
+ await del(value.id);
32
+ if (deleted) {
33
+ deleted(value);
34
+ }
35
+ };
36
+
37
+ return (
38
+ <div className="border rounded-lg px-8 py-4 shadow-lg flex flex-col items-center w-full lg:w-[480px]">
39
+ <div className="flex justify-between w-full mb-4">
40
+ <h3
41
+ className={`text-xl line-clamp-1 ${
42
+ value.completedAt
43
+ ? 'line-through text-gray-400 italic'
44
+ : 'text-gray-700'
45
+ }`}
46
+ >
47
+ {value.title}
48
+ </h3>
49
+ <div className="flex">
50
+ <input
51
+ type="checkbox"
52
+ className="checkbox mr-2"
53
+ checked={completed}
54
+ onChange={(e: ChangeEvent<HTMLInputElement>) =>
55
+ setCompleted(e.currentTarget.checked)
56
+ }
57
+ />
58
+ <TrashIcon
59
+ className="w-6 h-6 text-gray-500 cursor-pointer"
60
+ onClick={() => {
61
+ deleteTodo();
62
+ }}
63
+ />
64
+ </div>
65
+ </div>
66
+ <div className="flex justify-end w-full space-x-2">
67
+ <TimeInfo value={value} />
68
+ <Avatar user={value.owner} size={18} />
69
+ </div>
70
+ </div>
71
+ );
72
+ }
@@ -0,0 +1,77 @@
1
+ import Image from 'next/image';
2
+ import { List } from '@zenstackhq/runtime/types';
3
+ import { customAlphabet } from 'nanoid';
4
+ import { LockClosedIcon, TrashIcon } from '@heroicons/react/24/outline';
5
+ import { User } from 'next-auth';
6
+ import Avatar from './Avatar';
7
+ import Link from 'next/link';
8
+ import { useRouter } from 'next/router';
9
+ import { useList } from '@zenstackhq/runtime/hooks';
10
+ import TimeInfo from './TimeInfo';
11
+
12
+ type Props = {
13
+ value: List & { owner: User };
14
+ deleted?: (value: List) => void;
15
+ };
16
+
17
+ export default function TodoList({ value, deleted }: Props) {
18
+ const router = useRouter();
19
+
20
+ const { del } = useList();
21
+
22
+ const deleteList = async () => {
23
+ if (confirm('Are you sure to delete this list?')) {
24
+ await del(value.id);
25
+ if (deleted) {
26
+ deleted(value);
27
+ }
28
+ }
29
+ };
30
+
31
+ return (
32
+ <div className="card w-80 bg-base-100 shadow-xl cursor-pointer hover:bg-gray-50">
33
+ <Link href={`${router.asPath}/${value.id}`}>
34
+ <a>
35
+ <figure>
36
+ <Image
37
+ src={`https://picsum.photos/300/200?r=${customAlphabet(
38
+ '0123456789'
39
+ )(4)}`}
40
+ width={320}
41
+ height={200}
42
+ alt="Cover"
43
+ />
44
+ </figure>
45
+ </a>
46
+ </Link>
47
+ <div className="card-body">
48
+ <Link href={`${router.asPath}/${value.id}`}>
49
+ <a>
50
+ <h2 className="card-title line-clamp-1">
51
+ {value.title || 'Missing Title'}
52
+ </h2>
53
+ </a>
54
+ </Link>
55
+ <div className="card-actions flex w-full justify-between">
56
+ <div>
57
+ <TimeInfo value={value} />
58
+ </div>
59
+ <div className="flex space-x-2">
60
+ <Avatar user={value.owner} size={18} />
61
+ {value.private && (
62
+ <div className="tooltip" data-tip="Private">
63
+ <LockClosedIcon className="w-4 h-4 text-gray-500" />
64
+ </div>
65
+ )}
66
+ <TrashIcon
67
+ className="w-4 h-4 text-gray-500 cursor-pointer"
68
+ onClick={() => {
69
+ deleteList();
70
+ }}
71
+ />
72
+ </div>
73
+ </div>
74
+ </div>
75
+ </div>
76
+ );
77
+ }
@@ -0,0 +1,31 @@
1
+ import { useSpace } from '@zenstackhq/runtime/hooks';
2
+ import { Space } from '@zenstackhq/runtime/types';
3
+ import { User } from 'next-auth';
4
+ import { useSession } from 'next-auth/react';
5
+ import { useRouter } from 'next/router';
6
+ import { createContext } from 'react';
7
+
8
+ export const UserContext = createContext<User | undefined>(undefined);
9
+
10
+ export function useCurrentUser() {
11
+ const { data: session } = useSession();
12
+ return session?.user;
13
+ }
14
+
15
+ export const SpaceContext = createContext<Space | undefined>(undefined);
16
+
17
+ export function useCurrentSpace() {
18
+ const router = useRouter();
19
+ const { find } = useSpace();
20
+ const spaces = find({
21
+ where: {
22
+ slug: router.query.slug as string,
23
+ },
24
+ });
25
+
26
+ if (!router.query.slug) {
27
+ return undefined;
28
+ }
29
+
30
+ return spaces.data?.[0];
31
+ }
@@ -0,0 +1,10 @@
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {
3
+ reactStrictMode: true,
4
+ swcMinify: true,
5
+ images: {
6
+ domains: ['lh3.googleusercontent.com', 'picsum.photos'],
7
+ },
8
+ };
9
+
10
+ module.exports = nextConfig;