Compare commits
249 Commits
jazz-bette
...
jazz-bette
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d42fc9b34 | ||
|
|
c9bda7e1e3 | ||
|
|
476f2d7eee | ||
|
|
1ba3a2ca34 | ||
|
|
7dd3d005a3 | ||
|
|
2c2dfb52d4 | ||
|
|
d33917fbaa | ||
|
|
f0c73d9cc6 | ||
|
|
d9324a9809 | ||
|
|
f7b5454cc6 | ||
|
|
5de338bdaf | ||
|
|
e67d44d47a | ||
|
|
a310293346 | ||
|
|
716d770258 | ||
|
|
4e85b50e1b | ||
|
|
643297b42e | ||
|
|
261efd99be | ||
|
|
f75f4f9b2d | ||
|
|
a0021f060c | ||
|
|
86bd87e6d0 | ||
|
|
ae55e80801 | ||
|
|
e830caf966 | ||
|
|
2f7240121d | ||
|
|
97699a6d5b | ||
|
|
5f8a2ba8df | ||
|
|
fe06e12b85 | ||
|
|
5b2b16a5c6 | ||
|
|
a966912c8a | ||
|
|
b63b70fb80 | ||
|
|
6b3e02920a | ||
|
|
f566961390 | ||
|
|
265b265365 | ||
|
|
83fc22f39a | ||
|
|
794681a8bb | ||
|
|
899bb0d2a1 | ||
|
|
33cfc4cc25 | ||
|
|
42c60c99fe | ||
|
|
e42518ed29 | ||
|
|
5b7ef3cd89 | ||
|
|
fc02fc0608 | ||
|
|
ceaa555e83 | ||
|
|
03229b2ea9 | ||
|
|
e2737d44b6 | ||
|
|
4b73834883 | ||
|
|
1b3d43d5f4 | ||
|
|
9c9a689879 | ||
|
|
60b5288042 | ||
|
|
2fd88b938c | ||
|
|
d1f955006f | ||
|
|
bb3d5f1f87 | ||
|
|
26ce61ab78 | ||
|
|
1f300114d5 | ||
|
|
da69f812f8 | ||
|
|
0bcbf551ca | ||
|
|
6b3d5b5560 | ||
|
|
d1bdbf5d49 | ||
|
|
621e809fad | ||
|
|
d6600d9322 | ||
|
|
2b08bd77c1 | ||
|
|
c1c6e31711 | ||
|
|
0b16085f3c | ||
|
|
e53db2e96a | ||
|
|
384f0e23c0 | ||
|
|
daaf1789d9 | ||
|
|
1f9e20e753 | ||
|
|
ce9ca54f5c | ||
|
|
67e0968809 | ||
|
|
96a922cceb | ||
|
|
9b22fc74cd | ||
|
|
1bebe3c6c8 | ||
|
|
0a98b826f1 | ||
|
|
e1bd16d08b | ||
|
|
0967c2ee5a | ||
|
|
62a3854c41 | ||
|
|
f22ef4e646 | ||
|
|
6c35d0031d | ||
|
|
7bdb6f4279 | ||
|
|
93f3fb231b | ||
|
|
01d13d5df2 | ||
|
|
944e725b95 | ||
|
|
16024fec8e | ||
|
|
f90414ab95 | ||
|
|
492eecb46a | ||
|
|
51144ec832 | ||
|
|
fcaf4b9c30 | ||
|
|
afae2649f5 | ||
|
|
b5b0284c61 | ||
|
|
bf1475a143 | ||
|
|
e82cb80ca4 | ||
|
|
32c2a617d6 | ||
|
|
d3c2a41c81 | ||
|
|
4b99ff1fe3 | ||
|
|
3ebf8258a0 | ||
|
|
4809d14f6d | ||
|
|
5ae1f33127 | ||
|
|
ca5d84f6a9 | ||
|
|
6e6acc3404 | ||
|
|
b17b7b6481 | ||
|
|
5341646301 | ||
|
|
5416165d28 | ||
|
|
b5a9f681c5 | ||
|
|
7dffc006eb | ||
|
|
cd3cc5b0ab | ||
|
|
ceab75eb4d | ||
|
|
103d1b41f7 | ||
|
|
b87cc6973e | ||
|
|
3d541ca241 | ||
|
|
e72bfec884 | ||
|
|
19c7ad27d9 | ||
|
|
0bc7bfc5cc | ||
|
|
2c8120d46f | ||
|
|
c936c8c611 | ||
|
|
58c6013770 | ||
|
|
3eb3291a97 | ||
|
|
6b659f2df3 | ||
|
|
dcc9c9a5ec | ||
|
|
fe9a244363 | ||
|
|
9440bbc058 | ||
|
|
1c92cc2997 | ||
|
|
33ebbf0bdd | ||
|
|
d630b5bde5 | ||
|
|
8c56445882 | ||
|
|
1c6ae12cd9 | ||
|
|
ac5d20d159 | ||
|
|
21bcaabd5a | ||
|
|
17b4d5b668 | ||
|
|
3cd15862d5 | ||
|
|
b3d1ad7201 | ||
|
|
d87df11795 | ||
|
|
82c2a62b2a | ||
|
|
0a9112506e | ||
|
|
d9c9b5f099 | ||
|
|
fbc29f2f17 | ||
|
|
3915bbbf3c | ||
|
|
0b471c4e89 | ||
|
|
09077d37ef | ||
|
|
afe06b4fa6 | ||
|
|
d89b6e488a | ||
|
|
f6361ee43b | ||
|
|
726dbfb6df | ||
|
|
267f689f10 | ||
|
|
893ad3ae23 | ||
|
|
f5590b1be8 | ||
|
|
17a01f57e8 | ||
|
|
7318d86f52 | ||
|
|
1c8403e87a | ||
|
|
dd747c068a | ||
|
|
1f0f230fe2 | ||
|
|
da655cbff5 | ||
|
|
02f6c6220e | ||
|
|
0755cd198e | ||
|
|
c4a8227b66 | ||
|
|
86f0302233 | ||
|
|
72b5542130 | ||
|
|
5fd9225a54 | ||
|
|
9138d30208 | ||
|
|
a5ece15797 | ||
|
|
9f8877202e | ||
|
|
d190097ed9 | ||
|
|
9841617c66 | ||
|
|
165a6170cd | ||
|
|
5148419df9 | ||
|
|
fc0ecb0968 | ||
|
|
802b5a3060 | ||
|
|
e47af262b3 | ||
|
|
688a4850a4 | ||
|
|
e87fef751e | ||
|
|
8f714440f8 | ||
|
|
70cd09170e | ||
|
|
e98b610fd0 | ||
|
|
b554983558 | ||
|
|
4c63334299 | ||
|
|
4aef7cdac5 | ||
|
|
76adeb0d53 | ||
|
|
d95dcbe7db | ||
|
|
f9d538f049 | ||
|
|
40c7336c09 | ||
|
|
e0d2723615 | ||
|
|
93e68c62f5 | ||
|
|
dadee9dcc5 | ||
|
|
6724c4bd83 | ||
|
|
1942bd5de4 | ||
|
|
16764f6365 | ||
|
|
b56cfc2e1f | ||
|
|
7091bcf9c0 | ||
|
|
436cbfa095 | ||
|
|
c19a25f928 | ||
|
|
104e664bbb | ||
|
|
f199b451eb | ||
|
|
70bc48458e | ||
|
|
f28b2a6135 | ||
|
|
55b770b7c9 | ||
|
|
e6838dfb98 | ||
|
|
5e34061fdc | ||
|
|
6d9b77195a | ||
|
|
9bf7946ee6 | ||
|
|
acecffaeb2 | ||
|
|
cc291b590a | ||
|
|
1f144e89bf | ||
|
|
8e9acb37f8 | ||
|
|
5a48c9c44c | ||
|
|
8115e194d3 | ||
|
|
5c98ff4e4f | ||
|
|
51fcb8a44b | ||
|
|
c5888c39f5 | ||
|
|
2defcfae67 | ||
|
|
213de11c3b | ||
|
|
2f24d35471 | ||
|
|
42667c81bb | ||
|
|
1b881cc89f | ||
|
|
af295d816a | ||
|
|
fe8d3497c0 | ||
|
|
c2899e94ca | ||
|
|
f4be67e9b6 | ||
|
|
ba9ad295b6 | ||
|
|
9ed5a96ef8 | ||
|
|
4272ea9019 | ||
|
|
9509307ed1 | ||
|
|
be08921bc5 | ||
|
|
77e3c21cbd | ||
|
|
25be055a51 | ||
|
|
b173e0884a | ||
|
|
231947c97a | ||
|
|
d5b57ad1fc | ||
|
|
0bf5c53bec | ||
|
|
e7b1550003 | ||
|
|
f5039cefc1 | ||
|
|
6540893caf | ||
|
|
bfc85c4573 | ||
|
|
e9076313ab | ||
|
|
c6afd8ae36 | ||
|
|
370f20d13d | ||
|
|
f9b3116deb | ||
|
|
352d34979f | ||
|
|
7ff736ace4 | ||
|
|
5bab466fd0 | ||
|
|
329b8c3d6a | ||
|
|
c0aeb7baf9 | ||
|
|
8a14de10d7 | ||
|
|
b585b39a86 | ||
|
|
e9b2860e74 | ||
|
|
6327d74f68 | ||
|
|
bedbabdcb4 | ||
|
|
c2c223f22a | ||
|
|
d60e345b4d | ||
|
|
be47d866bc | ||
|
|
ac88bdcb98 | ||
|
|
30704bcaf7 | ||
|
|
03108871e9 |
@@ -14,7 +14,8 @@
|
|||||||
"jazz-betterauth-server-plugin",
|
"jazz-betterauth-server-plugin",
|
||||||
"jazz-react-auth-betterauth",
|
"jazz-react-auth-betterauth",
|
||||||
"jazz-run",
|
"jazz-run",
|
||||||
"jazz-tools"
|
"jazz-tools",
|
||||||
|
"community-jazz-vue"
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"access": "public",
|
"access": "public",
|
||||||
|
|||||||
5
.github/workflows/code-quality.yml
vendored
5
.github/workflows/code-quality.yml
vendored
@@ -22,9 +22,6 @@ jobs:
|
|||||||
- name: Setup Biome
|
- name: Setup Biome
|
||||||
uses: biomejs/setup-biome@v2
|
uses: biomejs/setup-biome@v2
|
||||||
with:
|
with:
|
||||||
version: 1.9.4
|
version: 2.1.3
|
||||||
- name: Run Biome
|
- name: Run Biome
|
||||||
run: biome ci .
|
run: biome ci .
|
||||||
|
|
||||||
- name: Check Catalog Dependencies
|
|
||||||
run: node scripts/check-catalog-deps.js
|
|
||||||
|
|||||||
1
.github/workflows/playwright.yml
vendored
1
.github/workflows/playwright.yml
vendored
@@ -48,6 +48,7 @@ jobs:
|
|||||||
"tests/e2e"
|
"tests/e2e"
|
||||||
"examples/chat"
|
"examples/chat"
|
||||||
"examples/chat-svelte"
|
"examples/chat-svelte"
|
||||||
|
"examples/community-clerk-vue"
|
||||||
"examples/clerk"
|
"examples/clerk"
|
||||||
"examples/betterauth"
|
"examples/betterauth"
|
||||||
"examples/file-share-svelte"
|
"examples/file-share-svelte"
|
||||||
|
|||||||
72
biome.json
72
biome.json
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
"$schema": "https://biomejs.dev/schemas/2.1.3/schema.json",
|
||||||
"vcs": {
|
"vcs": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"clientKind": "git",
|
"clientKind": "git",
|
||||||
@@ -7,39 +7,35 @@
|
|||||||
},
|
},
|
||||||
"files": {
|
"files": {
|
||||||
"ignoreUnknown": false,
|
"ignoreUnknown": false,
|
||||||
"ignore": [
|
"includes": [
|
||||||
"jazz-tools.json",
|
"**",
|
||||||
"**/ios/**",
|
"!**/jazz-tools.json",
|
||||||
"**/android/**",
|
"!**/ios/**",
|
||||||
"tests/jazz-svelte/src/**",
|
"!**/android/**",
|
||||||
"examples/*svelte*/**",
|
"!**/tests/jazz-svelte/src/**",
|
||||||
"starters/*svelte*/**",
|
"!**/examples/**/*svelte*/**",
|
||||||
"examples/server-worker-inbox/src/routeTree.gen.ts",
|
"!**/starters/**/*svelte*/**",
|
||||||
"homepage/homepage/**",
|
"!**/examples/server-worker-inbox/src/routeTree.gen.ts",
|
||||||
"**/package.json"
|
"!**/homepage/homepage/**",
|
||||||
|
"!**/package.json",
|
||||||
|
"!**/*svelte*/**"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"formatter": {
|
"formatter": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"indentStyle": "space"
|
"indentStyle": "space"
|
||||||
},
|
},
|
||||||
"organizeImports": {
|
"assist": { "actions": { "source": { "organizeImports": "off" } } },
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"linter": {
|
"linter": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"rules": {
|
"rules": {
|
||||||
"recommended": true,
|
"recommended": true,
|
||||||
"correctness": {
|
"correctness": {
|
||||||
|
"useExhaustiveDependencies": "off",
|
||||||
"useImportExtensions": {
|
"useImportExtensions": {
|
||||||
"level": "error",
|
"level": "error",
|
||||||
"options": {
|
"options": {
|
||||||
"suggestedExtensions": {
|
"forceJsExtensions": true
|
||||||
"ts": {
|
|
||||||
"module": "js",
|
|
||||||
"component": "jsx"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -47,16 +43,7 @@
|
|||||||
},
|
},
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
"include": ["packages/**/src/**"],
|
"includes": ["packages/community-jazz-vue/src/**"],
|
||||||
"linter": {
|
|
||||||
"enabled": true,
|
|
||||||
"rules": {
|
|
||||||
"recommended": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"include": ["packages/cojson/src/storage/*/**", "cojson-transport-ws/**"],
|
|
||||||
"linter": {
|
"linter": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"rules": {
|
"rules": {
|
||||||
@@ -65,7 +52,28 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"include": ["**/tests/**"],
|
"includes": ["**/packages/**/src/**"],
|
||||||
|
"linter": {
|
||||||
|
"enabled": true,
|
||||||
|
"rules": {
|
||||||
|
"recommended": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"includes": [
|
||||||
|
"**/packages/cojson/src/storage/**/*/**",
|
||||||
|
"**/cojson-transport-ws/**"
|
||||||
|
],
|
||||||
|
"linter": {
|
||||||
|
"enabled": true,
|
||||||
|
"rules": {
|
||||||
|
"recommended": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"includes": ["**/tests/**"],
|
||||||
"linter": {
|
"linter": {
|
||||||
"rules": {
|
"rules": {
|
||||||
"correctness": {
|
"correctness": {
|
||||||
@@ -75,7 +83,7 @@
|
|||||||
"noNonNullAssertion": "off"
|
"noNonNullAssertion": "off"
|
||||||
},
|
},
|
||||||
"suspicious": {
|
"suspicious": {
|
||||||
"noExplicitAny": "info"
|
"noExplicitAny": "off"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,22 +27,22 @@
|
|||||||
"jazz-tools": "workspace:*",
|
"jazz-tools": "workspace:*",
|
||||||
"lucide-react": "^0.510.0",
|
"lucide-react": "^0.510.0",
|
||||||
"next": "15.3.2",
|
"next": "15.3.2",
|
||||||
"react": "^18.0.0",
|
"react": "catalog:react",
|
||||||
"react-dom": "^18.0.0",
|
"react-dom": "catalog:react",
|
||||||
"sonner": "^2.0.3",
|
"sonner": "^2.0.3",
|
||||||
"tailwind-merge": "^3.3.0",
|
"tailwind-merge": "^3.3.0",
|
||||||
"tw-animate-css": "^1.2.5"
|
"tw-animate-css": "^1.2.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "1.9.4",
|
"@biomejs/biome": "catalog:default",
|
||||||
"@playwright/test": "^1.50.1",
|
"@playwright/test": "^1.50.1",
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@types/better-sqlite3": "^7.6.12",
|
"@types/better-sqlite3": "^7.6.12",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^18",
|
"@types/react": "catalog:react",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "catalog:react",
|
||||||
"react-email": "^4.0.11",
|
"react-email": "^4.0.11",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"typescript": "^5"
|
"typescript": "catalog:default"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,13 +13,13 @@
|
|||||||
"@bacons/text-decoder": "^0.0.0",
|
"@bacons/text-decoder": "^0.0.0",
|
||||||
"@bam.tech/react-native-image-resizer": "^3.0.11",
|
"@bam.tech/react-native-image-resizer": "^3.0.11",
|
||||||
"@react-native-community/netinfo": "11.4.1",
|
"@react-native-community/netinfo": "11.4.1",
|
||||||
"expo": "54.0.0-canary-20250701-6a945c5",
|
"expo": "catalog:expo",
|
||||||
"expo-clipboard": "^7.1.4",
|
"expo-clipboard": "catalog:expo",
|
||||||
"expo-secure-store": "~14.2.3",
|
"expo-secure-store": "catalog:expo",
|
||||||
"expo-sqlite": "~15.2.10",
|
"expo-sqlite": "catalog:expo",
|
||||||
"jazz-tools": "workspace:*",
|
"jazz-tools": "workspace:*",
|
||||||
"react": "19.1.0",
|
"react": "catalog:expo",
|
||||||
"react-native": "0.80.0",
|
"react-native": "catalog:expo",
|
||||||
"react-native-get-random-values": "^1.11.0",
|
"react-native-get-random-values": "^1.11.0",
|
||||||
"readable-stream": "^4.7.0"
|
"readable-stream": "^4.7.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -18,8 +18,8 @@
|
|||||||
"@react-navigation/native": "7.1.14",
|
"@react-navigation/native": "7.1.14",
|
||||||
"@react-navigation/native-stack": "7.3.19",
|
"@react-navigation/native-stack": "7.3.19",
|
||||||
"jazz-tools": "workspace:*",
|
"jazz-tools": "workspace:*",
|
||||||
"react": "19.1.0",
|
"react": "catalog:rn",
|
||||||
"react-native": "0.80.0",
|
"react-native": "catalog:rn",
|
||||||
"react-native-get-random-values": "^1.11.0",
|
"react-native-get-random-values": "^1.11.0",
|
||||||
"react-native-mmkv": "3.3.0",
|
"react-native-mmkv": "3.3.0",
|
||||||
"react-native-safe-area-context": "5.5.0",
|
"react-native-safe-area-context": "5.5.0",
|
||||||
@@ -31,16 +31,16 @@
|
|||||||
"@babel/plugin-transform-export-namespace-from": "^7.27.1",
|
"@babel/plugin-transform-export-namespace-from": "^7.27.1",
|
||||||
"@babel/preset-env": "^7.25.3",
|
"@babel/preset-env": "^7.25.3",
|
||||||
"@babel/runtime": "^7.25.0",
|
"@babel/runtime": "^7.25.0",
|
||||||
"@react-native-community/cli": "19.0.0",
|
"@react-native-community/cli": "catalog:rn",
|
||||||
"@react-native-community/cli-platform-android": "19.0.0",
|
"@react-native-community/cli-platform-android": "catalog:rn",
|
||||||
"@react-native-community/cli-platform-ios": "19.0.0",
|
"@react-native-community/cli-platform-ios": "catalog:rn",
|
||||||
"@react-native/babel-preset": "0.80.0",
|
"@react-native/babel-preset": "catalog:rn",
|
||||||
"@react-native/eslint-config": "0.80.0",
|
"@react-native/eslint-config": "catalog:rn",
|
||||||
"@react-native/metro-config": "0.80.0",
|
"@react-native/metro-config": "catalog:rn",
|
||||||
"@react-native/typescript-config": "0.80.0",
|
"@react-native/typescript-config": "catalog:rn",
|
||||||
"@rnx-kit/metro-config": "^2.0.1",
|
"@rnx-kit/metro-config": "^2.0.1",
|
||||||
"@rnx-kit/metro-resolver-symlinks": "^0.2.5",
|
"@rnx-kit/metro-resolver-symlinks": "^0.2.5",
|
||||||
"@types/react": "^19.1.0",
|
"@types/react": "catalog:rn",
|
||||||
"eslint": "^8.19.0",
|
"eslint": "^8.19.0",
|
||||||
"pod-install": "^0.3.5",
|
"pod-install": "^0.3.5",
|
||||||
"prettier": "2.8.8",
|
"prettier": "2.8.8",
|
||||||
|
|||||||
@@ -76,7 +76,9 @@ export function ChatScreen({ navigation }: { navigation: any }) {
|
|||||||
|
|
||||||
const renderMessageItem = ({
|
const renderMessageItem = ({
|
||||||
item,
|
item,
|
||||||
}: { item: Loaded<typeof Message, { text: true }> }) => {
|
}: {
|
||||||
|
item: Loaded<typeof Message, { text: true }>;
|
||||||
|
}) => {
|
||||||
const isMe = item._edits?.text?.by?.isMe;
|
const isMe = item._edits?.text?.by?.isMe;
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
|
|||||||
@@ -3,11 +3,7 @@ import React from "react";
|
|||||||
import { Text } from "react-native";
|
import { Text } from "react-native";
|
||||||
import { Chat } from "./schema";
|
import { Chat } from "./schema";
|
||||||
|
|
||||||
export function HandleInviteScreen({
|
export function HandleInviteScreen({ navigation }: { navigation: any }) {
|
||||||
navigation,
|
|
||||||
}: {
|
|
||||||
navigation: any;
|
|
||||||
}) {
|
|
||||||
useAcceptInviteNative({
|
useAcceptInviteNative({
|
||||||
invitedObjectSchema: Chat,
|
invitedObjectSchema: Chat,
|
||||||
onAccept: async (chatId) => {
|
onAccept: async (chatId) => {
|
||||||
|
|||||||
@@ -1,5 +1,65 @@
|
|||||||
# passkey-svelte
|
# passkey-svelte
|
||||||
|
|
||||||
|
## 0.0.117
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [7dd3d00]
|
||||||
|
- jazz-tools@0.17.4
|
||||||
|
|
||||||
|
## 0.0.116
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- jazz-tools@0.17.3
|
||||||
|
|
||||||
|
## 0.0.115
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [794681a]
|
||||||
|
- Updated dependencies [83fc22f]
|
||||||
|
- jazz-tools@0.17.2
|
||||||
|
|
||||||
|
## 0.0.114
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [0bcbf55]
|
||||||
|
- Updated dependencies [d1bdbf5]
|
||||||
|
- Updated dependencies [4b73834]
|
||||||
|
- jazz-tools@0.17.1
|
||||||
|
|
||||||
|
## 0.0.113
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [fcaf4b9]
|
||||||
|
- jazz-tools@0.17.0
|
||||||
|
|
||||||
|
## 0.0.112
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [67e0968]
|
||||||
|
- Updated dependencies [2c8120d]
|
||||||
|
- jazz-tools@0.16.6
|
||||||
|
|
||||||
|
## 0.0.111
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [3cd1586]
|
||||||
|
- Updated dependencies [33ebbf0]
|
||||||
|
- jazz-tools@0.16.5
|
||||||
|
|
||||||
|
## 0.0.110
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [16764f6]
|
||||||
|
- jazz-tools@0.16.4
|
||||||
|
|
||||||
## 0.0.109
|
## 0.0.109
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "chat-svelte",
|
"name": "chat-svelte",
|
||||||
"version": "0.0.109",
|
"version": "0.0.117",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ImageDefinition, type Loaded } from 'jazz-tools';
|
import { ImageDefinition, type Loaded } from 'jazz-tools';
|
||||||
import { useProgressiveImg } from '$lib/utils/useProgressiveImage.svelte';
|
import { Image } from 'jazz-tools/svelte';
|
||||||
let { image }: { image: Loaded<typeof ImageDefinition> } = $props();
|
let { image }: { image: Loaded<typeof ImageDefinition> } = $props();
|
||||||
const { src } = $derived(
|
|
||||||
useProgressiveImg({
|
|
||||||
image
|
|
||||||
})
|
|
||||||
);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<img class="h-auto max-h-[20rem] max-w-full rounded-t-xl mb-1" {src} alt="" />
|
<Image
|
||||||
|
imageId={image.id}
|
||||||
|
alt=""
|
||||||
|
class="h-auto max-h-[20rem] max-w-full rounded-t-xl mb-1"
|
||||||
|
/>
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
import { ImageDefinition, type Loaded } from 'jazz-tools';
|
|
||||||
import { onDestroy } from 'svelte';
|
|
||||||
|
|
||||||
export function useProgressiveImg({
|
|
||||||
image,
|
|
||||||
maxWidth,
|
|
||||||
targetWidth
|
|
||||||
}: {
|
|
||||||
image: Loaded<typeof ImageDefinition> | null | undefined;
|
|
||||||
maxWidth?: number;
|
|
||||||
targetWidth?: number;
|
|
||||||
}) {
|
|
||||||
let current = $state<{
|
|
||||||
src?: string;
|
|
||||||
res?: `${number}x${number}` | 'placeholder';
|
|
||||||
}>();
|
|
||||||
const originalSize = $state(image?.originalSize);
|
|
||||||
|
|
||||||
const unsubscribe = image?.subscribe({}, (update: Loaded<typeof ImageDefinition>) => {
|
|
||||||
const highestRes = ImageDefinition.highestResAvailable(update, { maxWidth, targetWidth });
|
|
||||||
if (highestRes) {
|
|
||||||
if (highestRes.res !== current?.res) {
|
|
||||||
const blob = highestRes.stream.toBlob();
|
|
||||||
if (blob) {
|
|
||||||
const blobURI = URL.createObjectURL(blob);
|
|
||||||
current = { src: blobURI, res: highestRes.res };
|
|
||||||
|
|
||||||
setTimeout(() => URL.revokeObjectURL(blobURI), 200);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
current = {
|
|
||||||
src: update?.placeholderDataURL,
|
|
||||||
res: 'placeholder'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
onDestroy(() => () => {
|
|
||||||
unsubscribe?.();
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
get src() {
|
|
||||||
return current?.src;
|
|
||||||
},
|
|
||||||
get res() {
|
|
||||||
return current?.res;
|
|
||||||
},
|
|
||||||
|
|
||||||
originalSize
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createImage } from 'jazz-tools/browser-media-images';
|
import { createImage } from 'jazz-tools/media';
|
||||||
import { AccountCoState, CoState } from 'jazz-tools/svelte';
|
import { AccountCoState, CoState } from 'jazz-tools/svelte';
|
||||||
import { Account, CoPlainText, type ID } from 'jazz-tools';
|
import { Account, CoPlainText, type ID } from 'jazz-tools';
|
||||||
|
|
||||||
|
|||||||
@@ -15,21 +15,21 @@
|
|||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
"hash-slash": "workspace:*",
|
"hash-slash": "workspace:*",
|
||||||
"jazz-tools": "workspace:*",
|
"jazz-tools": "workspace:*",
|
||||||
"lucide-react": "^0.274.0",
|
"lucide-react": "^0.536.0",
|
||||||
"react": "19.1.0",
|
"react": "catalog:react",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "catalog:react",
|
||||||
"zod": "3.25.76"
|
"zod": "3.25.76"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.50.1",
|
"@playwright/test": "^1.50.1",
|
||||||
"@tailwindcss/postcss": "^4.1.10",
|
"@tailwindcss/postcss": "^4.1.10",
|
||||||
"@types/react": "19.1.0",
|
"@types/react": "catalog:react",
|
||||||
"@types/react-dom": "19.1.0",
|
"@types/react-dom": "catalog:react",
|
||||||
"@vitejs/plugin-react-swc": "^3.10.1",
|
"@vitejs/plugin-react-swc": "^3.10.1",
|
||||||
"is-ci": "^3.0.1",
|
"is-ci": "^3.0.1",
|
||||||
"postcss": "^8.4.40",
|
"postcss": "^8.4.40",
|
||||||
"tailwindcss": "^4.1.10",
|
"tailwindcss": "^4.1.10",
|
||||||
"typescript": "5.6.2",
|
"typescript": "catalog:default",
|
||||||
"vite": "^6.3.5"
|
"vite": "catalog:default"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Account, co } from "jazz-tools";
|
import { Account } from "jazz-tools";
|
||||||
import { createImage, useAccount, useCoState } from "jazz-tools/react";
|
import { createImage } from "jazz-tools/media";
|
||||||
import { useState } from "react";
|
import { useAccount, useCoState } from "jazz-tools/react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
import { Chat, Message } from "./schema.ts";
|
import { Chat, Message } from "./schema.ts";
|
||||||
import {
|
import {
|
||||||
BubbleBody,
|
BubbleBody,
|
||||||
@@ -15,14 +16,17 @@ import {
|
|||||||
TextInput,
|
TextInput,
|
||||||
} from "./ui.tsx";
|
} from "./ui.tsx";
|
||||||
|
|
||||||
export function ChatScreen(props: { chatID: string }) {
|
const INITIAL_MESSAGES_TO_SHOW = 30;
|
||||||
const chat = useCoState(Chat, props.chatID, {
|
|
||||||
resolve: { $each: { text: true } },
|
|
||||||
});
|
|
||||||
const { me } = useAccount();
|
|
||||||
const [showNLastMessages, setShowNLastMessages] = useState(30);
|
|
||||||
|
|
||||||
if (!chat)
|
export function ChatScreen(props: { chatID: string }) {
|
||||||
|
const chat = useCoState(Chat, props.chatID);
|
||||||
|
const { me } = useAccount();
|
||||||
|
const [showNLastMessages, setShowNLastMessages] = useState(
|
||||||
|
INITIAL_MESSAGES_TO_SHOW,
|
||||||
|
);
|
||||||
|
const isLoading = useMessagesPreload(props.chatID);
|
||||||
|
|
||||||
|
if (!chat || isLoading)
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 flex justify-center items-center">Loading...</div>
|
<div className="flex-1 flex justify-center items-center">Loading...</div>
|
||||||
);
|
);
|
||||||
@@ -37,11 +41,15 @@ export function ChatScreen(props: { chatID: string }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
createImage(file, { owner: chat._owner }).then((image) => {
|
createImage(file, {
|
||||||
|
owner: chat._owner,
|
||||||
|
progressive: true,
|
||||||
|
placeholder: "blur",
|
||||||
|
}).then((image) => {
|
||||||
chat.push(
|
chat.push(
|
||||||
Message.create(
|
Message.create(
|
||||||
{
|
{
|
||||||
text: co.plainText().create(file.name, chat._owner),
|
text: file.name,
|
||||||
image: image,
|
image: image,
|
||||||
},
|
},
|
||||||
chat._owner,
|
chat._owner,
|
||||||
@@ -59,9 +67,14 @@ export function ChatScreen(props: { chatID: string }) {
|
|||||||
<ChatBody>
|
<ChatBody>
|
||||||
{chat.length > 0 ? (
|
{chat.length > 0 ? (
|
||||||
chat
|
chat
|
||||||
|
// We call slice before reverse to avoid mutating the original array
|
||||||
.slice(-showNLastMessages)
|
.slice(-showNLastMessages)
|
||||||
.reverse() // this plus flex-col-reverse on ChatBody gives us scroll-to-bottom behavior
|
// Reverse plus flex-col-reverse on ChatBody gives us scroll-to-bottom behavior
|
||||||
.map((msg) => <ChatBubble me={me} msg={msg} key={msg.id} />)
|
.reverse()
|
||||||
|
.map(
|
||||||
|
(msg) =>
|
||||||
|
msg?.text && <ChatBubble me={me} msg={msg} key={msg.id} />,
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<EmptyChatMessage />
|
<EmptyChatMessage />
|
||||||
)}
|
)}
|
||||||
@@ -80,12 +93,7 @@ export function ChatScreen(props: { chatID: string }) {
|
|||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
onSubmit={(text) => {
|
onSubmit={(text) => {
|
||||||
chat.push(
|
chat.push(Message.create({ text }, chat._owner));
|
||||||
Message.create(
|
|
||||||
{ text: co.plainText().create(text, chat._owner) },
|
|
||||||
chat._owner,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</InputBar>
|
</InputBar>
|
||||||
@@ -93,10 +101,7 @@ export function ChatScreen(props: { chatID: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ChatBubble(props: {
|
function ChatBubble(props: { me: Account; msg: Message }) {
|
||||||
me: Account;
|
|
||||||
msg: co.loaded<typeof Message, { text: true }>;
|
|
||||||
}) {
|
|
||||||
if (!props.me.canRead(props.msg) || !props.msg.text?.toString()) {
|
if (!props.me.canRead(props.msg) || !props.msg.text?.toString()) {
|
||||||
return (
|
return (
|
||||||
<BubbleContainer fromMe={false}>
|
<BubbleContainer fromMe={false}>
|
||||||
@@ -126,3 +131,35 @@ function ChatBubble(props: {
|
|||||||
</BubbleContainer>
|
</BubbleContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Warms the local cache with the initial messages to load only the initial messages
|
||||||
|
* and avoid flickering
|
||||||
|
*/
|
||||||
|
function useMessagesPreload(chatID: string) {
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
preloadChatMessages(chatID).finally(() => {
|
||||||
|
setIsLoading(false);
|
||||||
|
});
|
||||||
|
}, [chatID]);
|
||||||
|
|
||||||
|
return isLoading;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function preloadChatMessages(chatID: string) {
|
||||||
|
const chat = await Chat.load(chatID);
|
||||||
|
|
||||||
|
if (!chat?._refs) return;
|
||||||
|
|
||||||
|
const promises = [];
|
||||||
|
|
||||||
|
for (const msg of Array.from(chat._refs)
|
||||||
|
.reverse()
|
||||||
|
.slice(0, INITIAL_MESSAGES_TO_SHOW)) {
|
||||||
|
promises.push(Message.load(msg.id, { resolve: { text: true } }));
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { CoPlainText, ImageDefinition } from "jazz-tools";
|
import { CoPlainText, ImageDefinition } from "jazz-tools";
|
||||||
import { ProgressiveImg } from "jazz-tools/react";
|
import { Image } from "jazz-tools/react";
|
||||||
import { ImageIcon } from "lucide-react";
|
import { ImageIcon } from "lucide-react";
|
||||||
import { useId, useRef } from "react";
|
import { useId, useRef } from "react";
|
||||||
|
|
||||||
@@ -83,14 +83,12 @@ export function BubbleText(props: {
|
|||||||
|
|
||||||
export function BubbleImage(props: { image: ImageDefinition }) {
|
export function BubbleImage(props: { image: ImageDefinition }) {
|
||||||
return (
|
return (
|
||||||
<ProgressiveImg image={props.image}>
|
<Image
|
||||||
{({ src }) => (
|
imageId={props.image.id}
|
||||||
<img
|
className="h-auto max-h-80 max-w-full rounded-t-xl mb-1"
|
||||||
className="h-auto max-h-80 max-w-full rounded-t-xl mb-1"
|
height="original"
|
||||||
src={src}
|
width="original"
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</ProgressiveImg>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,7 +110,9 @@ export function InputBar(props: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
export function ImageInput({
|
export function ImageInput({
|
||||||
onImageChange,
|
onImageChange,
|
||||||
}: { onImageChange?: (event: React.ChangeEvent<HTMLInputElement>) => void }) {
|
}: {
|
||||||
|
onImageChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
}) {
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const onUploadClick = () => {
|
const onUploadClick = () => {
|
||||||
|
|||||||
@@ -14,21 +14,21 @@
|
|||||||
"@bam.tech/react-native-image-resizer": "^3.0.11",
|
"@bam.tech/react-native-image-resizer": "^3.0.11",
|
||||||
"@clerk/clerk-expo": "^2.13.1",
|
"@clerk/clerk-expo": "^2.13.1",
|
||||||
"@react-native-community/netinfo": "11.4.1",
|
"@react-native-community/netinfo": "11.4.1",
|
||||||
"expo": "54.0.0-canary-20250701-6a945c5",
|
"expo": "catalog:expo",
|
||||||
"expo-crypto": "~14.1.5",
|
"expo-crypto": "catalog:expo",
|
||||||
"expo-linking": "~7.1.5",
|
"expo-linking": "catalog:expo",
|
||||||
"expo-secure-store": "~14.2.3",
|
"expo-secure-store": "catalog:expo",
|
||||||
"expo-sqlite": "~15.2.10",
|
"expo-sqlite": "catalog:expo",
|
||||||
"expo-web-browser": "~14.2.0",
|
"expo-web-browser": "catalog:expo",
|
||||||
"jazz-tools": "workspace:*",
|
"jazz-tools": "workspace:*",
|
||||||
"react": "19.1.0",
|
"react": "catalog:expo",
|
||||||
"react-native": "0.80.0",
|
"react-native": "catalog:expo",
|
||||||
"react-native-get-random-values": "^1.11.0",
|
"react-native-get-random-values": "^1.11.0",
|
||||||
"readable-stream": "^4.7.0"
|
"readable-stream": "^4.7.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.25.2",
|
"@babel/core": "^7.25.2",
|
||||||
"@types/react": "~19.0.10",
|
"@types/react": "catalog:expo",
|
||||||
"typescript": "~5.8.3"
|
"typescript": "~5.8.3"
|
||||||
},
|
},
|
||||||
"private": true
|
"private": true
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ import {
|
|||||||
|
|
||||||
export function SignInScreen({
|
export function SignInScreen({
|
||||||
setPage,
|
setPage,
|
||||||
}: { setPage: (page: "sign-in" | "sign-up") => void }) {
|
}: {
|
||||||
|
setPage: (page: "sign-in" | "sign-up") => void;
|
||||||
|
}) {
|
||||||
const { signIn, setActive, isLoaded } = useSignIn();
|
const { signIn, setActive, isLoaded } = useSignIn();
|
||||||
|
|
||||||
const [emailAddress, setEmailAddress] = useState("");
|
const [emailAddress, setEmailAddress] = useState("");
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ import {
|
|||||||
|
|
||||||
export function SignUpScreen({
|
export function SignUpScreen({
|
||||||
setPage,
|
setPage,
|
||||||
}: { setPage: (page: "sign-in" | "sign-up") => void }) {
|
}: {
|
||||||
|
setPage: (page: "sign-in" | "sign-up") => void;
|
||||||
|
}) {
|
||||||
const { isLoaded, signUp, setActive } = useSignUp();
|
const { isLoaded, signUp, setActive } = useSignUp();
|
||||||
|
|
||||||
const [emailAddress, setEmailAddress] = React.useState("");
|
const [emailAddress, setEmailAddress] = React.useState("");
|
||||||
|
|||||||
@@ -14,17 +14,17 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clerk/clerk-react": "^5.4.1",
|
"@clerk/clerk-react": "^5.4.1",
|
||||||
"jazz-tools": "workspace:*",
|
"jazz-tools": "workspace:*",
|
||||||
"react": "19.1.0",
|
"react": "catalog:react",
|
||||||
"react-dom": "19.1.0"
|
"react-dom": "catalog:react"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.50.1",
|
"@playwright/test": "^1.50.1",
|
||||||
"@biomejs/biome": "1.9.4",
|
"@biomejs/biome": "catalog:default",
|
||||||
"@types/react": "19.1.0",
|
"@types/react": "catalog:react",
|
||||||
"@types/react-dom": "19.1.0",
|
"@types/react-dom": "catalog:react",
|
||||||
"@vitejs/plugin-react": "^4.5.1",
|
"@vitejs/plugin-react": "^4.5.1",
|
||||||
"globals": "^15.11.0",
|
"globals": "^15.11.0",
|
||||||
"typescript": "5.6.2",
|
"typescript": "catalog:default",
|
||||||
"vite": "^6.3.5"
|
"vite": "catalog:default"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
6
examples/community-chat-vue/.gitignore
vendored
Normal file
6
examples/community-chat-vue/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
dist
|
||||||
|
# env files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
!.env.test
|
||||||
7
examples/community-chat-vue/CHANGELOG.md
Normal file
7
examples/community-chat-vue/CHANGELOG.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# community-chat-vue
|
||||||
|
|
||||||
|
## 0.15.4
|
||||||
|
|
||||||
|
- rewrite from React to Vue
|
||||||
|
- jazz-tools@0.15.4
|
||||||
|
- community-jazz-vue@0.15.4
|
||||||
60
examples/community-chat-vue/README.md
Normal file
60
examples/community-chat-vue/README.md
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# Chat example with Jazz and Vue
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
You can either
|
||||||
|
1. Clone the jazz repository, and run the app within the monorepo.
|
||||||
|
2. Or create a new Jazz project using this example as a template.
|
||||||
|
|
||||||
|
### Using the example as a template
|
||||||
|
|
||||||
|
Create a new Jazz project, and use this example as a template.
|
||||||
|
```bash
|
||||||
|
npx create-jazz-app@latest chat-vue-app --example community-chat-vue
|
||||||
|
```
|
||||||
|
|
||||||
|
Go to the new project directory.
|
||||||
|
```bash
|
||||||
|
cd chat-vue-app
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the dev server.
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using the monorepo
|
||||||
|
|
||||||
|
This requires `pnpm` to be installed, see [https://pnpm.io/installation](https://pnpm.io/installation).
|
||||||
|
|
||||||
|
Clone the jazz repository.
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/garden-co/jazz.git
|
||||||
|
```
|
||||||
|
|
||||||
|
Install and build dependencies.
|
||||||
|
```bash
|
||||||
|
pnpm i && npx turbo build
|
||||||
|
```
|
||||||
|
|
||||||
|
Go to the example directory.
|
||||||
|
```bash
|
||||||
|
cd jazz/examples/community-chat-vue/
|
||||||
|
```
|
||||||
|
|
||||||
|
Start the dev server.
|
||||||
|
```bash
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open [http://localhost:5173](http://localhost:5173) with your browser to see the result.
|
||||||
|
|
||||||
|
## Questions / problems / feedback
|
||||||
|
|
||||||
|
If you have feedback, let us know on [Discord](https://discord.gg/utDMjHYg42) or open an issue or PR to fix something that seems wrong.
|
||||||
|
|
||||||
|
## Configuration: sync server
|
||||||
|
|
||||||
|
By default, the example app uses [Jazz Cloud](https://jazz.tools/cloud) (`wss://cloud.jazz.tools`) - so cross-device use, invites and collaboration should just work.
|
||||||
|
|
||||||
|
You can also run a local sync server by running `npx jazz-run sync`, and setting the `sync` parameter of `JazzProvider` in [./src/main.ts](./src/main.ts) to `{ peer: "ws://localhost:4200" }`.
|
||||||
1
examples/community-chat-vue/env.d.ts
vendored
Normal file
1
examples/community-chat-vue/env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
13
examples/community-chat-vue/index.html
Normal file
13
examples/community-chat-vue/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Jazz Chat Vue Example</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
37
examples/community-chat-vue/package.json
Normal file
37
examples/community-chat-vue/package.json
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "community-chat-vue",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build-type-check": "run-p type-check \"build {@}\" --",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"build": "vite build",
|
||||||
|
"type-check": "vue-tsc --build --force",
|
||||||
|
"format-and-lint": "biome check .",
|
||||||
|
"format-and-lint:fix": "biome check . --write"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"jazz-tools": "workspace:*",
|
||||||
|
"community-jazz-vue": "workspace:*",
|
||||||
|
"vue": "^3.5.11",
|
||||||
|
"vue-router": "^4.4.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tsconfig/node20": "^20.1.4",
|
||||||
|
"@types/node": "^22.5.1",
|
||||||
|
"@vitejs/plugin-vue": "^5.1.4",
|
||||||
|
"@vitejs/plugin-vue-jsx": "^4.0.1",
|
||||||
|
"@vue/tsconfig": "^0.5.1",
|
||||||
|
"eslint": "^9.7.0",
|
||||||
|
"eslint-plugin-vue": "^9.28.0",
|
||||||
|
"npm-run-all2": "^6.2.3",
|
||||||
|
"postcss": "^8.4.40",
|
||||||
|
"@tailwindcss/postcss": "^4.1.10",
|
||||||
|
"tailwindcss": "^4.1.10",
|
||||||
|
"typescript": "5.6.2",
|
||||||
|
"vite": "6.3.5",
|
||||||
|
"vite-plugin-vue-devtools": "^7.4.6",
|
||||||
|
"vue-tsc": "^2.1.6"
|
||||||
|
}
|
||||||
|
}
|
||||||
5
examples/community-chat-vue/postcss.config.js
Normal file
5
examples/community-chat-vue/postcss.config.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
"@tailwindcss/postcss": {},
|
||||||
|
},
|
||||||
|
};
|
||||||
BIN
examples/community-chat-vue/public/favicon.ico
Normal file
BIN
examples/community-chat-vue/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
24
examples/community-chat-vue/src/App.vue
Normal file
24
examples/community-chat-vue/src/App.vue
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<template>
|
||||||
|
<AppContainer>
|
||||||
|
<TopBar v-if="me">
|
||||||
|
<p>{{ me.profile?.name }}</p>
|
||||||
|
<button @click="logoutHandler">Log out</button>
|
||||||
|
</TopBar>
|
||||||
|
<router-view />
|
||||||
|
</AppContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useAccount } from "community-jazz-vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import AppContainer from "./components/AppContainer.vue";
|
||||||
|
import TopBar from "./components/TopBar.vue";
|
||||||
|
|
||||||
|
const { me, logOut } = useAccount();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
async function logoutHandler() {
|
||||||
|
await logOut();
|
||||||
|
router.push("/");
|
||||||
|
}
|
||||||
|
</script>
|
||||||
24
examples/community-chat-vue/src/RootApp.vue
Normal file
24
examples/community-chat-vue/src/RootApp.vue
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { JazzVueProvider, PasskeyAuthBasicUI } from "community-jazz-vue";
|
||||||
|
import { h } from "vue";
|
||||||
|
import "jazz-tools/inspector/register-custom-element";
|
||||||
|
import App from "./App.vue";
|
||||||
|
import { apiKey } from "./apiKey";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<JazzVueProvider
|
||||||
|
:sync="{
|
||||||
|
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<PasskeyAuthBasicUI appName="Jazz Vue Chat">
|
||||||
|
<App />
|
||||||
|
</PasskeyAuthBasicUI>
|
||||||
|
<component
|
||||||
|
:is="h('jazz-inspector', {
|
||||||
|
style: { position: 'fixed', left: '20px', bottom: '20px', zIndex: 9999 }
|
||||||
|
})"
|
||||||
|
/>
|
||||||
|
</JazzVueProvider>
|
||||||
|
</template>
|
||||||
1
examples/community-chat-vue/src/apiKey.ts
Normal file
1
examples/community-chat-vue/src/apiKey.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const apiKey = "chat-example-jazz@garden.co";
|
||||||
76
examples/community-chat-vue/src/assets/base.css
Normal file
76
examples/community-chat-vue/src/assets/base.css
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
/* color palette from <https://github.com/vuejs/theme> */
|
||||||
|
:root {
|
||||||
|
--vt-c-white: #ffffff;
|
||||||
|
--vt-c-white-soft: #f8f8f8;
|
||||||
|
--vt-c-white-mute: #f2f2f2;
|
||||||
|
|
||||||
|
--vt-c-black: #181818;
|
||||||
|
--vt-c-black-soft: #222222;
|
||||||
|
--vt-c-black-mute: #282828;
|
||||||
|
|
||||||
|
--vt-c-indigo: #2c3e50;
|
||||||
|
|
||||||
|
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
||||||
|
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
||||||
|
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
||||||
|
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
||||||
|
|
||||||
|
--vt-c-text-light-1: var(--vt-c-indigo);
|
||||||
|
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
||||||
|
--vt-c-text-dark-1: var(--vt-c-white);
|
||||||
|
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* semantic color variables for this project */
|
||||||
|
:root {
|
||||||
|
--color-background: var(--vt-c-white);
|
||||||
|
--color-background-soft: var(--vt-c-white-soft);
|
||||||
|
--color-background-mute: var(--vt-c-white-mute);
|
||||||
|
|
||||||
|
--color-border: var(--vt-c-divider-light-2);
|
||||||
|
--color-border-hover: var(--vt-c-divider-light-1);
|
||||||
|
|
||||||
|
--color-heading: var(--vt-c-text-light-1);
|
||||||
|
--color-text: var(--vt-c-text-light-1);
|
||||||
|
|
||||||
|
--section-gap: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--color-background: var(--vt-c-black);
|
||||||
|
--color-background-soft: var(--vt-c-black-soft);
|
||||||
|
--color-background-mute: var(--vt-c-black-mute);
|
||||||
|
|
||||||
|
--color-border: var(--vt-c-divider-dark-2);
|
||||||
|
--color-border-hover: var(--vt-c-divider-dark-1);
|
||||||
|
|
||||||
|
--color-heading: var(--vt-c-text-dark-1);
|
||||||
|
--color-text: var(--vt-c-text-dark-2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
color: var(--color-text);
|
||||||
|
background: var(--color-background);
|
||||||
|
transition:
|
||||||
|
color 0.5s,
|
||||||
|
background-color 0.5s;
|
||||||
|
line-height: 1.6;
|
||||||
|
font-family:
|
||||||
|
Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
|
||||||
|
Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
||||||
|
font-size: 15px;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
1
examples/community-chat-vue/src/assets/logo.svg
Normal file
1
examples/community-chat-vue/src/assets/logo.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
||||||
|
After Width: | Height: | Size: 276 B |
35
examples/community-chat-vue/src/assets/main.css
Normal file
35
examples/community-chat-vue/src/assets/main.css
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
@import "./base.css";
|
||||||
|
|
||||||
|
#app {
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
a,
|
||||||
|
.green {
|
||||||
|
text-decoration: none;
|
||||||
|
color: hsla(160, 100%, 37%, 1);
|
||||||
|
transition: 0.4s;
|
||||||
|
padding: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
a:hover {
|
||||||
|
background-color: hsla(160, 100%, 37%, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
padding: 0 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
examples/community-chat-vue/src/components/AppContainer.vue
Normal file
13
examples/community-chat-vue/src/components/AppContainer.vue
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="flex flex-col justify-between w-screen h-screen bg-stone-50 dark:bg-black dark:text-white"
|
||||||
|
>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default {
|
||||||
|
name: "AppContainer",
|
||||||
|
};
|
||||||
|
</script>
|
||||||
13
examples/community-chat-vue/src/components/BubbleBody.vue
Normal file
13
examples/community-chat-vue/src/components/BubbleBody.vue
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="rounded-2xl text-sm line-clamp-10 text-ellipsis bg-white max-w-full whitespace-pre-wrap dark:bg-stone-700 dark:text-white py-1 px-3 shadow-sm"
|
||||||
|
>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default {
|
||||||
|
name: "BubbleBody",
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="[alignClass, 'flex flex-col m-2']" role="row">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default {
|
||||||
|
name: "BubbleContainer",
|
||||||
|
props: {
|
||||||
|
fromMe: {
|
||||||
|
type: Boolean,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
alignClass() {
|
||||||
|
return this.fromMe ? "items-end" : "items-start";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
30
examples/community-chat-vue/src/components/BubbleImage.vue
Normal file
30
examples/community-chat-vue/src/components/BubbleImage.vue
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<template>
|
||||||
|
<div class="h-auto max-w-full rounded-t-xl mb-1">
|
||||||
|
<Image
|
||||||
|
:image-id="image.id"
|
||||||
|
alt="Uploaded image"
|
||||||
|
class-names="h-full rounded-t-xl"
|
||||||
|
width="original"
|
||||||
|
height="original"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import { type ImageDefinition } from "jazz-tools";
|
||||||
|
import { Image } from "community-jazz-vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "BubbleImage",
|
||||||
|
components: {
|
||||||
|
Image,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
image: {
|
||||||
|
type: Object as () => ImageDefinition,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
29
examples/community-chat-vue/src/components/BubbleInfo.vue
Normal file
29
examples/community-chat-vue/src/components/BubbleInfo.vue
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<template>
|
||||||
|
<div class="text-xs text-neutral-500 mt-1.5">
|
||||||
|
{{ by }} · {{ formattedTime }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { computed, defineComponent } from "vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "BubbleInfo",
|
||||||
|
props: {
|
||||||
|
by: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
madeAt: {
|
||||||
|
type: Date,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const formattedTime = computed(() => props.madeAt.toLocaleTimeString());
|
||||||
|
return {
|
||||||
|
formattedTime,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
11
examples/community-chat-vue/src/components/ChatBody.vue
Normal file
11
examples/community-chat-vue/src/components/ChatBody.vue
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex-1 overflow-y-auto flex flex-col-reverse" role="application">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default {
|
||||||
|
name: "ChatBody",
|
||||||
|
};
|
||||||
|
</script>
|
||||||
40
examples/community-chat-vue/src/components/ChatBubble.vue
Normal file
40
examples/community-chat-vue/src/components/ChatBubble.vue
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<template>
|
||||||
|
<BubbleContainer :fromMe="lastEdit.by?.isMe">
|
||||||
|
<BubbleBody>
|
||||||
|
<BubbleImage v-if="msg.image" :image="msg.image" />
|
||||||
|
{{ msg.text }}
|
||||||
|
</BubbleBody>
|
||||||
|
<BubbleInfo :by="lastEdit.by?.profile?.name" :madeAt="lastEdit.madeAt" />
|
||||||
|
</BubbleContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { computed, defineComponent } from "vue";
|
||||||
|
import BubbleBody from "./BubbleBody.vue";
|
||||||
|
import BubbleContainer from "./BubbleContainer.vue";
|
||||||
|
import BubbleInfo from "./BubbleInfo.vue";
|
||||||
|
import BubbleImage from "./BubbleImage.vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "ChatBubble",
|
||||||
|
components: {
|
||||||
|
BubbleContainer,
|
||||||
|
BubbleBody,
|
||||||
|
BubbleInfo,
|
||||||
|
BubbleImage,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
msg: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const lastEdit = computed(() => props.msg._edits.text);
|
||||||
|
return {
|
||||||
|
lastEdit,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
55
examples/community-chat-vue/src/components/ChatInput.vue
Normal file
55
examples/community-chat-vue/src/components/ChatInput.vue
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="p-3 bg-white border-t shadow-2xl mt-auto dark:bg-transparent dark:border-stone-800 flex gap-1"
|
||||||
|
>
|
||||||
|
<ImageInput @image-change="handleImageChange" />
|
||||||
|
|
||||||
|
<div class="flex-1">
|
||||||
|
<label class="sr-only" :for="inputId">Type a message and press Enter</label>
|
||||||
|
<input
|
||||||
|
:id="inputId"
|
||||||
|
v-model="inputValue"
|
||||||
|
class="rounded-full py-2 px-4 text-sm border block w-full dark:bg-black dark:text-white dark:border-stone-700"
|
||||||
|
placeholder="Type a message and press Enter"
|
||||||
|
maxlength="2048"
|
||||||
|
@keydown.enter.prevent="submitMessage"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, ref } from "vue";
|
||||||
|
import ImageInput from "./ImageInput.vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "ChatInput",
|
||||||
|
components: {
|
||||||
|
ImageInput,
|
||||||
|
},
|
||||||
|
emits: ["submit", "imageSubmit"],
|
||||||
|
setup(_, { emit }) {
|
||||||
|
const inputId = `input-${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
const inputValue = ref("");
|
||||||
|
|
||||||
|
function submitMessage() {
|
||||||
|
if (!inputValue.value) return;
|
||||||
|
emit("submit", inputValue.value);
|
||||||
|
inputValue.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleImageChange(file: File | undefined) {
|
||||||
|
if (file) {
|
||||||
|
emit("imageSubmit", file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
inputId,
|
||||||
|
inputValue,
|
||||||
|
submitMessage,
|
||||||
|
handleImageChange,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="h-full text-base text-stone-500 flex items-center justify-center px-3 md:text-xl"
|
||||||
|
>
|
||||||
|
Start a conversation below.
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default {
|
||||||
|
name: "EmptyChatMessage",
|
||||||
|
};
|
||||||
|
</script>
|
||||||
63
examples/community-chat-vue/src/components/ImageInput.vue
Normal file
63
examples/community-chat-vue/src/components/ImageInput.vue
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label="Send image"
|
||||||
|
title="Send image"
|
||||||
|
@click="onUploadClick"
|
||||||
|
class="text-stone-500 p-1.5 rounded-full hover:bg-stone-100 hover:text-stone-800 dark:hover:bg-stone-800 dark:hover:text-stone-200 transition-colors"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<rect width="18" height="18" x="3" y="3" rx="2" ry="2" />
|
||||||
|
<circle cx="9" cy="9" r="2" />
|
||||||
|
<path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<label class="sr-only">
|
||||||
|
Image
|
||||||
|
<input
|
||||||
|
ref="inputRef"
|
||||||
|
type="file"
|
||||||
|
accept="image/png, image/jpeg, image/gif"
|
||||||
|
@change="onImageChange"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, ref } from "vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "ImageInput",
|
||||||
|
emits: ["imageChange"],
|
||||||
|
setup(_, { emit }) {
|
||||||
|
const inputRef = ref<HTMLInputElement>();
|
||||||
|
|
||||||
|
function onUploadClick() {
|
||||||
|
inputRef.value?.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onImageChange(event: Event) {
|
||||||
|
const target = event.target as HTMLInputElement;
|
||||||
|
emit("imageChange", target.files?.[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
inputRef,
|
||||||
|
onUploadClick,
|
||||||
|
onImageChange,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
13
examples/community-chat-vue/src/components/TopBar.vue
Normal file
13
examples/community-chat-vue/src/components/TopBar.vue
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="p-3 bg-white w-full flex justify-end gap-1 text-xs border-b dark:bg-transparent dark:border-stone-800"
|
||||||
|
>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default {
|
||||||
|
name: "TopBar",
|
||||||
|
};
|
||||||
|
</script>
|
||||||
1
examples/community-chat-vue/src/index.css
Normal file
1
examples/community-chat-vue/src/index.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
8
examples/community-chat-vue/src/main.ts
Normal file
8
examples/community-chat-vue/src/main.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { createApp } from "vue";
|
||||||
|
import RootApp from "./RootApp.vue";
|
||||||
|
import "./index.css";
|
||||||
|
import router from "./router";
|
||||||
|
|
||||||
|
const app = createApp(RootApp);
|
||||||
|
app.use(router);
|
||||||
|
app.mount("#app");
|
||||||
17
examples/community-chat-vue/src/router.ts
Normal file
17
examples/community-chat-vue/src/router.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { createRouter, createWebHistory } from "vue-router";
|
||||||
|
import Chat from "./views/ChatView.vue";
|
||||||
|
import Home from "./views/HomeView.vue";
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
name: "Home",
|
||||||
|
component: Home,
|
||||||
|
},
|
||||||
|
{ path: "/chat/:chatId", name: "Chat", component: Chat, props: true },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
8
examples/community-chat-vue/src/schema.ts
Normal file
8
examples/community-chat-vue/src/schema.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { co } from "jazz-tools";
|
||||||
|
|
||||||
|
export const Message = co.map({
|
||||||
|
text: co.plainText(),
|
||||||
|
image: co.optional(co.image()),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Chat = co.list(Message);
|
||||||
108
examples/community-chat-vue/src/views/ChatView.vue
Normal file
108
examples/community-chat-vue/src/views/ChatView.vue
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="chat">
|
||||||
|
<ChatBody>
|
||||||
|
<template v-if="chat.length > 0">
|
||||||
|
<ChatBubble v-for="msg in displayedMessages" :key="msg.id" :msg="msg" />
|
||||||
|
</template>
|
||||||
|
<EmptyChatMessage v-else />
|
||||||
|
<button
|
||||||
|
v-if="chat.length > showNLastMessages"
|
||||||
|
class="px-4 py-1 block mx-auto my-2 border rounded"
|
||||||
|
@click="showMoreMessages"
|
||||||
|
>
|
||||||
|
Show more
|
||||||
|
</button>
|
||||||
|
</ChatBody>
|
||||||
|
<ChatInput @submit="handleSubmit" @image-submit="handleImageSubmit" />
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex-1 flex justify-center items-center">Loading...</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { useCoState, createImage } from "community-jazz-vue";
|
||||||
|
import { CoPlainText, type ID } from "jazz-tools";
|
||||||
|
import { type PropType, computed, defineComponent, ref } from "vue";
|
||||||
|
import ChatBody from "../components/ChatBody.vue";
|
||||||
|
import ChatBubble from "../components/ChatBubble.vue";
|
||||||
|
import ChatInput from "../components/ChatInput.vue";
|
||||||
|
import EmptyChatMessage from "../components/EmptyChatMessage.vue";
|
||||||
|
import { Chat, Message } from "../schema";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "ChatView",
|
||||||
|
components: {
|
||||||
|
ChatBody,
|
||||||
|
ChatInput,
|
||||||
|
EmptyChatMessage,
|
||||||
|
ChatBubble,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
chatId: {
|
||||||
|
type: String as unknown as PropType<ID<Chat>>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const chat = useCoState(Chat, props.chatId, { resolve: { $each: true } });
|
||||||
|
const showNLastMessages = ref(30);
|
||||||
|
|
||||||
|
const displayedMessages = computed(() => {
|
||||||
|
return chat.value?.slice(-showNLastMessages.value).reverse();
|
||||||
|
});
|
||||||
|
|
||||||
|
function showMoreMessages() {
|
||||||
|
showNLastMessages.value += 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSubmit(text: string) {
|
||||||
|
if (chat.value) {
|
||||||
|
chat.value.push(
|
||||||
|
Message.create(
|
||||||
|
{ text: CoPlainText.create(text, chat.value._owner) },
|
||||||
|
chat.value._owner,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleImageSubmit(file: File) {
|
||||||
|
if (!chat.value) return;
|
||||||
|
|
||||||
|
if (file.size > 5000000) {
|
||||||
|
alert("Please upload an image less than 5MB.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const image = await createImage(file, {
|
||||||
|
owner: chat.value._owner,
|
||||||
|
progressive: true,
|
||||||
|
placeholder: "blur",
|
||||||
|
});
|
||||||
|
|
||||||
|
chat.value.push(
|
||||||
|
Message.create(
|
||||||
|
{
|
||||||
|
text: CoPlainText.create(file.name, chat.value._owner),
|
||||||
|
image: image,
|
||||||
|
},
|
||||||
|
chat.value._owner,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to upload image:", error);
|
||||||
|
alert("Failed to upload image. Please try again.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
chat,
|
||||||
|
showNLastMessages,
|
||||||
|
displayedMessages,
|
||||||
|
showMoreMessages,
|
||||||
|
handleSubmit,
|
||||||
|
handleImageSubmit,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
33
examples/community-chat-vue/src/views/HomeView.vue
Normal file
33
examples/community-chat-vue/src/views/HomeView.vue
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="!me">Loading...</div>
|
||||||
|
<div v-else>Creating a new chat...</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useAccount, useIsAuthenticated } from "community-jazz-vue";
|
||||||
|
import { Group } from "jazz-tools";
|
||||||
|
import { watch } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import { Chat } from "../schema";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { me } = useAccount();
|
||||||
|
const isAuthenticated = useIsAuthenticated();
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[me, isAuthenticated],
|
||||||
|
([currentMe, authenticated]) => {
|
||||||
|
if (currentMe && authenticated) {
|
||||||
|
try {
|
||||||
|
const group = Group.create({ owner: currentMe });
|
||||||
|
group.addMember("everyone", "writer");
|
||||||
|
const chat = Chat.create([], { owner: group });
|
||||||
|
router.push(`/chat/${chat.id}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to create chat:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
</script>
|
||||||
14
examples/community-chat-vue/tsconfig.app.json
Normal file
14
examples/community-chat-vue/tsconfig.app.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||||
|
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||||
|
"exclude": ["src/**/__tests__/*"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
examples/community-chat-vue/tsconfig.json
Normal file
11
examples/community-chat-vue/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.node.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.app.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
19
examples/community-chat-vue/tsconfig.node.json
Normal file
19
examples/community-chat-vue/tsconfig.node.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"extends": "@tsconfig/node20/tsconfig.json",
|
||||||
|
"include": [
|
||||||
|
"vite.config.*",
|
||||||
|
"vitest.config.*",
|
||||||
|
"cypress.config.*",
|
||||||
|
"nightwatch.conf.*",
|
||||||
|
"playwright.config.*"
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"types": ["node"]
|
||||||
|
}
|
||||||
|
}
|
||||||
16
examples/community-chat-vue/vite.config.ts
Normal file
16
examples/community-chat-vue/vite.config.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { URL, fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
import vue from "@vitejs/plugin-vue";
|
||||||
|
import vueJsx from "@vitejs/plugin-vue-jsx";
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
import vueDevTools from "vite-plugin-vue-devtools";
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue(), vueJsx(), vueDevTools()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
1
examples/community-clerk-vue/.env.example
Normal file
1
examples/community-clerk-vue/.env.example
Normal file
@@ -0,0 +1 @@
|
|||||||
|
VITE_CLERK_PUBLISHABLE_KEY=
|
||||||
1
examples/community-clerk-vue/.env.test
Normal file
1
examples/community-clerk-vue/.env.test
Normal file
@@ -0,0 +1 @@
|
|||||||
|
VITE_CLERK_PUBLISHABLE_KEY=pk_test_ZXZpZGVudC1kYW5lLTg5LmNsZXJrLmFjY291bnRzLmRldiQ
|
||||||
32
examples/community-clerk-vue/.gitignore
vendored
Normal file
32
examples/community-clerk-vue/.gitignore
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
playwright-report
|
||||||
|
|
||||||
|
# Env
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
!.env.test
|
||||||
7
examples/community-clerk-vue/CHANGELOG.md
Normal file
7
examples/community-clerk-vue/CHANGELOG.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# community-clerk-vue
|
||||||
|
|
||||||
|
## 0.15.4
|
||||||
|
|
||||||
|
- rewrite from React to Vue
|
||||||
|
- jazz-tools@0.15.4
|
||||||
|
- community-jazz-vue@0.15.4
|
||||||
82
examples/community-clerk-vue/README.md
Normal file
82
examples/community-clerk-vue/README.md
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
# Clerk authentication example with Jazz and Vue
|
||||||
|
|
||||||
|
This is an example of how to use clerk authentication with Jazz in a Vue.js application.
|
||||||
|
|
||||||
|
Live version: [](Todo)
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
You can either
|
||||||
|
|
||||||
|
1. Clone the jazz repository, and run the app within the monorepo.
|
||||||
|
2. Or create a new Jazz project using this example as a template.
|
||||||
|
|
||||||
|
### Using the example as a template
|
||||||
|
|
||||||
|
Create a new Jazz project, and use this example as a template.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx create-jazz-app@latest clerk-vue-app --example community-clerk-vue
|
||||||
|
```
|
||||||
|
|
||||||
|
Go to the new project directory.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd clerk-vue-app
|
||||||
|
```
|
||||||
|
|
||||||
|
Rename .env.example to .env
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mv .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Update `VITE_CLERK_PUBLISHABLE_KEY` with your [Publishable Key](https://clerk.com/docs/deployments/clerk-environment-variables#clerk-publishable-and-secret-keys) from Clerk.
|
||||||
|
|
||||||
|
Run the dev server.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using the monorepo
|
||||||
|
|
||||||
|
This requires `pnpm` to be installed, see [https://pnpm.io/installation](https://pnpm.io/installation).
|
||||||
|
|
||||||
|
Clone the jazz repository.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/garden-co/jazz.git
|
||||||
|
```
|
||||||
|
|
||||||
|
Install and build dependencies.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm i && npx turbo build
|
||||||
|
```
|
||||||
|
|
||||||
|
Go to the example directory.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd jazz/examples/community-clerk-vue/
|
||||||
|
```
|
||||||
|
|
||||||
|
Rename .env.example to .env
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mv .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Update `VITE_CLERK_PUBLISHABLE_KEY` with your [Publishable Key](https://clerk.com/docs/deployments/clerk-environment-variables#clerk-publishable-and-secret-keys) from Clerk.
|
||||||
|
|
||||||
|
Start the dev server.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open [http://localhost:5173](http://localhost:5173) with your browser to see the result.
|
||||||
|
|
||||||
|
## Questions / problems / feedback
|
||||||
|
|
||||||
|
If you have feedback, let us know on [Discord](https://discord.gg/utDMjHYg42) or open an issue or PR to fix something that seems wrong.
|
||||||
16
examples/community-clerk-vue/index.html
Normal file
16
examples/community-clerk-vue/index.html
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Minimal Vue Auth Clerk Example | Jazz</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
30
examples/community-clerk-vue/package.json
Normal file
30
examples/community-clerk-vue/package.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "community-clerk-vue",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"format-and-lint": "biome check .",
|
||||||
|
"format-and-lint:fix": "biome check . --write",
|
||||||
|
"test:e2e": "playwright test",
|
||||||
|
"test:e2e:ui": "playwright test --ui"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@clerk/vue": "^1.8.10",
|
||||||
|
"community-jazz-vue": "workspace:*",
|
||||||
|
"jazz-tools": "workspace:*",
|
||||||
|
"vue": "^3.5.13",
|
||||||
|
"vue-router": "^4.5.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@biomejs/biome": "1.9.4",
|
||||||
|
"@clerk/testing": "^1.10.8",
|
||||||
|
"@playwright/test": "^1.50.1",
|
||||||
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
|
"globals": "^15.11.0",
|
||||||
|
"typescript": "5.6.2",
|
||||||
|
"vite": "^6.3.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
53
examples/community-clerk-vue/playwright.config.ts
Normal file
53
examples/community-clerk-vue/playwright.config.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { defineConfig, devices } from "@playwright/test";
|
||||||
|
import isCI from "is-ci";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read environment variables from file.
|
||||||
|
* https://github.com/motdotla/dotenv
|
||||||
|
*/
|
||||||
|
// import dotenv from 'dotenv';
|
||||||
|
// dotenv.config({ path: path.resolve(__dirname, '.env') });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See https://playwright.dev/docs/test-configuration.
|
||||||
|
*/
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: "./tests",
|
||||||
|
/* Run tests in files in parallel */
|
||||||
|
fullyParallel: true,
|
||||||
|
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||||
|
forbidOnly: isCI,
|
||||||
|
/* Retry on CI only */
|
||||||
|
retries: isCI ? 2 : 0,
|
||||||
|
/* Opt out of parallel tests on CI. */
|
||||||
|
workers: isCI ? 1 : undefined,
|
||||||
|
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||||
|
reporter: "html",
|
||||||
|
|
||||||
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||||
|
use: {
|
||||||
|
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||||
|
baseURL: "http://localhost:5173/",
|
||||||
|
|
||||||
|
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||||
|
trace: "on-first-retry",
|
||||||
|
permissions: ["clipboard-read", "clipboard-write"],
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Configure projects for major browsers */
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: "chromium",
|
||||||
|
use: { ...devices["Desktop Chrome"] },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
/* Run your local dev server before starting the tests */
|
||||||
|
webServer: [
|
||||||
|
{
|
||||||
|
command: "pnpm preview --port 5173",
|
||||||
|
url: "http://localhost:5173/",
|
||||||
|
reuseExistingServer: !isCI,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
BIN
examples/community-clerk-vue/public/favicon.ico
Normal file
BIN
examples/community-clerk-vue/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
37
examples/community-clerk-vue/src/App.vue
Normal file
37
examples/community-clerk-vue/src/App.vue
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { SignInButton, SignOutButton } from "@clerk/vue";
|
||||||
|
import { useIsAuthenticated, useJazzContext } from "community-jazz-vue";
|
||||||
|
import { Account } from "jazz-tools";
|
||||||
|
import { computed } from "vue";
|
||||||
|
|
||||||
|
const context = useJazzContext<Account>();
|
||||||
|
const isAuthenticated = useIsAuthenticated();
|
||||||
|
|
||||||
|
const me = computed(() => {
|
||||||
|
const ctx = context.value;
|
||||||
|
console.log("[App] me computed:", { hasContext: !!ctx, ctx });
|
||||||
|
if (!ctx) return null;
|
||||||
|
return "me" in ctx ? ctx.me : null;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="isAuthenticated" class="container">
|
||||||
|
<h1>You're logged in</h1>
|
||||||
|
<p>Welcome back, {{ me?.profile?.name || "User" }}</p>
|
||||||
|
<SignOutButton>Logout</SignOutButton>
|
||||||
|
</div>
|
||||||
|
<div v-else class="container">
|
||||||
|
<h1>You're not logged in</h1>
|
||||||
|
<SignInButton />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.container {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
1
examples/community-clerk-vue/src/apiKey.ts
Normal file
1
examples/community-clerk-vue/src/apiKey.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const apiKey = "minimal-auth-clerk-example@garden.co";
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { SignOutButton } from "@clerk/vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SignOutButton>Simulate expiration</SignOutButton>
|
||||||
|
</template>
|
||||||
31
examples/community-clerk-vue/src/components/JazzProvider.vue
Normal file
31
examples/community-clerk-vue/src/components/JazzProvider.vue
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useClerk } from "@clerk/vue";
|
||||||
|
import { JazzVueProviderWithClerk } from "community-jazz-vue";
|
||||||
|
import { h } from "vue";
|
||||||
|
|
||||||
|
import { apiKey } from "../apiKey";
|
||||||
|
|
||||||
|
const clerk = useClerk();
|
||||||
|
|
||||||
|
import "jazz-tools/inspector/register-custom-element";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<JazzVueProviderWithClerk
|
||||||
|
v-if="clerk"
|
||||||
|
:clerk="clerk"
|
||||||
|
:sync="{
|
||||||
|
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
<component
|
||||||
|
:is="h('jazz-inspector', {
|
||||||
|
style: { position: 'fixed', bottom: '20px', left: '20px', zIndex: 9999 }
|
||||||
|
})"
|
||||||
|
/>
|
||||||
|
</JazzVueProviderWithClerk>
|
||||||
|
<div v-else>
|
||||||
|
<p>Loading Clerk...</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
17
examples/community-clerk-vue/src/components/RootApp.vue
Normal file
17
examples/community-clerk-vue/src/components/RootApp.vue
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from "vue";
|
||||||
|
import App from "../App.vue";
|
||||||
|
import ExpirationTest from "./ExpirationTest.vue";
|
||||||
|
import JazzProvider from "./JazzProvider.vue";
|
||||||
|
|
||||||
|
const isExpirationTest = computed(() =>
|
||||||
|
location.search.includes("expirationTest"),
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ExpirationTest v-if="isExpirationTest" />
|
||||||
|
<JazzProvider v-else>
|
||||||
|
<App />
|
||||||
|
</JazzProvider>
|
||||||
|
</template>
|
||||||
72
examples/community-clerk-vue/src/index.css
Normal file
72
examples/community-clerk-vue/src/index.css
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
:root {
|
||||||
|
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
color-scheme: light dark;
|
||||||
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
background-color: #242424;
|
||||||
|
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #646cff;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #535bf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
place-items: center;
|
||||||
|
min-width: 320px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 3.2em;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 0;
|
||||||
|
padding: 0.6em 1.2em;
|
||||||
|
font-weight: 500;
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root {
|
||||||
|
color: #213547;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #747bff;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
20
examples/community-clerk-vue/src/main.ts
Normal file
20
examples/community-clerk-vue/src/main.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { clerkPlugin } from "@clerk/vue";
|
||||||
|
import { createApp } from "vue";
|
||||||
|
import RootApp from "./components/RootApp.vue";
|
||||||
|
import "./index.css";
|
||||||
|
|
||||||
|
// Import your publishable key
|
||||||
|
const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY;
|
||||||
|
|
||||||
|
if (!PUBLISHABLE_KEY) {
|
||||||
|
throw new Error("Add your Clerk publishable key to the .env.local file");
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = createApp(RootApp);
|
||||||
|
|
||||||
|
app.use(clerkPlugin, {
|
||||||
|
publishableKey: PUBLISHABLE_KEY,
|
||||||
|
afterSignOutUrl: "/",
|
||||||
|
});
|
||||||
|
|
||||||
|
app.mount("#app");
|
||||||
15
examples/community-clerk-vue/src/vite-env.d.ts
vendored
Normal file
15
examples/community-clerk-vue/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
declare module "*.vue" {
|
||||||
|
import type { DefineComponent } from "vue";
|
||||||
|
const component: DefineComponent<{}, {}, any>;
|
||||||
|
export default component;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
readonly VITE_CLERK_PUBLISHABLE_KEY: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv;
|
||||||
|
}
|
||||||
122
examples/community-clerk-vue/tests/clerk-integration.spec.ts
Normal file
122
examples/community-clerk-vue/tests/clerk-integration.spec.ts
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
|
||||||
|
test("clerk integration - complete sign in flow", async ({ page }) => {
|
||||||
|
await page.goto("/");
|
||||||
|
|
||||||
|
// Wait for page to load
|
||||||
|
await page.waitForTimeout(3000);
|
||||||
|
|
||||||
|
console.log("=== INITIAL PAGE STATE ===");
|
||||||
|
const pageText = await page.textContent("body");
|
||||||
|
console.log("Full page text:", pageText);
|
||||||
|
console.log(
|
||||||
|
'Page contains "You\'re not logged in":',
|
||||||
|
pageText?.includes("You're not logged in"),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if Clerk is loaded
|
||||||
|
const clerkLoaded = await page.evaluate(() => {
|
||||||
|
return typeof (window as any).Clerk !== "undefined";
|
||||||
|
});
|
||||||
|
console.log("Clerk loaded:", clerkLoaded);
|
||||||
|
|
||||||
|
// Check if Clerk publishable key is available in the page
|
||||||
|
const hasClerkKey = await page.evaluate(() => {
|
||||||
|
return document.querySelector('script[src*="clerk"]') !== null;
|
||||||
|
});
|
||||||
|
console.log("Clerk scripts loaded:", hasClerkKey);
|
||||||
|
|
||||||
|
// Check for any Vue app errors
|
||||||
|
const vueErrors = await page.evaluate(() => {
|
||||||
|
return (
|
||||||
|
(window as any).__VUE_DEVTOOLS_GLOBAL_HOOK__?.Vue?.config?.errorHandler ||
|
||||||
|
null
|
||||||
|
);
|
||||||
|
});
|
||||||
|
console.log("Vue errors:", vueErrors);
|
||||||
|
|
||||||
|
console.log("=== CLICKING SIGN IN ===");
|
||||||
|
const signInButton = page.getByRole("button", { name: "Sign in" });
|
||||||
|
const signInExists = await signInButton.isVisible();
|
||||||
|
console.log("Sign in button exists:", signInExists);
|
||||||
|
|
||||||
|
if (!signInExists) {
|
||||||
|
console.log("Sign in button not found, taking screenshot");
|
||||||
|
await page.screenshot({ path: "debug-no-signin-button.png" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await signInButton.click();
|
||||||
|
console.log("Clicked sign in button");
|
||||||
|
|
||||||
|
// Wait and check what happens
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// Check for any modals or overlays
|
||||||
|
const modals = await page
|
||||||
|
.locator(
|
||||||
|
'[role="dialog"], .cl-modal, .clerk-modal, [data-testid*="modal"], [class*="modal"]',
|
||||||
|
)
|
||||||
|
.all();
|
||||||
|
console.log("Found modals/dialogs:", modals.length);
|
||||||
|
|
||||||
|
for (let i = 0; i < modals.length; i++) {
|
||||||
|
const modal = modals[i];
|
||||||
|
const isVisible = await modal.isVisible();
|
||||||
|
const className = await modal.getAttribute("class");
|
||||||
|
console.log(`Modal ${i}: visible=${isVisible}, class="${className}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for any iframes (Clerk might use iframes)
|
||||||
|
const iframes = await page.locator("iframe").all();
|
||||||
|
console.log("Found iframes:", iframes.length);
|
||||||
|
|
||||||
|
for (let i = 0; i < iframes.length; i++) {
|
||||||
|
const iframe = iframes[i];
|
||||||
|
const src = await iframe.getAttribute("src");
|
||||||
|
const isVisible = await iframe.isVisible();
|
||||||
|
console.log(`Iframe ${i}: visible=${isVisible}, src="${src}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for any new elements that appeared
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
const allInputs = await page.locator("input").all();
|
||||||
|
console.log("Total inputs on page:", allInputs.length);
|
||||||
|
|
||||||
|
for (let i = 0; i < allInputs.length; i++) {
|
||||||
|
const input = allInputs[i];
|
||||||
|
const type = await input.getAttribute("type");
|
||||||
|
const name = await input.getAttribute("name");
|
||||||
|
const placeholder = await input.getAttribute("placeholder");
|
||||||
|
const isVisible = await input.isVisible();
|
||||||
|
console.log(
|
||||||
|
`Input ${i}: type="${type}" name="${name}" placeholder="${placeholder}" visible=${isVisible}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for any error messages
|
||||||
|
const errorElements = await page
|
||||||
|
.locator('[class*="error"], [role="alert"], .cl-formFieldErrorText')
|
||||||
|
.all();
|
||||||
|
console.log("Found error elements:", errorElements.length);
|
||||||
|
|
||||||
|
for (let i = 0; i < errorElements.length; i++) {
|
||||||
|
const error = errorElements[i];
|
||||||
|
const text = await error.textContent();
|
||||||
|
const isVisible = await error.isVisible();
|
||||||
|
console.log(`Error ${i}: visible=${isVisible}, text="${text}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check console errors
|
||||||
|
const logs: string[] = [];
|
||||||
|
page.on("console", (msg) => {
|
||||||
|
if (msg.type() === "error") {
|
||||||
|
logs.push(msg.text());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
if (logs.length > 0) {
|
||||||
|
console.log("Console errors:", logs);
|
||||||
|
}
|
||||||
|
});
|
||||||
68
examples/community-clerk-vue/tests/expiration.spec.ts
Normal file
68
examples/community-clerk-vue/tests/expiration.spec.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { clerk } from "@clerk/testing/playwright";
|
||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
|
||||||
|
test("login & expiration", async ({ page, context }) => {
|
||||||
|
// Clear cookies first
|
||||||
|
await context.clearCookies();
|
||||||
|
|
||||||
|
await page.goto("/");
|
||||||
|
|
||||||
|
// Clear storage after page loads to avoid security errors
|
||||||
|
await page.evaluate(() => {
|
||||||
|
try {
|
||||||
|
localStorage.clear();
|
||||||
|
sessionStorage.clear();
|
||||||
|
// Clear IndexedDB
|
||||||
|
if ("indexedDB" in window) {
|
||||||
|
indexedDB.databases().then((databases) => {
|
||||||
|
databases.forEach((db) => {
|
||||||
|
if (db.name) indexedDB.deleteDatabase(db.name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Storage clear failed:", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for page to load completely
|
||||||
|
await page.waitForTimeout(3000);
|
||||||
|
|
||||||
|
// Verify initial logged out state
|
||||||
|
await expect(page.getByText("You're not logged in")).toBeVisible();
|
||||||
|
|
||||||
|
// Manual login (works in our test environment)
|
||||||
|
await page.getByRole("button", { name: "Sign in" }).click();
|
||||||
|
await page.waitForTimeout(5000);
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByPlaceholder("Enter your email address")
|
||||||
|
.waitFor({ timeout: 30000 });
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByPlaceholder("Enter your email address")
|
||||||
|
.fill("guido+clerk-test@garden.co");
|
||||||
|
|
||||||
|
await page.keyboard.press("Enter");
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByPlaceholder("Enter your password")
|
||||||
|
.fill("guido+clerk-test@garden.co");
|
||||||
|
|
||||||
|
await page.keyboard.press("Enter");
|
||||||
|
|
||||||
|
await page.waitForURL("/");
|
||||||
|
|
||||||
|
// Verify user is logged in
|
||||||
|
await page.getByText("You're logged in").waitFor({ state: "visible" });
|
||||||
|
expect(page.getByText("You're logged in")).toBeVisible();
|
||||||
|
|
||||||
|
// Simulate expiration using clerk.signOut (ignore the warning about missing setup)
|
||||||
|
await clerk.signOut({ page });
|
||||||
|
|
||||||
|
// Navigate to home page to check logout state
|
||||||
|
await page.goto("/");
|
||||||
|
|
||||||
|
// Wait for logout to be processed and UI to update
|
||||||
|
await page.getByText("You're not logged in").waitFor({ state: "visible" });
|
||||||
|
});
|
||||||
62
examples/community-clerk-vue/tests/logout.spec.ts
Normal file
62
examples/community-clerk-vue/tests/logout.spec.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
|
||||||
|
test("login & logout", async ({ page }) => {
|
||||||
|
// Capture console messages
|
||||||
|
page.on("console", (msg) => {
|
||||||
|
console.log(`[BROWSER ${msg.type().toUpperCase()}]:`, msg.text());
|
||||||
|
});
|
||||||
|
|
||||||
|
// Capture network failures
|
||||||
|
page.on("requestfailed", (request) => {
|
||||||
|
console.log(
|
||||||
|
`[NETWORK FAILED]:`,
|
||||||
|
request.url(),
|
||||||
|
request.failure()?.errorText,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto("/");
|
||||||
|
|
||||||
|
// Wait for page to load completely
|
||||||
|
await page.waitForTimeout(3000);
|
||||||
|
|
||||||
|
// Wait for the page to load and show the logged out state
|
||||||
|
await expect(page.getByText("You're not logged in")).toBeVisible();
|
||||||
|
|
||||||
|
// Click sign in and wait for Clerk modal to appear
|
||||||
|
await page.getByRole("button", { name: "Sign in" }).click();
|
||||||
|
|
||||||
|
// Wait a bit for Clerk to initialize and show the form
|
||||||
|
await page.waitForTimeout(5000);
|
||||||
|
|
||||||
|
// Wait for Clerk to load and show the email input with a longer timeout
|
||||||
|
await page
|
||||||
|
.getByPlaceholder("Enter your email address")
|
||||||
|
.waitFor({ timeout: 15000 });
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByPlaceholder("Enter your email address")
|
||||||
|
.fill("guido+clerk-test@garden.co");
|
||||||
|
|
||||||
|
await page.keyboard.press("Enter");
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByPlaceholder("Enter your password")
|
||||||
|
.fill("guido+clerk-test@garden.co");
|
||||||
|
|
||||||
|
console.log("Pressing Enter to submit password...");
|
||||||
|
await page.keyboard.press("Enter");
|
||||||
|
|
||||||
|
console.log("Waiting for navigation to /...");
|
||||||
|
await page.waitForURL("/", { timeout: 60000 });
|
||||||
|
|
||||||
|
await page.getByText("You're logged in").waitFor({ state: "visible" });
|
||||||
|
|
||||||
|
expect(page.getByText("You're logged in")).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByRole("button", { name: "Logout" }).click();
|
||||||
|
|
||||||
|
await page.getByText("You're not logged in").waitFor({ state: "visible" });
|
||||||
|
|
||||||
|
expect(page.getByText("You're not logged in")).toBeVisible();
|
||||||
|
});
|
||||||
67
examples/community-clerk-vue/tests/reload.spec.ts
Normal file
67
examples/community-clerk-vue/tests/reload.spec.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
|
||||||
|
test("login & reload", async ({ page, context }) => {
|
||||||
|
// Clear cookies first
|
||||||
|
await context.clearCookies();
|
||||||
|
|
||||||
|
await page.goto("/");
|
||||||
|
|
||||||
|
// Clear storage after page loads to avoid security errors
|
||||||
|
await page.evaluate(() => {
|
||||||
|
try {
|
||||||
|
localStorage.clear();
|
||||||
|
sessionStorage.clear();
|
||||||
|
// Clear IndexedDB
|
||||||
|
if ("indexedDB" in window) {
|
||||||
|
indexedDB.databases().then((databases) => {
|
||||||
|
databases.forEach((db) => {
|
||||||
|
if (db.name) indexedDB.deleteDatabase(db.name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Storage clear failed:", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for page to load completely (like the working tests)
|
||||||
|
await page.waitForTimeout(3000);
|
||||||
|
|
||||||
|
// Wait for the page to load and show the logged out state
|
||||||
|
await expect(page.getByText("You're not logged in")).toBeVisible();
|
||||||
|
|
||||||
|
// Click sign in and wait for Clerk modal to appear
|
||||||
|
await page.getByRole("button", { name: "Sign in" }).click();
|
||||||
|
|
||||||
|
// Wait a bit for Clerk to initialize and show the form
|
||||||
|
await page.waitForTimeout(5000);
|
||||||
|
|
||||||
|
// Wait for Clerk to load and show the email input with a longer timeout
|
||||||
|
await page
|
||||||
|
.getByPlaceholder("Enter your email address")
|
||||||
|
.waitFor({ timeout: 30000 });
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByPlaceholder("Enter your email address")
|
||||||
|
.fill("guido+clerk-test@garden.co");
|
||||||
|
|
||||||
|
await page.keyboard.press("Enter");
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByPlaceholder("Enter your password")
|
||||||
|
.fill("guido+clerk-test@garden.co");
|
||||||
|
|
||||||
|
await page.keyboard.press("Enter");
|
||||||
|
|
||||||
|
await page.waitForURL("/");
|
||||||
|
|
||||||
|
await page.getByText("You're logged in").waitFor({ state: "visible" });
|
||||||
|
|
||||||
|
expect(page.getByText("You're logged in")).toBeVisible();
|
||||||
|
|
||||||
|
await page.reload();
|
||||||
|
|
||||||
|
await page.getByText("You're logged in").waitFor({ state: "visible" });
|
||||||
|
|
||||||
|
expect(page.getByText("You're logged in")).toBeVisible();
|
||||||
|
});
|
||||||
24
examples/community-clerk-vue/tsconfig.app.json
Normal file
24
examples/community-clerk-vue/tsconfig.app.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
7
examples/community-clerk-vue/tsconfig.json
Normal file
7
examples/community-clerk-vue/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
22
examples/community-clerk-vue/tsconfig.node.json
Normal file
22
examples/community-clerk-vue/tsconfig.node.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
3
examples/community-clerk-vue/vercel.json
Normal file
3
examples/community-clerk-vue/vercel.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"ignoreCommand": "npx turbo-ignore"
|
||||||
|
}
|
||||||
7
examples/community-clerk-vue/vite.config.ts
Normal file
7
examples/community-clerk-vue/vite.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import vue from "@vitejs/plugin-vue";
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
});
|
||||||
7
examples/community-todo-vue/.gitignore
vendored
Normal file
7
examples/community-todo-vue/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
dist
|
||||||
|
|
||||||
|
# env files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
!.env.test
|
||||||
1024
examples/community-todo-vue/CHANGELOG.md
Normal file
1024
examples/community-todo-vue/CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load Diff
61
examples/community-todo-vue/README.md
Normal file
61
examples/community-todo-vue/README.md
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
# Todo list example with Jazz and Vue
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
You can either
|
||||||
|
1. Clone the jazz repository, and run the app within the monorepo.
|
||||||
|
2. Or create a new Jazz project using this example as a template.
|
||||||
|
|
||||||
|
|
||||||
|
### Using the example as a template
|
||||||
|
|
||||||
|
Create a new Jazz project, and use this example as a template.
|
||||||
|
```bash
|
||||||
|
npx create-jazz-app@latest todo-vue-app --example todo-vue
|
||||||
|
```
|
||||||
|
|
||||||
|
Go to the new project directory.
|
||||||
|
```bash
|
||||||
|
cd todo-vue-app
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the dev server.
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using the monorepo
|
||||||
|
|
||||||
|
This requires `pnpm` to be installed, see [https://pnpm.io/installation](https://pnpm.io/installation).
|
||||||
|
|
||||||
|
Clone the jazz repository.
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/garden-co/jazz.git
|
||||||
|
```
|
||||||
|
|
||||||
|
Install and build dependencies.
|
||||||
|
```bash
|
||||||
|
pnpm i && npx turbo build
|
||||||
|
```
|
||||||
|
|
||||||
|
Go to the example directory.
|
||||||
|
```bash
|
||||||
|
cd jazz/examples/community-todo-vue/
|
||||||
|
```
|
||||||
|
|
||||||
|
Start the dev server.
|
||||||
|
```bash
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open [http://localhost:5173](http://localhost:5173) with your browser to see the result.
|
||||||
|
|
||||||
|
## Questions / problems / feedback
|
||||||
|
|
||||||
|
If you have feedback, let us know on [Discord](https://discord.gg/utDMjHYg42) or open an issue or PR to fix something that seems wrong.
|
||||||
|
|
||||||
|
## Configuration: sync server
|
||||||
|
|
||||||
|
By default, the example app uses [Jazz Cloud](https://jazz.tools/cloud) (`wss://cloud.jazz.tools`) - so cross-device use, invites and collaboration should just work.
|
||||||
|
|
||||||
|
You can also run a local sync server by running`npx jazz-run sync`, and setting the `sync` parameter of`JazzProvider` in [./src/main.ts](./src/main.ts) to`{ peer: "ws://localhost:4200" }`.
|
||||||
1
examples/community-todo-vue/env.d.ts
vendored
Normal file
1
examples/community-todo-vue/env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
13
examples/community-todo-vue/index.html
Normal file
13
examples/community-todo-vue/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Jazz Todo List Example</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
37
examples/community-todo-vue/package.json
Normal file
37
examples/community-todo-vue/package.json
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "community-todo-vue",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build-type-check": "run-p type-check \"build-only {@}\" --",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"build": "vite build",
|
||||||
|
"type-check": "vue-tsc --build --force",
|
||||||
|
"format-and-lint": "biome check .",
|
||||||
|
"format-and-lint:fix": "biome check . --write"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"jazz-tools": "workspace:*",
|
||||||
|
"community-jazz-vue": "workspace:*",
|
||||||
|
"vue": "^3.5.11",
|
||||||
|
"vue-router": "^4.4.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/postcss": "^4.1.10",
|
||||||
|
"@tsconfig/node22": "^22.0.0",
|
||||||
|
"@types/node": "^22.5.1",
|
||||||
|
"@vitejs/plugin-vue": "^5.1.4",
|
||||||
|
"@vitejs/plugin-vue-jsx": "^4.0.1",
|
||||||
|
"@vue/tsconfig": "^0.5.1",
|
||||||
|
"eslint": "^9.7.0",
|
||||||
|
"eslint-plugin-vue": "^9.28.0",
|
||||||
|
"npm-run-all2": "^6.2.3",
|
||||||
|
"postcss": "^8.4.40",
|
||||||
|
"tailwindcss": "^4.1.10",
|
||||||
|
"typescript": "5.6.2",
|
||||||
|
"vite": "6.3.5",
|
||||||
|
"vite-plugin-vue-devtools": "^7.4.6",
|
||||||
|
"vue-tsc": "^2.1.6"
|
||||||
|
}
|
||||||
|
}
|
||||||
5
examples/community-todo-vue/postcss.config.js
Normal file
5
examples/community-todo-vue/postcss.config.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
"@tailwindcss/postcss": {},
|
||||||
|
},
|
||||||
|
};
|
||||||
BIN
examples/community-todo-vue/public/favicon.ico
Normal file
BIN
examples/community-todo-vue/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
92
examples/community-todo-vue/src/App.vue
Normal file
92
examples/community-todo-vue/src/App.vue
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<header v-if="me" class="app-header">
|
||||||
|
<h1>Todo App</h1>
|
||||||
|
<div class="user-section">
|
||||||
|
<span>{{ me.profile?.name }}</span>
|
||||||
|
<button class="logout-btn" @click="logoutHandler">Log out</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<router-view />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.app-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: #2c3e50;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-section span {
|
||||||
|
color: #2c3e50;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-btn {
|
||||||
|
background-color: transparent;
|
||||||
|
border: 1px solid #dc3545;
|
||||||
|
color: #dc3545;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-btn:hover {
|
||||||
|
background-color: #dc3545;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useAcceptInvite, useAccount } from "community-jazz-vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import { TodoProject } from "./schema";
|
||||||
|
|
||||||
|
const { me, logOut } = useAccount();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
async function logoutHandler() {
|
||||||
|
await logOut();
|
||||||
|
// Redirect to home page to avoid permission issues with project paths
|
||||||
|
router.push("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle invite acceptance globally
|
||||||
|
useAcceptInvite({
|
||||||
|
invitedObjectSchema: TodoProject,
|
||||||
|
forValueHint: "project",
|
||||||
|
onAccept: (projectId: string) => {
|
||||||
|
router.push(`/project/${projectId}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
1
examples/community-todo-vue/src/apiKey.ts
Normal file
1
examples/community-todo-vue/src/apiKey.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const apiKey = "vue-todo-example-jazz@garden.co";
|
||||||
76
examples/community-todo-vue/src/assets/base.css
Normal file
76
examples/community-todo-vue/src/assets/base.css
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
/* color palette from <https://github.com/vuejs/theme> */
|
||||||
|
:root {
|
||||||
|
--vt-c-white: #ffffff;
|
||||||
|
--vt-c-white-soft: #f8f8f8;
|
||||||
|
--vt-c-white-mute: #f2f2f2;
|
||||||
|
|
||||||
|
--vt-c-black: #181818;
|
||||||
|
--vt-c-black-soft: #222222;
|
||||||
|
--vt-c-black-mute: #282828;
|
||||||
|
|
||||||
|
--vt-c-indigo: #2c3e50;
|
||||||
|
|
||||||
|
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
||||||
|
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
||||||
|
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
||||||
|
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
||||||
|
|
||||||
|
--vt-c-text-light-1: var(--vt-c-indigo);
|
||||||
|
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
||||||
|
--vt-c-text-dark-1: var(--vt-c-white);
|
||||||
|
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* semantic color variables for this project */
|
||||||
|
:root {
|
||||||
|
--color-background: var(--vt-c-white);
|
||||||
|
--color-background-soft: var(--vt-c-white-soft);
|
||||||
|
--color-background-mute: var(--vt-c-white-mute);
|
||||||
|
|
||||||
|
--color-border: var(--vt-c-divider-light-2);
|
||||||
|
--color-border-hover: var(--vt-c-divider-light-1);
|
||||||
|
|
||||||
|
--color-heading: var(--vt-c-text-light-1);
|
||||||
|
--color-text: var(--vt-c-text-light-1);
|
||||||
|
|
||||||
|
--section-gap: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--color-background: var(--vt-c-black);
|
||||||
|
--color-background-soft: var(--vt-c-black-soft);
|
||||||
|
--color-background-mute: var(--vt-c-black-mute);
|
||||||
|
|
||||||
|
--color-border: var(--vt-c-divider-dark-2);
|
||||||
|
--color-border-hover: var(--vt-c-divider-dark-1);
|
||||||
|
|
||||||
|
--color-heading: var(--vt-c-text-dark-1);
|
||||||
|
--color-text: var(--vt-c-text-dark-2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
color: var(--color-text);
|
||||||
|
background: var(--color-background);
|
||||||
|
transition:
|
||||||
|
color 0.5s,
|
||||||
|
background-color 0.5s;
|
||||||
|
line-height: 1.6;
|
||||||
|
font-family:
|
||||||
|
Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
|
||||||
|
Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
||||||
|
font-size: 15px;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
1
examples/community-todo-vue/src/assets/main.css
Normal file
1
examples/community-todo-vue/src/assets/main.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
@import "./base.css";
|
||||||
92
examples/community-todo-vue/src/components/InviteButton.vue
Normal file
92
examples/community-todo-vue/src/components/InviteButton.vue
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
<template>
|
||||||
|
<button
|
||||||
|
@click="handleInvite"
|
||||||
|
class="invite-button"
|
||||||
|
:disabled="!value"
|
||||||
|
title="Share this project"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="invite-icon"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.367 2.684 3 3 0 00-5.367-2.684z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
Share
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { createInviteLink } from "community-jazz-vue";
|
||||||
|
import type { CoValue } from "jazz-tools";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
value: CoValue | null | undefined;
|
||||||
|
valueHint: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const handleInvite = () => {
|
||||||
|
if (!props.value) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const inviteLink = createInviteLink(props.value, "writer", {
|
||||||
|
valueHint: props.valueHint,
|
||||||
|
});
|
||||||
|
|
||||||
|
navigator.clipboard
|
||||||
|
.writeText(inviteLink)
|
||||||
|
.then(() => {
|
||||||
|
alert(
|
||||||
|
`Invite link copied to clipboard!\n\nShare this link to give others access to this ${props.valueHint}.`,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// Fallback if clipboard API fails
|
||||||
|
prompt("Copy this invite link:", inviteLink);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to create invite link:", error);
|
||||||
|
alert("Failed to create invite link. Please try again.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.invite-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background: #3b82f6;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invite-button:hover:not(:disabled) {
|
||||||
|
background: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invite-button:disabled {
|
||||||
|
background: #9ca3af;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invite-icon {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
118
examples/community-todo-vue/src/components/NewProjectForm.vue
Normal file
118
examples/community-todo-vue/src/components/NewProjectForm.vue
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
<template>
|
||||||
|
<div class="new-project-form">
|
||||||
|
<form @submit.prevent="handleSubmit">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="project-title" class="form-label">Create New Project</label>
|
||||||
|
<input
|
||||||
|
id="project-title"
|
||||||
|
v-model="title"
|
||||||
|
type="text"
|
||||||
|
placeholder="New project title"
|
||||||
|
class="form-input"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="submit-button" :disabled="!title.trim()">
|
||||||
|
Create Project
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useAccount } from "community-jazz-vue";
|
||||||
|
import { Group, co } from "jazz-tools";
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import { Task, TodoAccount, TodoProject } from "../schema";
|
||||||
|
|
||||||
|
const { me } = useAccount(TodoAccount, {
|
||||||
|
resolve: { root: { projects: { $each: { $onError: null } } } },
|
||||||
|
});
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const title = ref("");
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
if (!me.value || !me.value.root?.projects || !title.value.trim()) return;
|
||||||
|
|
||||||
|
// To create a new todo project, we first create a `Group`,
|
||||||
|
// which is a scope for defining access rights (reader/writer/admin)
|
||||||
|
// of its members, which will apply to all CoValues owned by that group.
|
||||||
|
const projectGroup = Group.create({ owner: me.value });
|
||||||
|
|
||||||
|
// Then we create an empty todo project within that group
|
||||||
|
const project = TodoProject.create(
|
||||||
|
{
|
||||||
|
title: title.value,
|
||||||
|
tasks: co.list(Task).create([], { owner: projectGroup }),
|
||||||
|
},
|
||||||
|
{ owner: projectGroup },
|
||||||
|
);
|
||||||
|
|
||||||
|
me.value.root.projects.push(project);
|
||||||
|
|
||||||
|
router.push(`/project/${project.id}`);
|
||||||
|
title.value = "";
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.new-project-form {
|
||||||
|
max-width: 500px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
background: white;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #374151;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
background: #3b82f6;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-button:hover:not(:disabled) {
|
||||||
|
background: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-button:disabled {
|
||||||
|
background: #9ca3af;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
131
examples/community-todo-vue/src/components/NewTaskRow.vue
Normal file
131
examples/community-todo-vue/src/components/NewTaskRow.vue
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
<template>
|
||||||
|
<div class="new-task-row">
|
||||||
|
<div class="task-done">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
disabled
|
||||||
|
class="task-checkbox disabled"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="task-content">
|
||||||
|
<form @submit.prevent="handleSubmit" class="task-form">
|
||||||
|
<input
|
||||||
|
v-model="taskText"
|
||||||
|
type="text"
|
||||||
|
placeholder="New task"
|
||||||
|
class="task-input"
|
||||||
|
:disabled="disabled"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="add-button"
|
||||||
|
:disabled="disabled || !taskText.trim()"
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
createTask: (text: string) => void;
|
||||||
|
disabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const taskText = ref("");
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
if (!taskText.value.trim() || props.disabled) return;
|
||||||
|
|
||||||
|
props.createTask(taskText.value);
|
||||||
|
taskText.value = "";
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.new-task-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 60px 1fr;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
background: #f9fafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-done {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-checkbox {
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-checkbox.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-content {
|
||||||
|
padding: 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-form {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
width: 100%;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-input:disabled {
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #9ca3af;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-button {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background: #3b82f6;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-button:hover:not(:disabled) {
|
||||||
|
background: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-button:disabled {
|
||||||
|
background: #9ca3af;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
168
examples/community-todo-vue/src/components/TaskRow.vue
Normal file
168
examples/community-todo-vue/src/components/TaskRow.vue
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
<template>
|
||||||
|
<div class="task-row">
|
||||||
|
<div class="task-checkbox-container">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="task?.done"
|
||||||
|
@change="handleToggle"
|
||||||
|
class="task-checkbox"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="task-content">
|
||||||
|
<div class="task-text-container">
|
||||||
|
<span
|
||||||
|
v-if="task?.text"
|
||||||
|
:class="['task-text', { 'task-done': task?.done }]"
|
||||||
|
>
|
||||||
|
{{ task.text }}
|
||||||
|
</span>
|
||||||
|
<div v-else class="skeleton skeleton-text"></div>
|
||||||
|
|
||||||
|
<span
|
||||||
|
v-if="task?._edits?.text?.by?.profile?.name"
|
||||||
|
class="task-author"
|
||||||
|
:style="getAuthorStyle(task._edits.text.by?.id ?? '')"
|
||||||
|
>
|
||||||
|
{{ task._edits.text.by?.profile?.name }}
|
||||||
|
</span>
|
||||||
|
<div v-else class="skeleton skeleton-author"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Loaded } from "jazz-tools";
|
||||||
|
import type { Task } from "../schema";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
task: Loaded<typeof Task> | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const handleToggle = (event: Event) => {
|
||||||
|
const target = event.target as HTMLInputElement;
|
||||||
|
if (props.task) {
|
||||||
|
props.task.done = target.checked;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate unique colors for authors
|
||||||
|
const getAuthorStyle = (authorId: string) => {
|
||||||
|
// Simple hash function to generate consistent colors
|
||||||
|
let hash = 0;
|
||||||
|
for (let i = 0; i < authorId.length; i++) {
|
||||||
|
const char = authorId.charCodeAt(i);
|
||||||
|
hash = (hash << 5) - hash + char;
|
||||||
|
hash = hash & hash; // Convert to 32-bit integer
|
||||||
|
}
|
||||||
|
|
||||||
|
const hue = Math.abs(hash) % 360;
|
||||||
|
const isDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||||
|
|
||||||
|
return {
|
||||||
|
color: `hsl(${hue}, 70%, ${isDark ? "80%" : "20%"})`,
|
||||||
|
backgroundColor: `hsl(${hue}, 70%, ${isDark ? "20%" : "80%"})`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.task-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
min-height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-row:hover {
|
||||||
|
background: #f9fafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-done {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: left;
|
||||||
|
width: 60px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-checkbox-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: left;
|
||||||
|
padding-left: 1rem;
|
||||||
|
width: 60px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-checkbox {
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-content {
|
||||||
|
padding: 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-text-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-text {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #374151;
|
||||||
|
flex: 1;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-text.task-done {
|
||||||
|
text-decoration: line-through;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-author {
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton {
|
||||||
|
background: #e5e7eb;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-text {
|
||||||
|
height: 1rem;
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-author {
|
||||||
|
height: 1rem;
|
||||||
|
width: 50px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
48
examples/community-todo-vue/src/main.ts
Normal file
48
examples/community-todo-vue/src/main.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { JazzVueProvider, PasskeyAuthBasicUI } from "community-jazz-vue";
|
||||||
|
import { createApp, defineComponent, h, markRaw } from "vue";
|
||||||
|
import App from "./App.vue";
|
||||||
|
import "./assets/main.css";
|
||||||
|
import { apiKey } from "./apiKey";
|
||||||
|
import router from "./router";
|
||||||
|
import { TodoAccount } from "./schema";
|
||||||
|
|
||||||
|
import "jazz-tools/inspector/register-custom-element";
|
||||||
|
|
||||||
|
const RootComponent = defineComponent({
|
||||||
|
name: "RootComponent",
|
||||||
|
setup() {
|
||||||
|
return () =>
|
||||||
|
h(
|
||||||
|
JazzVueProvider,
|
||||||
|
{
|
||||||
|
AccountSchema: TodoAccount,
|
||||||
|
sync: {
|
||||||
|
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
() => [
|
||||||
|
h(
|
||||||
|
PasskeyAuthBasicUI,
|
||||||
|
{
|
||||||
|
appName: "Jazz Vue Todo",
|
||||||
|
},
|
||||||
|
() => h(App),
|
||||||
|
),
|
||||||
|
h("jazz-inspector", {
|
||||||
|
style: {
|
||||||
|
position: "fixed",
|
||||||
|
bottom: "20px",
|
||||||
|
right: "20px",
|
||||||
|
zIndex: 9999,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const app = createApp(RootComponent);
|
||||||
|
|
||||||
|
app.use(router);
|
||||||
|
|
||||||
|
app.mount("#app");
|
||||||
26
examples/community-todo-vue/src/router/index.ts
Normal file
26
examples/community-todo-vue/src/router/index.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { createRouter, createWebHistory } from "vue-router";
|
||||||
|
import Home from "../views/Home.vue";
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(),
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
name: "Home",
|
||||||
|
component: Home,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/project/:projectId",
|
||||||
|
name: "Project",
|
||||||
|
component: () => import("../views/ProjectView.vue"),
|
||||||
|
props: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/invite/:path*",
|
||||||
|
name: "AcceptInvite",
|
||||||
|
component: () => import("../views/AcceptInviteView.vue"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user