Compare commits
151 Commits
jazz-react
...
upgrade-sh
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b88ae25f1 | ||
|
|
f21089bd59 | ||
|
|
4760b84828 | ||
|
|
e021068051 | ||
|
|
16311977e9 | ||
|
|
ab2970413e | ||
|
|
34ff647221 | ||
|
|
ee01e2f059 | ||
|
|
525f24ffc1 | ||
|
|
82bb7733ff | ||
|
|
e307ecc524 | ||
|
|
f0f43a8b85 | ||
|
|
77d2909999 | ||
|
|
8c951fb08b | ||
|
|
d5f43c5225 | ||
|
|
4fe3d25adc | ||
|
|
bd84d87e94 | ||
|
|
5e9b33fded | ||
|
|
e2cbe62887 | ||
|
|
39734cb19f | ||
|
|
a1b1bab466 | ||
|
|
9383a0dd47 | ||
|
|
1e7ca2d008 | ||
|
|
65d1e94efc | ||
|
|
fcd8e21bfb | ||
|
|
e0d171cf79 | ||
|
|
aa45457f44 | ||
|
|
7189a1ea17 | ||
|
|
0ddf1c871e | ||
|
|
abc74dd567 | ||
|
|
69689289a1 | ||
|
|
2e6ea42d21 | ||
|
|
f1b8f4b5c0 | ||
|
|
89f39406ba | ||
|
|
8cc79473db | ||
|
|
690d9e2587 | ||
|
|
04d1c18378 | ||
|
|
ac8081ff01 | ||
|
|
7339782684 | ||
|
|
b8d64ba00a | ||
|
|
71b2dc21a9 | ||
|
|
3f822c3cac | ||
|
|
789cdab504 | ||
|
|
22c3b2b92b | ||
|
|
cd14f30129 | ||
|
|
2423b3ff46 | ||
|
|
91ada6fd2d | ||
|
|
2052c98094 | ||
|
|
daa8ddf890 | ||
|
|
760c90ccc0 | ||
|
|
5003067f47 | ||
|
|
b36ef59aea | ||
|
|
2fe509774b | ||
|
|
7e254cf9f5 | ||
|
|
dd25b5f926 | ||
|
|
1bb51398aa | ||
|
|
5deec8ddfd | ||
|
|
45ec2fabe3 | ||
|
|
c7f9d9d263 | ||
|
|
3285b85a90 | ||
|
|
64a88b7f54 | ||
|
|
dfc4b21fc8 | ||
|
|
fdc86bcb9a | ||
|
|
dca7edbf47 | ||
|
|
56d8d87195 | ||
|
|
b7731c8fca | ||
|
|
3e8afdafdc | ||
|
|
b39dc35fae | ||
|
|
31d8831faa | ||
|
|
673e194034 | ||
|
|
b579b32c69 | ||
|
|
c0e53dc8b8 | ||
|
|
99ad5008eb | ||
|
|
2c86de4176 | ||
|
|
ba3a0d4032 | ||
|
|
8f8d02e43e | ||
|
|
2118c46efe | ||
|
|
0073ea4950 | ||
|
|
744fcf53b2 | ||
|
|
37103fdff9 | ||
|
|
7345bc6ca4 | ||
|
|
042b407f2d | ||
|
|
ddcf91a014 | ||
|
|
366e1b1cc1 | ||
|
|
d8edad8041 | ||
|
|
20d823d6fd | ||
|
|
efb08caeae | ||
|
|
1a0e37eaed | ||
|
|
86bc03ac4a | ||
|
|
748c7160ba | ||
|
|
c77c617aaa | ||
|
|
9fb4961215 | ||
|
|
513ebd8fb9 | ||
|
|
6b22fe9a93 | ||
|
|
3076a90a51 | ||
|
|
7e6316b930 | ||
|
|
f7cdfd5767 | ||
|
|
ca6bb1c006 | ||
|
|
ba9f7eb487 | ||
|
|
3523ebf2bf | ||
|
|
9131894f40 | ||
|
|
618be5d016 | ||
|
|
8a18f30ba5 | ||
|
|
3128dda432 | ||
|
|
517662635e | ||
|
|
c119a613a7 | ||
|
|
ddd4bc63c3 | ||
|
|
e802cabd0f | ||
|
|
dfabf2d295 | ||
|
|
834607f8bc | ||
|
|
bda61d8440 | ||
|
|
3d882f0442 | ||
|
|
f61d568c9d | ||
|
|
6dd02d289c | ||
|
|
33a4944ba3 | ||
|
|
e367b6056d | ||
|
|
f3f56b9be0 | ||
|
|
4cae6bad34 | ||
|
|
17f2ef57de | ||
|
|
3a4d111a37 | ||
|
|
1e18c7f5fc | ||
|
|
8c7a6b27ed | ||
|
|
91f96e1188 | ||
|
|
28dac10723 | ||
|
|
9cb11e38dd | ||
|
|
f3e4bacb33 | ||
|
|
626d43f07b | ||
|
|
1f5d073035 | ||
|
|
a3b607e799 | ||
|
|
8fb93502af | ||
|
|
36774122e0 | ||
|
|
a6923128c1 | ||
|
|
706ca62feb | ||
|
|
01523dcca3 | ||
|
|
77f039b561 | ||
|
|
d661ba77be | ||
|
|
f8fbc59b6f | ||
|
|
cce0d22007 | ||
|
|
e3ff76e9cb | ||
|
|
4cbf71bff7 | ||
|
|
ceb060243a | ||
|
|
a70bebb96a | ||
|
|
b3b2507c35 | ||
|
|
6a8fa16b49 | ||
|
|
1f08807701 | ||
|
|
ba4a7f6170 | ||
|
|
a2854e3602 | ||
|
|
4ea87dc494 | ||
|
|
d8c87c5314 | ||
|
|
46f624a12e | ||
|
|
86ce770f38 |
18
.changeset/big-pens-pull.md
Normal file
18
.changeset/big-pens-pull.md
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
"jazz-tailwind-demo-auth-starter": patch
|
||||
"file-share-svelte": patch
|
||||
"jazz-password-manager": patch
|
||||
"version-history": patch
|
||||
"passkey-svelte": patch
|
||||
"chat-rn-clerk": patch
|
||||
"jazz-example-music-player": patch
|
||||
"passphrase": patch
|
||||
"multiauth": patch
|
||||
"reactions": patch
|
||||
"passkey": patch
|
||||
"clerk": patch
|
||||
"jazz-example-pets": patch
|
||||
"jazz-example-todo": patch
|
||||
---
|
||||
|
||||
Removed when="singedUp" from examples apps' Jazz providers. This is a really niche use-case option and can lead to broken-feeling experiences when anonymous users try to load something.
|
||||
6
.changeset/breezy-boxes-unite.md
Normal file
6
.changeset/breezy-boxes-unite.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"jazz-tools": minor
|
||||
"cojson": minor
|
||||
---
|
||||
|
||||
Check CoValue access permissions when loading
|
||||
5
.changeset/dull-tools-run.md
Normal file
5
.changeset/dull-tools-run.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"create-jazz-app": patch
|
||||
---
|
||||
|
||||
add directory param to create-jazz-app
|
||||
6
.changeset/eleven-moles-grow.md
Normal file
6
.changeset/eleven-moles-grow.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"jazz-inspector": patch
|
||||
"jazz-inspector-app": patch
|
||||
---
|
||||
|
||||
UI and JSON display improvements
|
||||
5
.changeset/fast-beans-decide.md
Normal file
5
.changeset/fast-beans-decide.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"jazz-tools": minor
|
||||
---
|
||||
|
||||
Implement new API for deep loading
|
||||
5
.changeset/funny-birds-flash.md
Normal file
5
.changeset/funny-birds-flash.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"cojson": minor
|
||||
---
|
||||
|
||||
Return the EVERYONE role if the account is not direct a member of the group
|
||||
@@ -28,7 +28,7 @@ export default function Conversation() {
|
||||
const { me } = useAccount();
|
||||
const [chat, setChat] = useState<Chat>();
|
||||
const [message, setMessage] = useState("");
|
||||
const loadedChat = useCoState(Chat, chat?.id, [{}]);
|
||||
const loadedChat = useCoState(Chat, chat?.id, { resolve: { $each: true } });
|
||||
const navigation = useNavigation();
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
|
||||
@@ -71,7 +71,7 @@ export default function Conversation() {
|
||||
|
||||
const loadChat = async (chatId: ID<Chat>) => {
|
||||
try {
|
||||
const chat = await Chat.load(chatId, me, []);
|
||||
const chat = await Chat.load(chatId, me);
|
||||
setChat(chat);
|
||||
} catch (error) {
|
||||
console.log("Error loading chat", error);
|
||||
|
||||
@@ -12,7 +12,6 @@ export function JazzAndAuth({ children }: PropsWithChildren) {
|
||||
storage="sqlite"
|
||||
sync={{
|
||||
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
|
||||
when: "signedUp", // This makes the app work in local mode when the user is not authenticated
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -20,7 +20,7 @@ import { Chat, Message } from "./schema";
|
||||
export default function ChatScreen({ navigation }: { navigation: any }) {
|
||||
const { me, logOut } = useAccount();
|
||||
const [chatId, setChatId] = useState<ID<Chat>>();
|
||||
const loadedChat = useCoState(Chat, chatId, [{}]);
|
||||
const loadedChat = useCoState(Chat, chatId, { resolve: { $each: true } });
|
||||
const [message, setMessage] = useState("");
|
||||
const profile = useCoState(Profile, me._refs.profile?.id, {});
|
||||
|
||||
|
||||
@@ -11,12 +11,12 @@ You can either
|
||||
|
||||
Create a new Jazz project, and use this example as a template.
|
||||
```bash
|
||||
npx create-jazz-app@latest --example chat-vue --project-name chat-vue
|
||||
npx create-jazz-app@latest chat-vue-app --example chat-vue
|
||||
```
|
||||
|
||||
Go to the new project directory.
|
||||
```bash
|
||||
cd chat-vue
|
||||
cd chat-vue-app
|
||||
```
|
||||
|
||||
Run the dev server.
|
||||
|
||||
@@ -49,7 +49,7 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const chat = useCoState(Chat, props.chatId, [{}]);
|
||||
const chat = useCoState(Chat, props.chatId, { resolve: { $each: true } });
|
||||
const showNLastMessages = ref(30);
|
||||
|
||||
const displayedMessages = computed(() => {
|
||||
|
||||
@@ -13,12 +13,12 @@ You can either
|
||||
|
||||
Create a new Jazz project, and use this example as a template.
|
||||
```bash
|
||||
npx create-jazz-app@latest --example chat --project-name chat
|
||||
npx create-jazz-app@latest chat-app --example chat
|
||||
```
|
||||
|
||||
Go to the new project directory.
|
||||
```bash
|
||||
cd chat
|
||||
cd chat-app
|
||||
```
|
||||
|
||||
Run the dev server.
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"clsx": "^2.0.0",
|
||||
"hash-slash": "workspace:*",
|
||||
"jazz-browser-media-images": "workspace:*",
|
||||
"jazz-inspector": "workspace:*",
|
||||
"jazz-react": "workspace:*",
|
||||
"jazz-tools": "workspace:*",
|
||||
"lucide-react": "^0.274.0",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { apiKey } from "@/apiKey.ts";
|
||||
import { getRandomUsername, inIframe, onChatLoad } from "@/util.ts";
|
||||
import { useIframeHashRouter } from "hash-slash";
|
||||
import { JazzInspector } from "jazz-inspector";
|
||||
import { JazzProvider, useAccount } from "jazz-react";
|
||||
import { Group, ID } from "jazz-tools";
|
||||
import { StrictMode } from "react";
|
||||
@@ -61,6 +62,7 @@ createRoot(document.getElementById("root")!).render(
|
||||
defaultProfileName={defaultProfileName}
|
||||
>
|
||||
<App />
|
||||
<JazzInspector />
|
||||
</JazzProvider>
|
||||
</StrictMode>
|
||||
</ThemeProvider>,
|
||||
|
||||
@@ -17,8 +17,8 @@ import {
|
||||
} from "./ui.tsx";
|
||||
|
||||
export function ChatScreen(props: { chatID: ID<Chat> }) {
|
||||
const chat = useCoState(Chat, props.chatID, { resolve: { $each: true } });
|
||||
const account = useAccount();
|
||||
const chat = useCoState(Chat, props.chatID, [{}]);
|
||||
const [showNLastMessages, setShowNLastMessages] = useState(30);
|
||||
|
||||
if (!chat)
|
||||
|
||||
@@ -15,12 +15,12 @@ You can either
|
||||
|
||||
Create a new Jazz project, and use this example as a template.
|
||||
```bash
|
||||
npx create-jazz-app@latest --example clerk --project-name clerk
|
||||
npx create-jazz-app@latest clerk-app --example clerk
|
||||
```
|
||||
|
||||
Go to the new project directory.
|
||||
```bash
|
||||
cd clerk
|
||||
cd clerk-app
|
||||
```
|
||||
|
||||
Run the dev server.
|
||||
|
||||
@@ -21,7 +21,6 @@ function JazzProvider({ children }: { children: React.ReactNode }) {
|
||||
clerk={clerk}
|
||||
sync={{
|
||||
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
|
||||
when: "signedUp", // This makes the app work in local mode when the user is not authenticated
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
AccountSchema={FileShareAccount}
|
||||
sync={{
|
||||
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
|
||||
when: "signedUp",
|
||||
}}
|
||||
>
|
||||
<PasskeyAuthBasicUI appName="File Share">
|
||||
|
||||
@@ -28,12 +28,12 @@ You can either
|
||||
|
||||
Create a new Jazz project, and use this example as a template.
|
||||
```bash
|
||||
npx create-jazz-app@latest --example form --project-name form
|
||||
npx create-jazz-app@latest form-app --example form
|
||||
```
|
||||
|
||||
Go to the new project directory.
|
||||
```bash
|
||||
cd form
|
||||
cd form-app
|
||||
```
|
||||
|
||||
Run the dev server.
|
||||
|
||||
@@ -12,7 +12,9 @@ import {
|
||||
} from "./schema.ts";
|
||||
|
||||
export function CreateOrder() {
|
||||
const { me } = useAccount({ root: { draft: {}, orders: [] } });
|
||||
const { me } = useAccount({
|
||||
resolve: { root: { draft: true, orders: true } },
|
||||
});
|
||||
const router = useIframeHashRouter();
|
||||
const [errors, setErrors] = useState<string[]>([]);
|
||||
|
||||
@@ -60,7 +62,7 @@ function CreateOrderForm({
|
||||
onSave: (draft: DraftBubbleTeaOrder) => void;
|
||||
}) {
|
||||
const draft = useCoState(DraftBubbleTeaOrder, id, {
|
||||
addOns: [],
|
||||
resolve: { addOns: true },
|
||||
});
|
||||
|
||||
if (!draft) return;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useAccount } from "jazz-react";
|
||||
|
||||
export function DraftIndicator() {
|
||||
const { me } = useAccount({
|
||||
root: { draft: {} },
|
||||
resolve: { root: { draft: true } },
|
||||
});
|
||||
|
||||
if (me?.root.draft?.hasChanges) {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { OrderThumbnail } from "./OrderThumbnail.tsx";
|
||||
import { BubbleTeaOrder } from "./schema.ts";
|
||||
|
||||
export function EditOrder(props: { id: ID<BubbleTeaOrder> }) {
|
||||
const order = useCoState(BubbleTeaOrder, props.id, []);
|
||||
const order = useCoState(BubbleTeaOrder, props.id);
|
||||
|
||||
if (!order) return;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { OrderThumbnail } from "./OrderThumbnail.tsx";
|
||||
|
||||
export function Orders() {
|
||||
const { me } = useAccount({
|
||||
root: { orders: [] },
|
||||
resolve: { root: { orders: true } },
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -15,12 +15,12 @@ You can either
|
||||
|
||||
Create a new Jazz project, and use this example as a template.
|
||||
```bash
|
||||
npx create-jazz-app@latest --example image-upload --project-name image-upload
|
||||
npx create-jazz-app@latest image-upload-app --example image-upload
|
||||
```
|
||||
|
||||
Go to the new project directory.
|
||||
```bash
|
||||
cd image-upload
|
||||
cd image-upload-app
|
||||
```
|
||||
|
||||
Run the dev server.
|
||||
|
||||
@@ -11,27 +11,19 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@radix-ui/react-toast": "^1.1.4",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"jazz-inspector": "workspace:*",
|
||||
"clsx": "^2.0.0",
|
||||
"cojson": "workspace:0.11.5",
|
||||
"cojson-transport-ws": "workspace:0.11.5",
|
||||
"hash-slash": "workspace:0.2.2",
|
||||
"lucide-react": "^0.274.0",
|
||||
"qrcode": "^1.5.3",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router": "^6.16.0",
|
||||
"react-router-dom": "^6.16.0",
|
||||
"react-use": "^17.4.0",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"uniqolor": "^1.1.0"
|
||||
"react-use": "^17.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/qrcode": "^1.5.1",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||
|
||||
@@ -1,92 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 20 14.3% 4.1%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 20 14.3% 4.1%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 20 14.3% 4.1%;
|
||||
|
||||
--primary: 24 9.8% 10%;
|
||||
--primary-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--secondary: 60 4.8% 95.9%;
|
||||
--secondary-foreground: 24 9.8% 10%;
|
||||
|
||||
--muted: 60 4.8% 95.9%;
|
||||
--muted-foreground: 25 5.3% 44.7%;
|
||||
|
||||
--accent: 60 4.8% 95.9%;
|
||||
--accent-foreground: 24 9.8% 10%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--border: 20 5.9% 90%;
|
||||
--input: 20 5.9% 90%;
|
||||
--ring: 20 14.3% 4.1%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 20 14.3% 4.1%;
|
||||
--foreground: 60 9.1% 97.8%;
|
||||
|
||||
--card: 20 14.3% 4.1%;
|
||||
--card-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--popover: 20 14.3% 4.1%;
|
||||
--popover-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--primary: 60 9.1% 97.8%;
|
||||
--primary-foreground: 24 9.8% 10%;
|
||||
|
||||
--secondary: 12 6.5% 15.1%;
|
||||
--secondary-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--muted: 12 6.5% 15.1%;
|
||||
--muted-foreground: 24 5.4% 63.9%;
|
||||
|
||||
--accent: 12 6.5% 15.1%;
|
||||
--accent-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--border: 12 6.5% 15.1%;
|
||||
--input: 12 6.5% 15.1%;
|
||||
--ring: 24 5.7% 82.9%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-in {
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateZ(400px) translateY(30px) scale(1.05);
|
||||
opacity: 0.4;
|
||||
}
|
||||
to {
|
||||
transform: translateZ(0) scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
export function LinkIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="w-3 h-3"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import React from "react";
|
||||
import { PageInfo } from "./types";
|
||||
|
||||
interface BreadcrumbsProps {
|
||||
path: PageInfo[];
|
||||
onBreadcrumbClick: (index: number) => void;
|
||||
}
|
||||
|
||||
export const Breadcrumbs: React.FC<BreadcrumbsProps> = ({
|
||||
path,
|
||||
onBreadcrumbClick,
|
||||
}) => {
|
||||
return (
|
||||
<div className="z-20 relative bg-indigo-400/10 backdrop-blur-sm rounded-lg inline-flex px-2 py-1 whitespace-pre transition-all items-center space-x-1 min-h-10">
|
||||
<button
|
||||
onClick={() => onBreadcrumbClick(-1)}
|
||||
className="flex items-center justify-center p-1 rounded-sm hover:bg-indigo-500/10 transition-colors"
|
||||
aria-label="Go to home"
|
||||
>
|
||||
<img src="jazz-logo.png" alt="Jazz Logo" className="size-5" />
|
||||
</button>
|
||||
{path.map((page, index) => {
|
||||
return (
|
||||
<span key={index} className="inline-block first:pl-1 last:pr-1">
|
||||
{index === 0 ? null : (
|
||||
<span className="text-indigo-500/30">{" / "}</span>
|
||||
)}
|
||||
<button
|
||||
onClick={() => onBreadcrumbClick(index)}
|
||||
className="text-indigo-700 hover:underline"
|
||||
>
|
||||
{index === 0 ? page.name || "Root" : page.name}
|
||||
</button>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,344 +0,0 @@
|
||||
import {
|
||||
CoID,
|
||||
LocalNode,
|
||||
RawBinaryCoStream,
|
||||
RawCoStream,
|
||||
RawCoValue,
|
||||
} from "cojson";
|
||||
import { base64URLtoBytes } from "cojson";
|
||||
import { BinaryStreamItem, BinaryStreamStart, CoStreamItem } from "cojson";
|
||||
import { JsonObject, JsonValue } from "cojson";
|
||||
import { ArrowDownToLine } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { PageInfo } from "./types";
|
||||
import { AccountOrGroupPreview } from "./value-renderer";
|
||||
|
||||
// typeguard for BinaryStreamStart
|
||||
function isBinaryStreamStart(item: unknown): item is BinaryStreamStart {
|
||||
return (
|
||||
typeof item === "object" &&
|
||||
item !== null &&
|
||||
"type" in item &&
|
||||
item.type === "start"
|
||||
);
|
||||
}
|
||||
|
||||
function detectCoStreamType(value: RawCoStream | RawBinaryCoStream) {
|
||||
const firstKey = Object.keys(value.items)[0];
|
||||
if (!firstKey)
|
||||
return {
|
||||
type: "unknown",
|
||||
};
|
||||
|
||||
const items = value.items[firstKey as never]?.map((v) => v.value);
|
||||
|
||||
if (!items)
|
||||
return {
|
||||
type: "unknown",
|
||||
};
|
||||
const firstItem = items[0];
|
||||
if (!firstItem)
|
||||
return {
|
||||
type: "unknown",
|
||||
};
|
||||
// This is a binary stream
|
||||
if (isBinaryStreamStart(firstItem)) {
|
||||
return {
|
||||
type: "binary",
|
||||
items: items as BinaryStreamItem[],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: "coStream",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function getBlobFromCoStream({
|
||||
items,
|
||||
onlyFirstChunk = false,
|
||||
}: {
|
||||
items: BinaryStreamItem[];
|
||||
onlyFirstChunk?: boolean;
|
||||
}) {
|
||||
if (onlyFirstChunk && items.length > 1) {
|
||||
items = items.slice(0, 2);
|
||||
}
|
||||
|
||||
const chunks: Uint8Array[] = [];
|
||||
|
||||
const binary_U_prefixLength = 8;
|
||||
|
||||
let lastProgressUpdate = Date.now();
|
||||
|
||||
for (const item of items.slice(1)) {
|
||||
if (item.type === "end") {
|
||||
break;
|
||||
}
|
||||
|
||||
if (item.type !== "chunk") {
|
||||
console.error("Invalid binary stream chunk", item);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const chunk = base64URLtoBytes(item.chunk.slice(binary_U_prefixLength));
|
||||
// totalLength += chunk.length;
|
||||
chunks.push(chunk);
|
||||
|
||||
if (Date.now() - lastProgressUpdate > 100) {
|
||||
lastProgressUpdate = Date.now();
|
||||
}
|
||||
}
|
||||
const defaultMime = "mimeType" in items[0] ? items[0].mimeType : null;
|
||||
|
||||
const blob = new Blob(chunks, defaultMime ? { type: defaultMime } : {});
|
||||
|
||||
const mimeType =
|
||||
defaultMime === "" ? await detectPDFMimeType(blob) : defaultMime;
|
||||
|
||||
return {
|
||||
blob,
|
||||
mimeType: mimeType as string,
|
||||
unfinishedChunks: items.length > 1,
|
||||
totalSize:
|
||||
"totalSizeBytes" in items[0]
|
||||
? (items[0].totalSizeBytes as number)
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const detectPDFMimeType = async (blob: Blob): Promise<string> => {
|
||||
const arrayBuffer = await blob.slice(0, 4).arrayBuffer();
|
||||
const uint8Array = new Uint8Array(arrayBuffer);
|
||||
const header = uint8Array.reduce(
|
||||
(acc, byte) => acc + String.fromCharCode(byte),
|
||||
"",
|
||||
);
|
||||
|
||||
if (header === "%PDF") {
|
||||
return "application/pdf";
|
||||
}
|
||||
return "application/octet-stream";
|
||||
};
|
||||
|
||||
const BinaryDownloadButton = ({
|
||||
pdfBlob,
|
||||
fileName = "document",
|
||||
label,
|
||||
mimeType,
|
||||
}: {
|
||||
pdfBlob: Blob;
|
||||
mimeType?: string;
|
||||
fileName?: string;
|
||||
label: string;
|
||||
}) => {
|
||||
const downloadFile = () => {
|
||||
const url = URL.createObjectURL(
|
||||
new Blob([pdfBlob], mimeType ? { type: mimeType } : {}),
|
||||
);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download =
|
||||
mimeType === "application/pdf" ? `${fileName}.pdf` : fileName;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
className="flex items-center gap-2 px-2 py-1 text-gray-900 border border-gray-900/10 bg-clip-border shadow-sm transition-colors rounded bg-gray-50 text-sm"
|
||||
onClick={downloadFile}
|
||||
>
|
||||
<ArrowDownToLine size={16} />
|
||||
{label}
|
||||
{/* Download {mimeType === "application/pdf" ? "PDF" : "File"} */}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const LabelContentPair = ({
|
||||
label,
|
||||
content,
|
||||
}: {
|
||||
label: string;
|
||||
content: React.ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5 ">
|
||||
<span className="uppercase text-xs font-medium text-gray-600 tracking-wide">
|
||||
{label}
|
||||
</span>
|
||||
<span>{content}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function RenderCoBinaryStream({
|
||||
value,
|
||||
items,
|
||||
}: {
|
||||
items: BinaryStreamItem[];
|
||||
value: RawBinaryCoStream;
|
||||
}) {
|
||||
const [file, setFile] = useState<
|
||||
| {
|
||||
blob: Blob;
|
||||
mimeType: string;
|
||||
unfinishedChunks: boolean;
|
||||
totalSize: number | undefined;
|
||||
}
|
||||
| undefined
|
||||
| null
|
||||
>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// load only the first chunk to get the mime type and size
|
||||
getBlobFromCoStream({
|
||||
items,
|
||||
onlyFirstChunk: true,
|
||||
})
|
||||
.then((v) => {
|
||||
if (v) {
|
||||
setFile(v);
|
||||
if (v.mimeType.includes("image")) {
|
||||
// If it's an image, load the full blob
|
||||
getBlobFromCoStream({
|
||||
items,
|
||||
}).then((s) => {
|
||||
if (s) setFile(s);
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [items]);
|
||||
|
||||
if (!isLoading && !file) return <div>No blob</div>;
|
||||
|
||||
if (isLoading) return <div>Loading...</div>;
|
||||
if (!file) return <div>No blob</div>;
|
||||
|
||||
const { blob, mimeType } = file;
|
||||
|
||||
const sizeInKB = (file.totalSize || 0) / 1024;
|
||||
|
||||
return (
|
||||
<div className="space-y-8 mt-4">
|
||||
<div className="grid grid-cols-3 gap-2 max-w-3xl">
|
||||
<LabelContentPair
|
||||
label="Mime Type"
|
||||
content={
|
||||
<span className="font-mono bg-gray-100 rounded px-2 py-1 text-sm">
|
||||
{mimeType || "No mime type"}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<LabelContentPair
|
||||
label="Size"
|
||||
content={<span>{sizeInKB.toFixed(2)} KB</span>}
|
||||
/>
|
||||
<LabelContentPair
|
||||
label="Download"
|
||||
content={
|
||||
<BinaryDownloadButton
|
||||
fileName={value.id.toString()}
|
||||
pdfBlob={blob}
|
||||
mimeType={mimeType}
|
||||
label={
|
||||
mimeType === "application/pdf"
|
||||
? "Download PDF"
|
||||
: "Download File"
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{mimeType === "image/png" || mimeType === "image/jpeg" ? (
|
||||
<LabelContentPair
|
||||
label="Preview"
|
||||
content={
|
||||
<div className="bg-gray-50 p-3 rounded-sm">
|
||||
<RenderBlobImage blob={blob} />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RenderCoStream({
|
||||
value,
|
||||
node,
|
||||
}: {
|
||||
value: RawCoStream;
|
||||
node: LocalNode;
|
||||
}) {
|
||||
const streamPerUser = Object.keys(value.items);
|
||||
const userCoIds = streamPerUser.map((stream) => stream.split("_session")[0]);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{userCoIds.map((id, idx) => (
|
||||
<div
|
||||
className="bg-gray-100 p-3 rounded-lg transition-colors overflow-hidden bg-white border hover:bg-gray-100/5 cursor-pointer shadow-sm"
|
||||
key={id}
|
||||
>
|
||||
<AccountOrGroupPreview coId={id as CoID<RawCoValue>} node={node} />
|
||||
{/* @ts-expect-error - TODO: fix types */}
|
||||
{value.items[streamPerUser[idx]]?.map(
|
||||
(item: CoStreamItem<JsonValue>) => (
|
||||
<div>
|
||||
{new Date(item.madeAt).toLocaleString()}{" "}
|
||||
{JSON.stringify(item.value)}
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function CoStreamView({
|
||||
value,
|
||||
node,
|
||||
}: {
|
||||
data: JsonObject;
|
||||
onNavigate: (pages: PageInfo[]) => void;
|
||||
node: LocalNode;
|
||||
value: RawCoStream;
|
||||
}) {
|
||||
// if (!value) return <div>No value</div>;
|
||||
|
||||
const streamType = detectCoStreamType(value);
|
||||
|
||||
if (streamType.type === "binary") {
|
||||
if (streamType.items === undefined) {
|
||||
return <div>No binary stream</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<RenderCoBinaryStream
|
||||
value={value as RawBinaryCoStream}
|
||||
items={streamType.items}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (streamType.type === "coStream") {
|
||||
return <RenderCoStream value={value} node={node} />;
|
||||
}
|
||||
|
||||
if (streamType.type === "unknown") return <div>Unknown stream type</div>;
|
||||
|
||||
return <div>Unknown stream type</div>;
|
||||
}
|
||||
|
||||
function RenderBlobImage({ blob }: { blob: Blob }) {
|
||||
const urlCreator = window.URL || window.webkitURL;
|
||||
return <img src={urlCreator.createObjectURL(blob)} />;
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import clsx from "clsx";
|
||||
import { CoID, LocalNode, RawCoValue } from "cojson";
|
||||
import { JsonObject } from "cojson";
|
||||
import { ResolveIcon } from "./type-icon";
|
||||
import { PageInfo, isCoId } from "./types";
|
||||
import { CoMapPreview, ValueRenderer } from "./value-renderer";
|
||||
|
||||
export function GridView({
|
||||
data,
|
||||
onNavigate,
|
||||
node,
|
||||
}: {
|
||||
data: JsonObject;
|
||||
onNavigate: (pages: PageInfo[]) => void;
|
||||
node: LocalNode;
|
||||
}) {
|
||||
const entries = Object.entries(data);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 p-2">
|
||||
{entries.map(([key, child], childIndex) => (
|
||||
<div
|
||||
key={childIndex}
|
||||
className={clsx(
|
||||
"bg-gray-100 p-3 rounded-lg transition-colors overflow-hidden",
|
||||
isCoId(child)
|
||||
? "bg-white border hover:bg-gray-100/5 cursor-pointer shadow-sm"
|
||||
: "bg-gray-50",
|
||||
)}
|
||||
onClick={() =>
|
||||
isCoId(child) &&
|
||||
onNavigate([{ coId: child as CoID<RawCoValue>, name: key }])
|
||||
}
|
||||
>
|
||||
<h3 className="truncate">
|
||||
{isCoId(child) ? (
|
||||
<span className="font-medium flex justify-between">
|
||||
{key}
|
||||
|
||||
<div className="px-2 py-1 text-xs bg-gray-100 rounded">
|
||||
<ResolveIcon coId={child as CoID<RawCoValue>} node={node} />
|
||||
</div>
|
||||
</span>
|
||||
) : (
|
||||
<span>{key}</span>
|
||||
)}
|
||||
</h3>
|
||||
<div className="mt-2 text-sm">
|
||||
{isCoId(child) ? (
|
||||
<CoMapPreview coId={child as CoID<RawCoValue>} node={node} />
|
||||
) : (
|
||||
<ValueRenderer
|
||||
json={child}
|
||||
onCoIDClick={(coId) => {
|
||||
onNavigate([{ coId, name: key }]);
|
||||
}}
|
||||
compact
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { LocalNode } from "cojson";
|
||||
import { Breadcrumbs } from "./breadcrumbs";
|
||||
import { PageStack } from "./page-stack";
|
||||
import { PageInfo } from "./types";
|
||||
import { Breadcrumbs, PageStack } from "jazz-inspector";
|
||||
import type { PageInfo } from "jazz-inspector";
|
||||
import { usePagePath } from "./use-page-path";
|
||||
|
||||
export default function CoJsonViewer({
|
||||
|
||||
@@ -9,10 +9,15 @@ import {
|
||||
} from "cojson";
|
||||
import { createWebSocketPeer } from "cojson-transport-ws";
|
||||
import { WasmCrypto } from "cojson/crypto/WasmCrypto";
|
||||
import { Trash2 } from "lucide-react";
|
||||
import {
|
||||
Breadcrumbs,
|
||||
Button,
|
||||
Icon,
|
||||
Input,
|
||||
PageStack,
|
||||
Select,
|
||||
} from "jazz-inspector";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Breadcrumbs } from "./breadcrumbs";
|
||||
import { PageStack } from "./page-stack";
|
||||
import { usePagePath } from "./use-page-path";
|
||||
import { resolveCoValue, useResolvedCoValue } from "./use-resolve-covalue";
|
||||
|
||||
@@ -121,15 +126,23 @@ export default function CoJsonViewerApp() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full h-screen bg-gray-100 p-4 overflow-hidden flex flex-col">
|
||||
<div className="flex items-center mb-4 gap-4">
|
||||
<div
|
||||
className={clsx(
|
||||
"h-screen overflow-hidden flex flex-col",
|
||||
" text-stone-700 bg-white",
|
||||
"dark:text-stone-300 dark:bg-stone-950",
|
||||
)}
|
||||
>
|
||||
<header className="flex items-center gap-4 p-3">
|
||||
<Breadcrumbs path={path} onBreadcrumbClick={goToIndex} />
|
||||
<div className="flex-1">
|
||||
<form onSubmit={handleCoValueIdSubmit}>
|
||||
{path.length !== 0 && (
|
||||
<input
|
||||
className="border p-2 rounded-lg min-w-[21rem] font-mono"
|
||||
<Input
|
||||
className="min-w-[21rem] font-mono"
|
||||
placeholder="co_z1234567890abcdef123456789"
|
||||
label="CoValue ID"
|
||||
hideLabel
|
||||
value={coValueId}
|
||||
onChange={(e) =>
|
||||
setCoValueId(e.target.value as CoID<RawCoValue>)
|
||||
@@ -145,7 +158,7 @@ export default function CoJsonViewerApp() {
|
||||
deleteCurrentAccount={deleteCurrentAccount}
|
||||
localNode={localNode}
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<PageStack
|
||||
path={path}
|
||||
@@ -153,49 +166,39 @@ export default function CoJsonViewerApp() {
|
||||
goBack={goBack}
|
||||
addPages={addPages}
|
||||
>
|
||||
{!currentAccount ? (
|
||||
<AddAccountForm addAccount={addAccount} />
|
||||
) : (
|
||||
{!currentAccount && <AddAccountForm addAccount={addAccount} />}
|
||||
|
||||
{currentAccount && path.length <= 0 && (
|
||||
<form
|
||||
onSubmit={handleCoValueIdSubmit}
|
||||
aria-hidden={path.length !== 0}
|
||||
className={clsx(
|
||||
"flex flex-col justify-center items-center gap-2 h-full w-full mb-20 ",
|
||||
"transition-all duration-150",
|
||||
path.length > 0
|
||||
? "opacity-0 -translate-y-2 scale-95"
|
||||
: "opacity-100",
|
||||
)}
|
||||
className="flex flex-col relative -top-6 justify-center gap-2 h-full w-full max-w-sm mx-auto"
|
||||
>
|
||||
<fieldset className="flex flex-col gap-2 text-sm">
|
||||
<h2 className="text-3xl font-medium text-gray-950 text-center mb-4">
|
||||
Jazz CoValue Inspector
|
||||
</h2>
|
||||
<input
|
||||
className="border p-4 rounded-lg min-w-[21rem] font-mono"
|
||||
placeholder="co_z1234567890abcdef123456789"
|
||||
value={coValueId}
|
||||
onChange={(e) =>
|
||||
setCoValueId(e.target.value as CoID<RawCoValue>)
|
||||
}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-indigo-500 hover:bg-indigo-500/80 text-white px-4 py-2 rounded-md"
|
||||
>
|
||||
Inspect
|
||||
</button>
|
||||
<hr />
|
||||
<button
|
||||
type="button"
|
||||
className="border inline-block px-2 py-1.5 text-black rounded"
|
||||
onClick={() => {
|
||||
setPage(currentAccount.id);
|
||||
}}
|
||||
>
|
||||
Inspect My Account
|
||||
</button>
|
||||
</fieldset>
|
||||
<h1 className="text-lg text-center font-medium mb-4 text-stone-900 dark:text-white">
|
||||
Jazz CoValue Inspector
|
||||
</h1>
|
||||
<Input
|
||||
label="CoValue ID"
|
||||
className="font-mono"
|
||||
hideLabel
|
||||
placeholder="co_z1234567890abcdef123456789"
|
||||
value={coValueId}
|
||||
onChange={(e) => setCoValueId(e.target.value as CoID<RawCoValue>)}
|
||||
/>
|
||||
<Button type="submit" variant="primary">
|
||||
Inspect CoValue
|
||||
</Button>
|
||||
|
||||
<p className="text-center">or</p>
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setPage(currentAccount.id);
|
||||
}}
|
||||
>
|
||||
Inspect my account
|
||||
</Button>
|
||||
</form>
|
||||
)}
|
||||
</PageStack>
|
||||
@@ -217,8 +220,10 @@ function AccountSwitcher({
|
||||
localNode: LocalNode | null;
|
||||
}) {
|
||||
return (
|
||||
<div className="relative flex items-center gap-1">
|
||||
<select
|
||||
<div className="relative flex items-stretch gap-1">
|
||||
<Select
|
||||
label="Account to inspect"
|
||||
className="label:sr-only max-w-96"
|
||||
value={currentAccount?.id || "add-account"}
|
||||
onChange={(e) => {
|
||||
if (e.target.value === "add-account") {
|
||||
@@ -228,7 +233,6 @@ function AccountSwitcher({
|
||||
setCurrentAccount(account || null);
|
||||
}
|
||||
}}
|
||||
className="p-2 px-4 bg-gray-100/50 border border-indigo-500/10 backdrop-blur-sm rounded-md text-indigo-700 appearance-none"
|
||||
>
|
||||
{accounts.map((account) => (
|
||||
<option key={account.id} value={account.id}>
|
||||
@@ -240,15 +244,16 @@ function AccountSwitcher({
|
||||
</option>
|
||||
))}
|
||||
<option value="add-account">Add account</option>
|
||||
</select>
|
||||
</Select>
|
||||
{currentAccount && (
|
||||
<button
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={deleteCurrentAccount}
|
||||
className="p-3 rounded hover:bg-gray-200 transition-colors"
|
||||
title="Delete Account"
|
||||
className="rounded-md p-2 ml-1"
|
||||
aria-label="Remove account"
|
||||
>
|
||||
<Trash2 size={16} className="text-gray-500" />
|
||||
</button>
|
||||
<Icon name="delete" className="text-gray-500" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -272,30 +277,34 @@ function AddAccountForm({
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="flex flex-col gap-2 max-w-md mx-auto h-full justify-center"
|
||||
className="flex flex-col gap-3 max-w-md mx-auto h-full justify-center"
|
||||
>
|
||||
<h2 className="text-2xl font-medium text-gray-900 mb-3">
|
||||
Add an Account to Inspect
|
||||
<h2 className="text-2xl font-medium text-gray-900 dark:text-white">
|
||||
Add an account to inspect
|
||||
</h2>
|
||||
<input
|
||||
className="border py-2 px-3 rounded-md"
|
||||
placeholder="Account ID"
|
||||
<p className="leading-relaxed mb-5">
|
||||
Use the{" "}
|
||||
<code className="whitespace-nowrap text-stone-900 dark:text-white font-semibold">
|
||||
jazz-logged-in-secret
|
||||
</code>{" "}
|
||||
local storage key from within your Jazz app for your account
|
||||
credentials.
|
||||
</p>
|
||||
<Input
|
||||
label="Account ID"
|
||||
value={id}
|
||||
placeholder="co_z1234567890abcdef123456789"
|
||||
onChange={(e) => setId(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
<Input
|
||||
label="Account secret"
|
||||
type="password"
|
||||
className="border py-2 px-3 rounded-md"
|
||||
placeholder="Account Secret"
|
||||
value={secret}
|
||||
onChange={(e) => setSecret(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-indigo-500 text-white px-4 py-2 rounded-md"
|
||||
>
|
||||
Add Account
|
||||
</button>
|
||||
<Button className="mt-3" type="submit">
|
||||
Add account
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import { CoID, LocalNode, RawCoValue } from "cojson";
|
||||
import { Page } from "./page"; // Assuming you have a Page component
|
||||
|
||||
// Define the structure of a page in the path
|
||||
interface PageInfo {
|
||||
coId: CoID<RawCoValue>;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
// Props for the PageStack component
|
||||
interface PageStackProps {
|
||||
path: PageInfo[];
|
||||
node?: LocalNode | null;
|
||||
goBack: () => void;
|
||||
addPages: (pages: PageInfo[]) => void;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function PageStack({
|
||||
path,
|
||||
node,
|
||||
goBack,
|
||||
addPages,
|
||||
children,
|
||||
}: PageStackProps) {
|
||||
return (
|
||||
<div className="relative mt-4 h-[calc(100vh-6rem)]">
|
||||
{children && <div className="absolute inset-0 pb-20">{children}</div>}
|
||||
{node &&
|
||||
path.map((page, index) => (
|
||||
<Page
|
||||
key={`${page.coId}-${index}`}
|
||||
coId={page.coId}
|
||||
node={node}
|
||||
name={page.name || page.coId}
|
||||
onHeaderClick={goBack}
|
||||
onNavigate={addPages}
|
||||
isTopLevel={index === path.length - 1}
|
||||
style={{
|
||||
transform: `translateZ(${(index - path.length + 1) * 200}px) scale(${
|
||||
1 - (path.length - index - 1) * 0.05
|
||||
}) translateY(${-(index - path.length + 1) * -4}%)`,
|
||||
opacity: 1 - (path.length - index - 1) * 0.05,
|
||||
zIndex: index,
|
||||
transitionProperty: "transform, opacity",
|
||||
transitionDuration: "0.3s",
|
||||
transitionTimingFunction: "ease-out",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
import clsx from "clsx";
|
||||
import { CoID, LocalNode, RawCoStream, RawCoValue } from "cojson";
|
||||
import { useEffect, useState } from "react";
|
||||
import { CoStreamView } from "./co-stream-view";
|
||||
import { GridView } from "./grid-view";
|
||||
import { TableView } from "./table-viewer";
|
||||
import { TypeIcon } from "./type-icon";
|
||||
import { PageInfo } from "./types";
|
||||
import { useResolvedCoValue } from "./use-resolve-covalue";
|
||||
import { AccountOrGroupPreview } from "./value-renderer";
|
||||
|
||||
type PageProps = {
|
||||
coId: CoID<RawCoValue>;
|
||||
node: LocalNode;
|
||||
name: string;
|
||||
onNavigate: (newPages: PageInfo[]) => void;
|
||||
onHeaderClick?: () => void;
|
||||
isTopLevel?: boolean;
|
||||
style: React.CSSProperties;
|
||||
};
|
||||
|
||||
export function Page({
|
||||
coId,
|
||||
node,
|
||||
name,
|
||||
onNavigate,
|
||||
onHeaderClick,
|
||||
style,
|
||||
isTopLevel,
|
||||
}: PageProps) {
|
||||
const { value, snapshot, type, extendedType } = useResolvedCoValue(
|
||||
coId,
|
||||
node,
|
||||
);
|
||||
const [viewMode, setViewMode] = useState<"grid" | "table">("grid");
|
||||
|
||||
const supportsTableView = type === "colist" || extendedType === "record";
|
||||
|
||||
// Automatically switch to table view if the page is a CoMap record
|
||||
useEffect(() => {
|
||||
if (supportsTableView) {
|
||||
setViewMode("table");
|
||||
}
|
||||
}, [supportsTableView]);
|
||||
|
||||
if (snapshot === "unavailable") {
|
||||
return <div style={style}>Data unavailable</div>;
|
||||
}
|
||||
|
||||
if (!snapshot) {
|
||||
return <div style={style}></div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={style}
|
||||
className={clsx(
|
||||
"absolute inset-0 border border-gray-900/5 bg-clip-padding bg-white rounded-xl shadow-lg p-6 animate-in",
|
||||
)}
|
||||
>
|
||||
{!isTopLevel && (
|
||||
<div
|
||||
className="absolute inset-x-0 top-0 h-10"
|
||||
aria-label="Back"
|
||||
onClick={() => {
|
||||
onHeaderClick?.();
|
||||
}}
|
||||
aria-hidden="true"
|
||||
></div>
|
||||
)}
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h2 className="text-2xl font-bold flex items-start flex-col gap-1">
|
||||
<span>
|
||||
{name}
|
||||
{typeof snapshot === "object" && "name" in snapshot ? (
|
||||
<span className="text-gray-600 font-medium">
|
||||
{" "}
|
||||
{
|
||||
(
|
||||
snapshot as {
|
||||
name: string;
|
||||
}
|
||||
).name
|
||||
}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-gray-700 font-medium py-0.5 px-1 -ml-0.5 rounded bg-gray-700/5 inline-block font-mono">
|
||||
{type && <TypeIcon type={type} extendedType={extendedType} />}
|
||||
</span>
|
||||
<span className="text-xs text-gray-700 font-medium py-0.5 px-1 -ml-0.5 rounded bg-gray-700/5 inline-block font-mono">
|
||||
{coId}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* {supportsTableView && (
|
||||
<button
|
||||
onClick={toggleViewMode}
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"
|
||||
>
|
||||
{viewMode === "grid" ? "Table View" : "Grid View"}
|
||||
</button>
|
||||
)} */}
|
||||
</div>
|
||||
<div className="overflow-auto max-h-[calc(100%-4rem)]">
|
||||
{type === "costream" ? (
|
||||
<CoStreamView
|
||||
data={snapshot}
|
||||
onNavigate={onNavigate}
|
||||
node={node}
|
||||
value={value as RawCoStream}
|
||||
/>
|
||||
) : viewMode === "grid" ? (
|
||||
<GridView data={snapshot} onNavigate={onNavigate} node={node} />
|
||||
) : (
|
||||
<TableView data={snapshot} node={node} onNavigate={onNavigate} />
|
||||
)}
|
||||
{/* --- */}
|
||||
{extendedType !== "account" && extendedType !== "group" && (
|
||||
<div className="text-xs text-gray-500 mt-4">
|
||||
Owned by{" "}
|
||||
<AccountOrGroupPreview
|
||||
coId={value.group.id}
|
||||
node={node}
|
||||
showId
|
||||
onClick={() => {
|
||||
onNavigate([{ coId: value.group.id, name: "owner" }]);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
import { CoID, LocalNode, RawCoValue } from "cojson";
|
||||
import { JsonObject } from "cojson";
|
||||
import { useMemo, useState } from "react";
|
||||
import { LinkIcon } from "../link-icon";
|
||||
import { PageInfo } from "./types";
|
||||
import { useResolvedCoValues } from "./use-resolve-covalue";
|
||||
import { ValueRenderer } from "./value-renderer";
|
||||
|
||||
export function TableView({
|
||||
data,
|
||||
node,
|
||||
onNavigate,
|
||||
}: {
|
||||
data: JsonObject;
|
||||
node: LocalNode;
|
||||
onNavigate: (pages: PageInfo[]) => void;
|
||||
}) {
|
||||
const [visibleRowsCount, setVisibleRowsCount] = useState(10);
|
||||
const [coIdArray, visibleRows] = useMemo(() => {
|
||||
const coIdArray = Array.isArray(data)
|
||||
? data
|
||||
: Object.values(data).every(
|
||||
(k) => typeof k === "string" && k.startsWith("co_"),
|
||||
)
|
||||
? Object.values(data).map((k) => k as CoID<RawCoValue>)
|
||||
: [];
|
||||
|
||||
const visibleRows = coIdArray.slice(0, visibleRowsCount);
|
||||
|
||||
return [coIdArray, visibleRows];
|
||||
}, [data, visibleRowsCount]);
|
||||
const resolvedRows = useResolvedCoValues(visibleRows, node);
|
||||
|
||||
const hasMore = visibleRowsCount < coIdArray.length;
|
||||
|
||||
if (!coIdArray.length) {
|
||||
return <div>No data to display</div>;
|
||||
}
|
||||
|
||||
if (resolvedRows.length === 0) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
const keys = Array.from(
|
||||
new Set(resolvedRows.flatMap((item) => Object.keys(item.snapshot || {}))),
|
||||
);
|
||||
|
||||
const loadMore = () => {
|
||||
setVisibleRowsCount((prevVisibleRows) => prevVisibleRows + 10);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="sticky top-0 border-b">
|
||||
<tr>
|
||||
{["", ...keys].map((key) => (
|
||||
<th
|
||||
key={key}
|
||||
className="px-4 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 rounded"
|
||||
>
|
||||
{key}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{resolvedRows.slice(0, visibleRowsCount).map((item, index) => (
|
||||
<tr key={index}>
|
||||
<td className="px-1 py-0">
|
||||
<button
|
||||
onClick={() =>
|
||||
onNavigate([
|
||||
{
|
||||
coId: item.value!.id,
|
||||
name: index.toString(),
|
||||
},
|
||||
])
|
||||
}
|
||||
className="px-4 py-4 whitespace-nowrap text-sm text-gray-500 hover:text-blue-500 hover:bg-gray-100 rounded"
|
||||
>
|
||||
<LinkIcon />
|
||||
</button>
|
||||
</td>
|
||||
{keys.map((key) => (
|
||||
<td
|
||||
key={key}
|
||||
className="px-4 py-4 whitespace-nowrap text-sm text-gray-500"
|
||||
>
|
||||
<ValueRenderer
|
||||
json={(item.snapshot as JsonObject)[key]}
|
||||
onCoIDClick={(coId) => {
|
||||
async function handleClick() {
|
||||
onNavigate([
|
||||
{
|
||||
coId: item.value!.id,
|
||||
name: index.toString(),
|
||||
},
|
||||
{
|
||||
coId: coId,
|
||||
name: key,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
handleClick();
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="py-4 text-gray-500 flex items-center justify-between gap-2">
|
||||
<span>
|
||||
Showing {Math.min(visibleRowsCount, coIdArray.length)} of{" "}
|
||||
{coIdArray.length}
|
||||
</span>
|
||||
{hasMore && (
|
||||
<div className="text-center">
|
||||
<button
|
||||
onClick={loadMore}
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
||||
>
|
||||
Load More
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import { CoID, LocalNode, RawCoValue } from "cojson";
|
||||
import {
|
||||
CoJsonType,
|
||||
ExtendedCoJsonType,
|
||||
useResolvedCoValue,
|
||||
} from "./use-resolve-covalue";
|
||||
|
||||
export const TypeIcon = ({
|
||||
type,
|
||||
extendedType,
|
||||
}: {
|
||||
type: CoJsonType;
|
||||
extendedType?: ExtendedCoJsonType;
|
||||
}) => {
|
||||
const iconMap: Record<ExtendedCoJsonType | CoJsonType, string> = {
|
||||
record: "{} Record",
|
||||
image: "🖼️ Image",
|
||||
comap: "{} CoMap",
|
||||
costream: "≋ CoStream",
|
||||
colist: "☰ CoList",
|
||||
account: "👤 Account",
|
||||
group: "👥 Group",
|
||||
};
|
||||
|
||||
const iconKey = extendedType || type;
|
||||
const icon = iconMap[iconKey as keyof typeof iconMap];
|
||||
|
||||
return icon ? <span className="font-mono">{icon}</span> : null;
|
||||
};
|
||||
|
||||
export const ResolveIcon = ({
|
||||
coId,
|
||||
node,
|
||||
}: {
|
||||
coId: CoID<RawCoValue>;
|
||||
node: LocalNode;
|
||||
}) => {
|
||||
const { type, extendedType, snapshot } = useResolvedCoValue(coId, node);
|
||||
|
||||
if (snapshot === "unavailable" && !type) {
|
||||
return <div className="text-gray-600 font-medium">Unavailable</div>;
|
||||
}
|
||||
|
||||
if (!type) return <div className="whitespace-pre w-14 font-mono"> </div>;
|
||||
|
||||
return <TypeIcon type={type} extendedType={extendedType} />;
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
import { CoID, RawCoValue } from "cojson";
|
||||
|
||||
export type PageInfo = {
|
||||
coId: CoID<RawCoValue>;
|
||||
name?: string;
|
||||
};
|
||||
|
||||
export const isCoId = (coId: unknown): coId is CoID<RawCoValue> =>
|
||||
typeof coId === "string" && coId.startsWith("co_");
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CoID, RawCoValue } from "cojson";
|
||||
import { PageInfo } from "jazz-inspector";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { PageInfo } from "./types";
|
||||
|
||||
export function usePagePath(defaultPath?: PageInfo[]) {
|
||||
const [path, setPath] = useState<PageInfo[]>(() => {
|
||||
|
||||
@@ -1,260 +0,0 @@
|
||||
import clsx from "clsx";
|
||||
import { CoID, JsonValue, LocalNode, RawCoValue } from "cojson";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { LinkIcon } from "../link-icon";
|
||||
import {
|
||||
isBrowserImage,
|
||||
resolveCoValue,
|
||||
useResolvedCoValue,
|
||||
} from "./use-resolve-covalue";
|
||||
|
||||
// Is there a chance we can pass the actual CoValue here?
|
||||
export function ValueRenderer({
|
||||
json,
|
||||
compact,
|
||||
onCoIDClick,
|
||||
}: {
|
||||
json: JsonValue | undefined;
|
||||
compact?: boolean;
|
||||
onCoIDClick?: (childNode: CoID<RawCoValue>) => void;
|
||||
}) {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
if (typeof json === "undefined" || json === undefined) {
|
||||
return <span className="text-gray-400">undefined</span>;
|
||||
}
|
||||
|
||||
if (json === null) {
|
||||
return <span className="text-gray-400">null</span>;
|
||||
}
|
||||
|
||||
if (typeof json === "string" && json.startsWith("co_")) {
|
||||
return (
|
||||
<span
|
||||
className={clsx(
|
||||
"inline-flex gap-1 items-center",
|
||||
onCoIDClick && "text-blue-500 cursor-pointer hover:underline",
|
||||
)}
|
||||
onClick={() => {
|
||||
onCoIDClick?.(json as CoID<RawCoValue>);
|
||||
}}
|
||||
>
|
||||
{json}
|
||||
{onCoIDClick && <LinkIcon />}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof json === "string") {
|
||||
return (
|
||||
<span className="text-green-900 font-mono">
|
||||
{/* <span className="select-none opacity-70">{'"'}</span> */}
|
||||
{json}
|
||||
{/* <span className="select-none opacity-70">{'"'}</span> */}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof json === "number") {
|
||||
return <span className="text-purple-500">{json}</span>;
|
||||
}
|
||||
|
||||
if (typeof json === "boolean") {
|
||||
return (
|
||||
<span
|
||||
className={clsx(
|
||||
json
|
||||
? "text-green-700 bg-green-700/5"
|
||||
: "text-amber-700 bg-amber-500/5",
|
||||
"font-mono",
|
||||
"inline-block px-1 py-0.5 rounded",
|
||||
)}
|
||||
>
|
||||
{json.toString()}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (Array.isArray(json)) {
|
||||
return (
|
||||
<span title={JSON.stringify(json)}>
|
||||
Array <span className="text-gray-500">({json.length})</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof json === "object") {
|
||||
return (
|
||||
<span
|
||||
title={JSON.stringify(json, null, 2)}
|
||||
className="inline-block max-w-64"
|
||||
>
|
||||
{compact ? (
|
||||
<span>
|
||||
Object{" "}
|
||||
<span className="text-gray-500">({Object.keys(json).length})</span>
|
||||
<pre className="mt-1 text-sm whitespace-pre-wrap">
|
||||
{isExpanded
|
||||
? JSON.stringify(json, null, 2)
|
||||
: JSON.stringify(json, null, 2)
|
||||
.split("\n")
|
||||
.slice(0, 3)
|
||||
.join("\n") + (Object.keys(json).length > 2 ? "\n..." : "")}
|
||||
</pre>
|
||||
<button
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="text-xs text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
{isExpanded ? "Show less" : "Show more"}
|
||||
</button>
|
||||
</span>
|
||||
) : (
|
||||
<pre className="whitespace-pre-wrap">
|
||||
{JSON.stringify(json, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return <span>{String(json)}</span>;
|
||||
}
|
||||
|
||||
export const CoMapPreview = ({
|
||||
coId,
|
||||
node,
|
||||
limit = 6,
|
||||
}: {
|
||||
coId: CoID<RawCoValue>;
|
||||
node: LocalNode;
|
||||
limit?: number;
|
||||
}) => {
|
||||
const { value, snapshot, type, extendedType } = useResolvedCoValue(
|
||||
coId,
|
||||
node,
|
||||
);
|
||||
|
||||
if (!snapshot) {
|
||||
return (
|
||||
<div className="rounded bg-gray-100 animate-pulse whitespace-pre w-24">
|
||||
{" "}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (snapshot === "unavailable" && !value) {
|
||||
return <div className="text-gray-500">Unavailable</div>;
|
||||
}
|
||||
|
||||
if (extendedType === "image" && isBrowserImage(snapshot)) {
|
||||
return (
|
||||
<div>
|
||||
<img
|
||||
src={snapshot.placeholderDataURL}
|
||||
className="size-8 border-2 border-white drop-shadow-md my-2"
|
||||
/>
|
||||
<span className="text-gray-500 text-sm">
|
||||
{snapshot.originalSize[0]} x {snapshot.originalSize[1]}
|
||||
</span>
|
||||
|
||||
{/* <CoMapPreview coId={value[]} node={node} /> */}
|
||||
{/* <ProgressiveImg image={value}>
|
||||
{({ src }) => <img src={src} className={clsx("w-full")} />}
|
||||
</ProgressiveImg> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (extendedType === "record") {
|
||||
return (
|
||||
<div>
|
||||
Record{" "}
|
||||
<span className="text-gray-500">({Object.keys(snapshot).length})</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === "colist") {
|
||||
return (
|
||||
<div>
|
||||
List{" "}
|
||||
<span className="text-gray-500">
|
||||
({(snapshot as unknown as []).length})
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="text-sm flex flex-col gap-2 items-start">
|
||||
<div className="grid grid-cols-[auto_1fr] gap-2">
|
||||
{Object.entries(snapshot)
|
||||
.slice(0, limit)
|
||||
.map(([key, value]) => (
|
||||
<React.Fragment key={key}>
|
||||
<span className="font-medium">{key}: </span>
|
||||
<span>
|
||||
<ValueRenderer json={value} />
|
||||
</span>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
{Object.entries(snapshot).length > limit && (
|
||||
<div className="text-left text-xs text-gray-500 mt-2">
|
||||
{Object.entries(snapshot).length - limit} more
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export function AccountOrGroupPreview({
|
||||
coId,
|
||||
node,
|
||||
showId = false,
|
||||
onClick,
|
||||
}: {
|
||||
coId: CoID<RawCoValue>;
|
||||
node: LocalNode;
|
||||
showId?: boolean;
|
||||
onClick?: (name?: string) => void;
|
||||
}) {
|
||||
const { snapshot, extendedType } = useResolvedCoValue(coId, node);
|
||||
const [name, setName] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (extendedType === "account") {
|
||||
resolveCoValue(
|
||||
(snapshot as unknown as { profile: CoID<RawCoValue> }).profile,
|
||||
node,
|
||||
).then(({ snapshot }) => {
|
||||
if (
|
||||
typeof snapshot === "object" &&
|
||||
"name" in snapshot &&
|
||||
typeof snapshot.name === "string"
|
||||
) {
|
||||
setName(snapshot.name);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [snapshot, node, extendedType]);
|
||||
|
||||
if (!snapshot) return <span>Loading...</span>;
|
||||
if (extendedType !== "account" && extendedType !== "group") {
|
||||
return <span>CoID is not an account or group</span>;
|
||||
}
|
||||
|
||||
const displayName = extendedType === "account" ? name || "Account" : "Group";
|
||||
const displayText = showId ? `${displayName} (${coId})` : displayName;
|
||||
|
||||
const props = onClick
|
||||
? {
|
||||
onClick: () => onClick(displayName),
|
||||
className: "text-blue-500 cursor-pointer hover:underline",
|
||||
}
|
||||
: {
|
||||
className: "text-gray-500",
|
||||
};
|
||||
|
||||
return <span {...props}>{displayText}</span>;
|
||||
}
|
||||
@@ -1,5 +1,30 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
import animate from "tailwindcss-animate";
|
||||
import plugin from "tailwindcss/plugin";
|
||||
|
||||
const stonePalette = {
|
||||
50: "oklch(0.988281 0.002 75)",
|
||||
100: "oklch(0.980563 0.002 75)",
|
||||
200: "oklch(0.917969 0.002 75)",
|
||||
300: "oklch(0.853516 0.002 75)",
|
||||
400: "oklch(0.789063 0.002 75)",
|
||||
500: "oklch(0.726563 0.002 75)",
|
||||
600: "oklch(0.613281 0.002 75)",
|
||||
700: "oklch(0.523438 0.002 75)",
|
||||
800: "oklch(0.412109 0.002 75)",
|
||||
900: "oklch(0.302734 0.002 75)",
|
||||
925: "oklch(0.220000 0.002 75)",
|
||||
950: "oklch(0.193359 0.002 75)",
|
||||
};
|
||||
|
||||
const stonePaletteWithAlpha = { ...stonePalette };
|
||||
|
||||
Object.keys(stonePalette).forEach((key) => {
|
||||
// @ts-ignore
|
||||
stonePaletteWithAlpha[key] = stonePaletteWithAlpha[key].replace(
|
||||
")",
|
||||
"/ <alpha-value>)",
|
||||
);
|
||||
});
|
||||
|
||||
const config: Config = {
|
||||
content: [
|
||||
@@ -18,62 +43,26 @@ const config: Config = {
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
primary: {
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
stone: stonePaletteWithAlpha,
|
||||
gray: stonePaletteWithAlpha,
|
||||
blue: {
|
||||
50: "#f5f7ff",
|
||||
100: "#ebf0fe",
|
||||
200: "#d6e0fd",
|
||||
300: "#b3c7fc",
|
||||
400: "#8aa6f9",
|
||||
500: "#5870F1",
|
||||
600: "#3651E7",
|
||||
700: "#3313F7",
|
||||
800: "#2A12BE",
|
||||
900: "#12046A",
|
||||
950: "#1e1b4b",
|
||||
DEFAULT: "#3313F7",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
foreground: "hsl(var(--destructive-foreground))",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsl(var(--popover))",
|
||||
foreground: "hsl(var(--popover-foreground))",
|
||||
},
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
keyframes: {
|
||||
"accordion-down": {
|
||||
from: { height: "0" },
|
||||
to: { height: "var(--radix-accordion-content-height)" },
|
||||
},
|
||||
"accordion-up": {
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: "0" },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [animate],
|
||||
plugins: [plugin(({ addVariant }) => addVariant("label", "& :is(label)"))],
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@@ -13,11 +13,11 @@ To run this example, you may either:
|
||||
|
||||
1. Create a new Jazz project, and use this example as a template.
|
||||
```bash
|
||||
npx create-jazz-app@latest --example counter --project-name counter
|
||||
npx create-jazz-app@latest counter-app --example counter
|
||||
```
|
||||
2. Navigate to the new project and start the development server.
|
||||
```bash
|
||||
cd counter
|
||||
cd counter-app
|
||||
npm run dev
|
||||
```
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ createRoot(document.getElementById("root")!).render(
|
||||
<OmniAuth
|
||||
sync={{
|
||||
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
|
||||
when: "signedUp", // This makes the app work in local mode when the user is not authenticated
|
||||
}}
|
||||
>
|
||||
<App />
|
||||
|
||||
@@ -13,12 +13,12 @@ You can either
|
||||
|
||||
Create a new Jazz project, and use this example as a template.
|
||||
```bash
|
||||
npx create-jazz-app@latest --example music-player --project-name music-player
|
||||
npx create-jazz-app@latest music-player-app --example music-player
|
||||
```
|
||||
|
||||
Go to the new project directory.
|
||||
```bash
|
||||
cd music-player
|
||||
cd music-player-app
|
||||
```
|
||||
|
||||
Run the dev server.
|
||||
|
||||
@@ -72,7 +72,6 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<JazzProvider
|
||||
sync={{
|
||||
peer,
|
||||
when: "signedUp", // This makes the app work in local mode when the user is anonymous
|
||||
}}
|
||||
storage="indexedDB"
|
||||
AccountSchema={MusicaAccount}
|
||||
|
||||
@@ -24,10 +24,7 @@ export function HomePage({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
|
||||
* access rights to CoValues. We get it from the top-level provider `<WithJazz/>`.
|
||||
*/
|
||||
const { me } = useAccount({
|
||||
root: {
|
||||
rootPlaylist: {},
|
||||
playlists: [],
|
||||
},
|
||||
resolve: { root: { rootPlaylist: true, playlists: true } },
|
||||
});
|
||||
|
||||
const navigate = useNavigate();
|
||||
@@ -51,8 +48,9 @@ export function HomePage({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
|
||||
|
||||
const params = useParams<{ playlistId: ID<Playlist> }>();
|
||||
const playlistId = params.playlistId ?? me?.root._refs.rootPlaylist.id;
|
||||
|
||||
const playlist = useCoState(Playlist, playlistId, {
|
||||
tracks: [],
|
||||
resolve: { tracks: true },
|
||||
});
|
||||
|
||||
const isRootPlaylist = !params.playlistId;
|
||||
|
||||
@@ -27,9 +27,11 @@ export async function uploadMusicTracks(
|
||||
isExampleTrack: boolean = false,
|
||||
) {
|
||||
const { root } = await MusicaAccount.getMe().ensureLoaded({
|
||||
root: {
|
||||
rootPlaylist: {
|
||||
tracks: [],
|
||||
resolve: {
|
||||
root: {
|
||||
rootPlaylist: {
|
||||
tracks: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -65,8 +67,10 @@ export async function uploadMusicTracks(
|
||||
|
||||
export async function createNewPlaylist() {
|
||||
const { root } = await MusicaAccount.getMe().ensureLoaded({
|
||||
root: {
|
||||
playlists: [],
|
||||
resolve: {
|
||||
root: {
|
||||
playlists: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -149,9 +153,11 @@ export async function updateMusicTrackTitle(track: MusicTrack, title: string) {
|
||||
|
||||
export async function updateActivePlaylist(playlist?: Playlist) {
|
||||
const { root } = await MusicaAccount.getMe().ensureLoaded({
|
||||
root: {
|
||||
activePlaylist: {},
|
||||
rootPlaylist: {},
|
||||
resolve: {
|
||||
root: {
|
||||
activePlaylist: true,
|
||||
rootPlaylist: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -160,7 +166,9 @@ export async function updateActivePlaylist(playlist?: Playlist) {
|
||||
|
||||
export async function updateActiveTrack(track: MusicTrack) {
|
||||
const { root } = await MusicaAccount.getMe().ensureLoaded({
|
||||
root: {},
|
||||
resolve: {
|
||||
root: {},
|
||||
},
|
||||
});
|
||||
|
||||
root.activeTrack = track;
|
||||
@@ -170,17 +178,23 @@ export async function onAnonymousAccountDiscarded(
|
||||
anonymousAccount: MusicaAccount,
|
||||
) {
|
||||
const { root: anonymousAccountRoot } = await anonymousAccount.ensureLoaded({
|
||||
root: {
|
||||
rootPlaylist: {
|
||||
tracks: [{}],
|
||||
resolve: {
|
||||
root: {
|
||||
rootPlaylist: {
|
||||
tracks: {
|
||||
$each: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const me = await MusicaAccount.getMe().ensureLoaded({
|
||||
root: {
|
||||
rootPlaylist: {
|
||||
tracks: [],
|
||||
resolve: {
|
||||
root: {
|
||||
rootPlaylist: {
|
||||
tracks: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -197,8 +211,10 @@ export async function onAnonymousAccountDiscarded(
|
||||
|
||||
export async function deletePlaylist(playlistId: string) {
|
||||
const { root } = await MusicaAccount.getMe().ensureLoaded({
|
||||
root: {
|
||||
playlists: [],
|
||||
resolve: {
|
||||
root: {
|
||||
playlists: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { getNextTrack, getPrevTrack } from "./lib/getters";
|
||||
|
||||
export function useMediaPlayer() {
|
||||
const { me } = useAccount({
|
||||
root: {},
|
||||
resolve: { root: true },
|
||||
});
|
||||
|
||||
const playState = usePlayState();
|
||||
|
||||
@@ -16,8 +16,10 @@ export function InvitePage() {
|
||||
const playlist = await Playlist.load(playlistId, {});
|
||||
|
||||
const me = await MusicaAccount.getMe().ensureLoaded({
|
||||
root: {
|
||||
playlists: [],
|
||||
resolve: {
|
||||
root: {
|
||||
playlists: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -22,9 +22,13 @@ export function AuthModal({ open, onOpenChange }: AuthModalProps) {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const { me } = useAccount({
|
||||
root: {
|
||||
rootPlaylist: {
|
||||
tracks: [{}],
|
||||
resolve: {
|
||||
root: {
|
||||
rootPlaylist: {
|
||||
tracks: {
|
||||
$each: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -30,9 +30,7 @@ export function MusicTrackRow({
|
||||
const track = useCoState(MusicTrack, trackId);
|
||||
|
||||
const { me } = useAccount({
|
||||
root: {
|
||||
playlists: [{}],
|
||||
},
|
||||
resolve: { root: { playlists: { $each: true } } },
|
||||
});
|
||||
|
||||
const playlists = me?.root.playlists ?? [];
|
||||
|
||||
@@ -12,9 +12,7 @@ export function PlayerControls({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
|
||||
const isPlaying = playState.value === "play";
|
||||
|
||||
const activePlaylist = useAccount({
|
||||
root: {
|
||||
activePlaylist: {},
|
||||
},
|
||||
resolve: { root: { activePlaylist: true } },
|
||||
}).me?.root.activePlaylist;
|
||||
|
||||
useMediaEndListener(mediaPlayer.playNextTrack);
|
||||
@@ -25,7 +23,7 @@ export function PlayerControls({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
|
||||
});
|
||||
|
||||
const activeTrack = useCoState(MusicTrack, mediaPlayer.activeTrackId, {
|
||||
waveform: {},
|
||||
resolve: { waveform: true },
|
||||
});
|
||||
|
||||
if (!activeTrack) return null;
|
||||
|
||||
@@ -8,9 +8,7 @@ export function SidePanel() {
|
||||
const { playlistId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const { me } = useAccount({
|
||||
root: {
|
||||
playlists: [{}],
|
||||
},
|
||||
resolve: { root: { playlists: { $each: true } } },
|
||||
});
|
||||
|
||||
function handleAllTracksClick(evt: React.MouseEvent<HTMLAnchorElement>) {
|
||||
|
||||
@@ -8,7 +8,6 @@ export function Waveform(props: { track: MusicTrack; height: number }) {
|
||||
const waveformData = useCoState(
|
||||
MusicTrackWaveform,
|
||||
track._refs.waveform.id,
|
||||
{},
|
||||
)?.data;
|
||||
const duration = track.duration;
|
||||
|
||||
|
||||
@@ -2,9 +2,11 @@ import { MusicaAccount } from "../1_schema";
|
||||
|
||||
export async function getNextTrack() {
|
||||
const me = await MusicaAccount.getMe().ensureLoaded({
|
||||
root: {
|
||||
activePlaylist: {
|
||||
tracks: [],
|
||||
resolve: {
|
||||
root: {
|
||||
activePlaylist: {
|
||||
tracks: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -21,9 +23,11 @@ export async function getNextTrack() {
|
||||
|
||||
export async function getPrevTrack() {
|
||||
const me = await MusicaAccount.getMe().ensureLoaded({
|
||||
root: {
|
||||
activePlaylist: {
|
||||
tracks: [],
|
||||
resolve: {
|
||||
root: {
|
||||
activePlaylist: {
|
||||
tracks: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ export function useUploadExampleData() {
|
||||
|
||||
async function uploadOnboardingData() {
|
||||
const me = await MusicaAccount.getMe().ensureLoaded({
|
||||
root: {},
|
||||
resolve: { root: true },
|
||||
});
|
||||
|
||||
if (me.root.exampleDataLoaded) return;
|
||||
|
||||
@@ -16,12 +16,12 @@ You can either
|
||||
|
||||
Create a new Jazz project, and use this example as a template.
|
||||
```bash
|
||||
npx create-jazz-app@latest --example organization --project-name organization
|
||||
npx create-jazz-app@latest organization-app --example organization
|
||||
```
|
||||
|
||||
Go to the new project directory.
|
||||
```bash
|
||||
cd organization
|
||||
cd organization-app
|
||||
```
|
||||
|
||||
Run the dev server.
|
||||
|
||||
@@ -5,11 +5,11 @@ import { Organization } from "./schema.ts";
|
||||
|
||||
export function AcceptInvitePage() {
|
||||
const navigate = useNavigate();
|
||||
const { me } = useAccount({ root: { organizations: [] } });
|
||||
const { me } = useAccount({ resolve: { root: { organizations: true } } });
|
||||
|
||||
const onAccept = (organizationId: ID<Organization>) => {
|
||||
if (me?.root?.organizations) {
|
||||
Organization.load(organizationId, me, []).then((organization) => {
|
||||
Organization.load(organizationId).then((organization) => {
|
||||
if (organization) {
|
||||
// avoid duplicates
|
||||
const ids = me.root.organizations.map(
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Heading } from "./components/Heading.tsx";
|
||||
|
||||
export function HomePage() {
|
||||
const { me } = useAccount({
|
||||
root: { organizations: [{}] },
|
||||
resolve: { root: { organizations: true } },
|
||||
});
|
||||
|
||||
if (!me?.root.organizations) return;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { UserIcon } from "lucide-react";
|
||||
|
||||
export function Layout({ children }: { children: React.ReactNode }) {
|
||||
const { me, logOut } = useAccount({
|
||||
root: { draftOrganization: {} },
|
||||
resolve: { root: { draftOrganization: true } },
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -13,7 +13,7 @@ export function OrganizationPage() {
|
||||
.organizationId;
|
||||
|
||||
const organization = useCoState(Organization, paramOrganizationId, {
|
||||
projects: [],
|
||||
resolve: { projects: true },
|
||||
});
|
||||
|
||||
if (!organization) return <p>Loading organization...</p>;
|
||||
|
||||
@@ -8,7 +8,7 @@ import { OrganizationForm } from "./OrganizationForm.tsx";
|
||||
|
||||
export function CreateOrganization() {
|
||||
const { me } = useAccount({
|
||||
root: { draftOrganization: {}, organizations: [] },
|
||||
resolve: { root: { draftOrganization: true, organizations: true } },
|
||||
});
|
||||
const [errors, setErrors] = useState<string[]>([]);
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Organization } from "../schema.ts";
|
||||
|
||||
export function OrganizationSelector({ className }: { className?: string }) {
|
||||
const { me } = useAccount({
|
||||
root: { organizations: [{}] },
|
||||
resolve: { root: { organizations: { $each: true } } },
|
||||
});
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -21,12 +21,12 @@ You can either
|
||||
|
||||
Create a new Jazz project, and use this example as a template.
|
||||
```bash
|
||||
npx create-jazz-app@latest --example passkey-svelte --project-name passkey-svelte
|
||||
npx create-jazz-app@latest passkey-svelte-app --example passkey-svelte
|
||||
```
|
||||
|
||||
Go to the new project directory.
|
||||
```bash
|
||||
cd passkey-svelte
|
||||
cd passkey-svelte-app
|
||||
```
|
||||
|
||||
Run the dev server.
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
<JazzProvider
|
||||
sync={{
|
||||
peer: `wss://cloud.jazz.tools/?key={apiKey}`,
|
||||
when: "signedUp",
|
||||
}}
|
||||
>
|
||||
<PasskeyAuthBasicUI appName="minimal-svelte-auth-passkey">
|
||||
|
||||
@@ -15,12 +15,12 @@ You can either
|
||||
|
||||
Create a new Jazz project, and use this example as a template.
|
||||
```bash
|
||||
npx create-jazz-app@latest --example passkey --project-name passkey
|
||||
npx create-jazz-app@latest passkey-app --example passkey
|
||||
```
|
||||
|
||||
Go to the new project directory.
|
||||
```bash
|
||||
cd passkey
|
||||
cd passkey-app
|
||||
```
|
||||
|
||||
Run the dev server.
|
||||
|
||||
@@ -10,7 +10,6 @@ function JazzAndAuth({ children }: { children: React.ReactNode }) {
|
||||
<JazzProvider
|
||||
sync={{
|
||||
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
|
||||
when: "signedUp",
|
||||
}}
|
||||
>
|
||||
<PasskeyAuthBasicUI appName="Jazz Minimal Auth Passkey Example">
|
||||
|
||||
@@ -12,12 +12,12 @@ You can either
|
||||
|
||||
Create a new Jazz project, and use this example as a template.
|
||||
```bash
|
||||
npx create-jazz-app@latest --example passphrase --project-name passphrase
|
||||
npx create-jazz-app@latest passphrase-app --example passphrase
|
||||
```
|
||||
|
||||
Go to the new project directory.
|
||||
```bash
|
||||
cd passphrase
|
||||
cd passphrase-app
|
||||
```
|
||||
|
||||
Run the dev server.
|
||||
|
||||
@@ -142,7 +142,6 @@ function JazzAndAuth({ children }: { children: React.ReactNode }) {
|
||||
<JazzProvider
|
||||
sync={{
|
||||
peer: "wss://cloud.jazz.tools/?key=minimal-auth-passphrase-example@garden.co",
|
||||
when: "signedUp",
|
||||
}}
|
||||
>
|
||||
<PassphraseAuthBasicUI
|
||||
|
||||
@@ -15,12 +15,12 @@ You can either
|
||||
|
||||
Create a new Jazz project, and use this example as a template.
|
||||
```bash
|
||||
npx create-jazz-app@latest --example password-manager --project-name password-manager
|
||||
npx create-jazz-app@latest password-manager-app --example password-manager
|
||||
```
|
||||
|
||||
Go to the new project directory.
|
||||
```bash
|
||||
cd password-manager
|
||||
cd password-manager-app
|
||||
```
|
||||
|
||||
Run the dev server.
|
||||
|
||||
@@ -12,7 +12,6 @@ function JazzAndAuth({ children }: { children: React.ReactNode }) {
|
||||
AccountSchema={PasswordManagerAccount}
|
||||
sync={{
|
||||
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
|
||||
when: "signedUp",
|
||||
}}
|
||||
>
|
||||
<PasskeyAuthBasicUI appName="Jazz Password Manager">
|
||||
|
||||
@@ -43,9 +43,11 @@ const VaultPage: React.FC = () => {
|
||||
(item): item is Exclude<typeof item, null> => !!item,
|
||||
) || [],
|
||||
);
|
||||
const folders = useCoState(FolderList, me.root?._refs.folders?.id, [
|
||||
{ items: [{}] },
|
||||
]);
|
||||
const folders = useCoState(FolderList, me.root?._refs.folders?.id, {
|
||||
resolve: {
|
||||
$each: { items: { $each: true } },
|
||||
},
|
||||
});
|
||||
|
||||
const [selectedFolder, setSelectedFolder] = useState<Folder | undefined>();
|
||||
const [isNewItemModalOpen, setIsNewItemModalOpen] = useState(false);
|
||||
|
||||
@@ -60,11 +60,9 @@ export async function addSharedFolder(
|
||||
me: PasswordManagerAccount,
|
||||
) {
|
||||
const [sharedFolder, account] = await Promise.all([
|
||||
Folder.load(sharedFolderId, me, {}),
|
||||
PasswordManagerAccount.load(me.id, me, {
|
||||
root: {
|
||||
folders: [],
|
||||
},
|
||||
Folder.load(sharedFolderId),
|
||||
PasswordManagerAccount.load(me.id, {
|
||||
resolve: { root: { folders: true } },
|
||||
}),
|
||||
]);
|
||||
|
||||
|
||||
@@ -13,12 +13,12 @@ You can either
|
||||
|
||||
Create a new Jazz project, and use this example as a template.
|
||||
```bash
|
||||
npx create-jazz-app@latest --example pets --project-name pets
|
||||
npx create-jazz-app@latest pets-app --example pets
|
||||
```
|
||||
|
||||
Go to the new project directory.
|
||||
```bash
|
||||
cd pets
|
||||
cd pets-app
|
||||
```
|
||||
|
||||
Run the dev server.
|
||||
|
||||
@@ -52,7 +52,6 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<JazzProvider
|
||||
sync={{
|
||||
peer,
|
||||
when: "signedUp",
|
||||
}}
|
||||
AccountSchema={PetAccount}
|
||||
>
|
||||
|
||||
@@ -13,12 +13,12 @@ You can either
|
||||
|
||||
Create a new Jazz project, and use this example as a template.
|
||||
```bash
|
||||
npx create-jazz-app@latest --example reactions --project-name reactions
|
||||
npx create-jazz-app@latest reactions-app --example reactions
|
||||
```
|
||||
|
||||
Go to the new project directory.
|
||||
```bash
|
||||
cd reactions
|
||||
cd reactions-app
|
||||
```
|
||||
|
||||
Run the dev server.
|
||||
|
||||
@@ -14,7 +14,7 @@ const reactionEmojiMap: {
|
||||
};
|
||||
|
||||
export function ReactionsScreen(props: { id: ID<Reactions> }) {
|
||||
const reactions = useCoState(Reactions, props.id, []);
|
||||
const reactions = useCoState(Reactions, props.id);
|
||||
|
||||
if (!reactions) return;
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ createRoot(document.getElementById("root")!).render(
|
||||
<JazzProvider
|
||||
sync={{
|
||||
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
|
||||
when: "signedUp",
|
||||
}}
|
||||
>
|
||||
<PasskeyAuthBasicUI appName="Jazz Reactions Example">
|
||||
|
||||
@@ -11,12 +11,12 @@ You can either
|
||||
|
||||
Create a new Jazz project, and use this example as a template.
|
||||
```bash
|
||||
npx create-jazz-app@latest --example todo-vue --project-name todo-vue
|
||||
npx create-jazz-app@latest todo-vue-app --example todo-vue
|
||||
```
|
||||
|
||||
Go to the new project directory.
|
||||
```bash
|
||||
cd todo-vue
|
||||
cd todo-vue-app
|
||||
```
|
||||
|
||||
Run the dev server.
|
||||
|
||||
@@ -4,15 +4,15 @@
|
||||
<div class="section-header">
|
||||
<h2>Folders</h2>
|
||||
<div class="new-folder">
|
||||
<input
|
||||
v-model="newFolderName"
|
||||
placeholder="New folder name"
|
||||
<input
|
||||
v-model="newFolderName"
|
||||
placeholder="New folder name"
|
||||
class="input"
|
||||
/>
|
||||
<button class="btn btn-primary" @click="createFolder">Create</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="folder-list">
|
||||
<div
|
||||
v-for="folder in folders"
|
||||
@@ -32,9 +32,9 @@
|
||||
<div class="section-header">
|
||||
<h2>{{ selectedFolder?.name }}</h2>
|
||||
<div class="new-todo">
|
||||
<input
|
||||
v-model="newTodoTitle"
|
||||
placeholder="Add a new task"
|
||||
<input
|
||||
v-model="newTodoTitle"
|
||||
placeholder="Add a new task"
|
||||
class="input"
|
||||
/>
|
||||
<button class="btn btn-primary" @click="createTodo">Add</button>
|
||||
@@ -72,7 +72,9 @@ import { Folder, FolderList, ToDoItem, ToDoList } from "../schema";
|
||||
const { me } = useAccount();
|
||||
|
||||
const computedFoldersId = computed(() => me.value?.root?.folders?.id);
|
||||
const folders = useCoState(FolderList, computedFoldersId, [{ items: [{}] }]);
|
||||
const folders = useCoState(FolderList, computedFoldersId, {
|
||||
resolve: { $each: { items: true } },
|
||||
});
|
||||
|
||||
const selectedFolder = ref<Folder>();
|
||||
const newFolderName = ref("");
|
||||
|
||||
@@ -13,12 +13,12 @@ You can either
|
||||
|
||||
Create a new Jazz project, and use this example as a template.
|
||||
```bash
|
||||
npx create-jazz-app@latest --example todo --project-name todo
|
||||
npx create-jazz-app@latest todo-app --example todo
|
||||
```
|
||||
|
||||
Go to the new project directory.
|
||||
```bash
|
||||
cd todo
|
||||
cd todo-app
|
||||
```
|
||||
|
||||
Run the dev server.
|
||||
|
||||
@@ -42,7 +42,6 @@ function JazzAndAuth({ children }: { children: React.ReactNode }) {
|
||||
<JazzProvider
|
||||
sync={{
|
||||
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
|
||||
when: "signedUp",
|
||||
}}
|
||||
AccountSchema={TodoAccount}
|
||||
>
|
||||
@@ -122,7 +121,7 @@ export default function App() {
|
||||
|
||||
function HomeScreen() {
|
||||
const { me } = useAccount({
|
||||
root: { projects: [{}] },
|
||||
resolve: { root: { projects: { $each: true } } },
|
||||
});
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
||||
@@ -13,12 +13,12 @@ You can either
|
||||
|
||||
Create a new Jazz project, and use this example as a template.
|
||||
```bash
|
||||
npx create-jazz-app@latest --example version-history --project-name version-history
|
||||
npx create-jazz-app@latest version-history-app --example version-history
|
||||
```
|
||||
|
||||
Go to the new project directory.
|
||||
```bash
|
||||
cd version-history
|
||||
cd version-history-app
|
||||
```
|
||||
|
||||
Run the dev server.
|
||||
|
||||
@@ -4,7 +4,9 @@ import { ID } from "jazz-tools";
|
||||
import { IssueComponent } from "./Issue.tsx";
|
||||
import { Issue, Project } from "./schema.ts";
|
||||
export function ProjectComponent({ projectID }: { projectID: ID<Project> }) {
|
||||
const project = useCoState(Project, projectID, { issues: [{}] });
|
||||
const project = useCoState(Project, projectID, {
|
||||
resolve: { issues: { $each: true } },
|
||||
});
|
||||
|
||||
if (!project) return;
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ createRoot(document.getElementById("root")!).render(
|
||||
<JazzProvider
|
||||
sync={{
|
||||
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
|
||||
when: "signedUp",
|
||||
}}
|
||||
>
|
||||
<DemoAuthBasicUI appName="Jazz Version History Example">
|
||||
|
||||
37
homepage/gcmp/app/api/rss/route.ts
Normal file
37
homepage/gcmp/app/api/rss/route.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { metaTags } from "@/app/layout";
|
||||
import { posts } from "@/lib/posts";
|
||||
import { Feed } from "feed";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET() {
|
||||
const feed = new Feed({
|
||||
title: "Garden Computing Blog",
|
||||
description: "News from Garden Computing",
|
||||
id: metaTags.url,
|
||||
link: `${metaTags.url}/news`,
|
||||
language: "en",
|
||||
image: `${metaTags.url}/social-image.png`,
|
||||
favicon: `${metaTags.url}/favicon.ico`,
|
||||
copyright: `${new Date().getFullYear()} Garden Computing, Inc.`,
|
||||
});
|
||||
|
||||
posts.forEach((post) => {
|
||||
feed.addItem({
|
||||
title: post.meta.title,
|
||||
description: post.meta.subtitle,
|
||||
id: post.meta.slug,
|
||||
link: `${metaTags.url}/news/${post.meta.slug}`,
|
||||
date: new Date(post.meta.date),
|
||||
author: [{ name: post.meta.author.name }],
|
||||
guid: post.meta.slug,
|
||||
image: `${metaTags.url}${post.meta.coverImage}`,
|
||||
});
|
||||
});
|
||||
|
||||
return new NextResponse(feed.rss2(), {
|
||||
headers: {
|
||||
"Content-Type": "application/xml",
|
||||
"Cache-Control": "public, s-maxage=3600, stale-while-revalidate=86400",
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -41,7 +41,7 @@ const commitMono = localFont({
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const metaTags = {
|
||||
export const metaTags = {
|
||||
title: "garden computing",
|
||||
description:
|
||||
"Computers are magic. So why do we put up with so much complexity? We believe just a few new ideas can make all the difference.",
|
||||
@@ -70,6 +70,16 @@ export const metadata: Metadata = {
|
||||
},
|
||||
],
|
||||
},
|
||||
alternates: {
|
||||
canonical: metaTags.url,
|
||||
types: {
|
||||
"application/rss+xml": `${
|
||||
process.env.VERCEL_URL
|
||||
? `https://${process.env.VERCEL_URL}`
|
||||
: "http://localhost:3000"
|
||||
}/api/rss`,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
|
||||
@@ -49,7 +49,7 @@ function highlightPlugin() {
|
||||
token.content.includes("// old"),
|
||||
);
|
||||
const isBinnedLine = line.some((token) =>
|
||||
token.content.includes("// *bin*"),
|
||||
token.content.includes("// [!code --]"),
|
||||
);
|
||||
const isHighlighted = line.some((token) =>
|
||||
token.content.includes("// *highlight*"),
|
||||
@@ -70,7 +70,7 @@ function highlightPlugin() {
|
||||
line
|
||||
.map((token) => {
|
||||
let color = isHighlighted ? "currentColor" : token.color;
|
||||
return `<span style="color: ${color};${isSubduedLine ? "opacity: 0.4;" : ""}">${escape(token.content.replace("// old", "").replace("// *bin*", "").replace("// *highlight*", ""))}</span>`;
|
||||
return `<span style="color: ${color};${isSubduedLine ? "opacity: 0.4;" : ""}">${escape(token.content.replace("// old", "").replace("// [!code --]", "").replace("// *highlight*", ""))}</span>`;
|
||||
})
|
||||
.join("") +
|
||||
"</span>"
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"@vercel/analytics": "^1.3.1",
|
||||
"@vercel/speed-insights": "^1.0.12",
|
||||
"clsx": "^2.1.1",
|
||||
"feed": "^4.2.2",
|
||||
"gcmp-design-system": "workspace:*",
|
||||
"lucide-react": "^0.436.0",
|
||||
"mdast-util-from-markdown": "^2.0.0",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { TocProvider } from "@/components/TocProvider";
|
||||
import { ApiNav } from "@/components/docs/ApiNav";
|
||||
import DocsLayout from "@/components/docs/DocsLayout";
|
||||
import { Prose } from "gcmp-design-system/src/app/components/molecules/Prose";
|
||||
@@ -8,8 +9,10 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<DocsLayout nav={<ApiNav />} navIcon="package" navName="API Ref">
|
||||
<Prose className="overflow-x-hidden lg:flex-1 py-10">{children}</Prose>
|
||||
</DocsLayout>
|
||||
<TocProvider>
|
||||
<DocsLayout nav={<ApiNav />} navIcon="package" navName="API Ref">
|
||||
<Prose className="overflow-x-hidden lg:flex-1 py-10">{children}</Prose>
|
||||
</DocsLayout>
|
||||
</TocProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import DocsLayout from "@/components/docs/DocsLayout";
|
||||
import { DocNav } from "@/components/docs/nav";
|
||||
import { Prose } from "gcmp-design-system/src/app/components/molecules/Prose";
|
||||
|
||||
export default function Layout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<DocsLayout nav={<DocNav />}>
|
||||
<Prose className="max-w-3xl mx-auto lg:flex-1 py-10">{children}</Prose>
|
||||
</DocsLayout>
|
||||
);
|
||||
}
|
||||
@@ -331,7 +331,7 @@ This works because CoValues
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
const unsub = issue.subscribe([], (updatedIssue) => console.log(updatedIssue));
|
||||
const unsub = issue.subscribe({ resolve: true }, (updatedIssue) => console.log(updatedIssue));
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
@@ -339,7 +339,7 @@ This works because CoValues
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
const unsub = Issue.subscribe(issueID, me, [], (updatedIssue) => {
|
||||
const unsub = Issue.subscribe(issueID, me, { resolve: true }, (updatedIssue) => {
|
||||
console.log(updatedIssue);
|
||||
});
|
||||
```
|
||||
@@ -358,7 +358,7 @@ This works because CoValues
|
||||
function useCoState<V extends CoValue>(Schema: CoValueClass<V>, id?: ID<V>): V | undefined {
|
||||
const [value, setValue] = useState<V>();
|
||||
|
||||
useEffect(() => Schema.subscribe(id, [], setValue), [id]);
|
||||
useEffect(() => Schema.subscribe(id, { resolve: true }, setValue), [id]);
|
||||
|
||||
return value;
|
||||
}
|
||||
@@ -454,7 +454,7 @@ function App() { // old
|
||||
const issue = useCoState(Issue, issueID); // old
|
||||
// old
|
||||
const createIssue = () => { // old
|
||||
const group = Group.create({ owner: me });
|
||||
const group = Group.create();
|
||||
group.addMember("everyone", "writer");
|
||||
// old
|
||||
const newIssue = Issue.create( // old
|
||||
@@ -543,7 +543,7 @@ function App() { // old
|
||||
(window.location.search?.replace("?project=", "") || undefined) as ID<Project> | undefined
|
||||
);
|
||||
// old
|
||||
const issue = useCoState(Issue, issueID); // *bin*
|
||||
const issue = useCoState(Issue, issueID); // [!code --]
|
||||
// old
|
||||
const createProject = () => {
|
||||
const group = Group.create();
|
||||
@@ -605,6 +605,8 @@ export function ProjectComponent({ projectID }: { projectID: ID<Project> }) {
|
||||
<button onClick={createAndAddIssue}>Create Issue</button>
|
||||
</div>
|
||||
</div>
|
||||
) : project === null ? (
|
||||
<div>Project not found or access denied</div>
|
||||
) : (
|
||||
<div>Loading project...</div>
|
||||
);
|
||||
@@ -620,7 +622,7 @@ Two things to note here:
|
||||
- We only need to use `useCoState` on the Project, and the nested `ListOfIssues` and each `Issue` will be **automatically loaded and subscribed to when we access them.**
|
||||
- However, because either the `Project`, `ListOfIssues`, or each `Issue` might not be loaded yet, we have to check for them being defined.
|
||||
|
||||
### Precise loading depths
|
||||
### Precise resolve queries
|
||||
|
||||
The load-and-subscribe-on-access is a convenient way to have your rendering drive data loading (including in nested components!) and lets you quickly chuck UIs together without worrying too much about the shape of all data you'll need.
|
||||
|
||||
@@ -635,7 +637,7 @@ import { IssueComponent } from "./Issue.tsx"; // old
|
||||
import { useCoState } from "jazz-react"; // old
|
||||
// old
|
||||
export function ProjectComponent({ projectID }: { projectID: ID<Project> }) {// old
|
||||
const project = useCoState(Project, projectID, { issues: [{}] });
|
||||
const project = useCoState(Project, projectID, { resolve: { issues: { $each: true } } });
|
||||
|
||||
const createAndAddIssue = () => {// old
|
||||
project?.issues.push(Issue.create({
|
||||
@@ -663,7 +665,7 @@ export function ProjectComponent({ projectID }: { projectID: ID<Project> }) {//
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
The loading-depth spec `{ issues: [{}] }` means "in `Project`, load `issues` and load each item in `issues` shallowly". (Since an `Issue` doesn't have any further references, "shallowly" actually means all its properties will be available).
|
||||
The resolve query `{ resolve: { issues: { $each: true } } }` means "in `Project`, load `issues` and load each item in `issues` deeply". (Since an `Issue` doesn't have any further references, "deeply" actually means all its properties will be available).
|
||||
|
||||
- Now, we can get rid of a lot of conditional accesses because we know that once `project` is loaded, `project.issues` and each `Issue` in it will be loaded as well.
|
||||
- This also results in only one rerender and visual update when everything is loaded, which is faster (especially for long lists) and gives you more control over the loading UX.
|
||||
@@ -710,7 +712,7 @@ function App() { // old
|
||||
// old
|
||||
const createProject = () => { // old
|
||||
const group = Group.create(); // old
|
||||
group.addMember("everyone", "writer"); // *bin*
|
||||
group.addMember("everyone", "writer"); // [!code --]
|
||||
// old
|
||||
const newProject = Project.create( // old
|
||||
{ // old
|
||||
@@ -747,7 +749,7 @@ import { createInviteLink } from "jazz-react";
|
||||
// old
|
||||
|
||||
export function ProjectComponent({ projectID }: { projectID: ID<Project> }) {// old
|
||||
const project = useCoState(Project, projectID, { issues: [{}] }); // old
|
||||
const project = useCoState(Project, projectID, { resolve: { issues: { $each: true } } }); // old
|
||||
|
||||
const { me } = useAccount();
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import DocsLayout from "@/components/docs/DocsLayout";
|
||||
import { TocItemsSetter } from "@/components/docs/TocItemsSetter";
|
||||
import ComingSoonPage from "@/components/docs/coming-soon.mdx";
|
||||
import { DocNav } from "@/components/docs/nav";
|
||||
import { docNavigationItems } from "@/lib/docNavigationItems.js";
|
||||
import { Framework, frameworks } from "@/lib/framework";
|
||||
import type { Toc } from "@stefanprobst/rehype-extract-toc";
|
||||
import { Prose } from "gcmp-design-system/src/app/components/molecules/Prose";
|
||||
|
||||
async function getMdxSource(slugPath: string, framework: string) {
|
||||
// Try to import the framework-specific file first
|
||||
@@ -46,7 +44,6 @@ export default async function Page({
|
||||
}: { params: Promise<{ slug: string[]; framework: string }> }) {
|
||||
const { slug, framework } = await params;
|
||||
const slugPath = slug.join("/");
|
||||
const bodyClassName = "overflow-x-hidden lg:flex-1 py-10 max-w-3xl mx-auto";
|
||||
|
||||
try {
|
||||
const mdxSource = await getMdxSource(slugPath, framework);
|
||||
@@ -56,19 +53,17 @@ export default async function Page({
|
||||
const tocItems = (tableOfContents as Toc)?.[0]?.children;
|
||||
|
||||
return (
|
||||
<DocsLayout toc={tocItems} nav={<DocNav />}>
|
||||
<Prose className={bodyClassName}>
|
||||
<Content />
|
||||
</Prose>
|
||||
</DocsLayout>
|
||||
<>
|
||||
<TocItemsSetter items={tocItems} />
|
||||
<Content />
|
||||
</>
|
||||
);
|
||||
} catch (error) {
|
||||
return (
|
||||
<DocsLayout nav={<DocNav />}>
|
||||
<Prose className={bodyClassName}>
|
||||
<ComingSoonPage />
|
||||
</Prose>
|
||||
</DocsLayout>
|
||||
<>
|
||||
<TocItemsSetter items={[]} />
|
||||
<ComingSoonPage />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ declare module "jazz-react" {// old
|
||||
## Resolving CoValues starting at `profile` or `root`
|
||||
|
||||
<ContentByFramework framework="react">
|
||||
To use per-user data in your app, you typically use `useAccount` somewhere in a high-level component, specifying which references to resolve using a depth-spec (see [Subscribing & deep loading](/docs/using-covalues/subscription-and-loading)).
|
||||
To use per-user data in your app, you typically use `useAccount` somewhere in a high-level component, specifying which references to resolve using a resolve query (see [Subscribing & deep loading](/docs/using-covalues/subscription-and-loading)).
|
||||
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
@@ -163,12 +163,12 @@ export class MyAppAccount extends Account {
|
||||
profile = co.ref(MyAppProfile);
|
||||
|
||||
async migrate(this: MyAppAccount, creationProps?: { name: string }) {
|
||||
// we specifically need to check for undefined,
|
||||
// we specifically need to check for undefined,
|
||||
// because the root might simply be not loaded (`null`) yet
|
||||
if (this.root === undefined) {
|
||||
this.root = MyAppRoot.create({
|
||||
// Using a group to set the owner is always a good idea.
|
||||
// This way if in the future we want to share
|
||||
// This way if in the future we want to share
|
||||
// this coValue we can do so easily.
|
||||
myChats: ListOfChats.create([], Group.create()),
|
||||
myContacts: ListOfAccounts.create([], Group.create())
|
||||
@@ -218,8 +218,8 @@ export class MyAppAccount extends Account {
|
||||
root: {},
|
||||
});
|
||||
|
||||
// we specifically need to check for undefined,
|
||||
// because myBookmarks might simply be not loaded (`null`) yet
|
||||
// we specifically need to check for undefined,
|
||||
// because myBookmarks might simply be not loaded (`null`) yet
|
||||
if (root.myBookmarks === undefined) {
|
||||
root.myBookmarks = ListOfBookmarks.create([], Group.create());
|
||||
}
|
||||
@@ -228,9 +228,9 @@ export class MyAppAccount extends Account {
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
{/*
|
||||
TODO: Add best practice: only ever add fields
|
||||
{/*
|
||||
TODO: Add best practice: only ever add fields
|
||||
|
||||
Note: explain and reassure that there will be more guardrails in the future
|
||||
Note: explain and reassure that there will be more guardrails in the future
|
||||
https://github.com/garden-co/jazz/issues/1160
|
||||
*/}
|
||||
|
||||
@@ -29,9 +29,9 @@ Here is what's changed in this release:
|
||||
{/* prettier-ignore */}
|
||||
```tsx
|
||||
<JazzProvider
|
||||
auth={authMethod} // removed // *bin*
|
||||
peer="wss://cloud.jazz.tools/?key=you@example.com" // moved into sync // *bin*
|
||||
sync={{ peer: "wss://cloud.jazz.tools/?key=you@example.com" }} // *add*
|
||||
auth={authMethod} // removed // [!code --]
|
||||
peer="wss://cloud.jazz.tools/?key=you@example.com" // moved into sync // [!code --]
|
||||
sync={{ peer: "wss://cloud.jazz.tools/?key=you@example.com" }} // [!code ++]
|
||||
>
|
||||
<App />
|
||||
</JazzProvider>
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
import { CodeGroup } from '@/components/forMdx'
|
||||
|
||||
export const metadata = { title: "Jazz 0.12.0 - Deeply resolved data" };
|
||||
|
||||
# Jazz 0.12.0 - Deeply resolved data
|
||||
|
||||
Jazz 0.12.0 makes it easier and safer to load nested data. You can now specify exactly which nested data you want to load, and Jazz will check permissions and handle missing data gracefully. This helps catch errors earlier during development and makes your code more reliable.
|
||||
|
||||
## What's new?
|
||||
|
||||
- New resolve API for a more type-safe deep loading
|
||||
- A single, consistent load option for all loading methods
|
||||
- Improved permission checks on deep loading
|
||||
- Easier type safety with the `Resolved` type helper
|
||||
|
||||
## Breaking changes
|
||||
|
||||
### New Resolve API
|
||||
|
||||
We're introducing a new resolve API for deep loading, more friendly to TypeScript, IDE autocompletion and LLMs.
|
||||
|
||||
**Major changes:**
|
||||
|
||||
1. Functions and hooks for loading now take the resolve query as an explicit nested `resolve` prop
|
||||
2. Shallowly loading a collection is now done with `true` instead of `[]` or `{}`
|
||||
|
||||
<CodeGroup>
|
||||
```tsx twoslash
|
||||
// @noErrors: 2451
|
||||
import { CoMap, CoList, co, Account } from "jazz-tools";
|
||||
import { useAccount } from "jazz-react";
|
||||
class AccountRoot extends CoMap { friends = co.ref(ListOfAccounts); }
|
||||
class ListOfAccounts extends CoList.Of(co.ref(Account)) {}
|
||||
class MyAppAccount extends Account { root = co.ref(AccountRoot);}
|
||||
declare module "jazz-react" { interface Register { Account: MyAppAccount; } }
|
||||
// ---cut-before---
|
||||
// Before
|
||||
// @ts-expect-error
|
||||
const { me } = useAccount({ root: { friends: [] } }); // [!code --]
|
||||
|
||||
// After
|
||||
const { me } = useAccount({ // [!code ++]
|
||||
resolve: { root: { friends: true } } // [!code ++]
|
||||
}); // [!code ++]
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
3. For collections, resolving items deeply is now done with a special `$each` key.
|
||||
|
||||
For a `CoList`:
|
||||
|
||||
<CodeGroup>
|
||||
```tsx
|
||||
class ListOfTasks extends CoList.Of(co.ref(Task)) {}
|
||||
|
||||
// Before
|
||||
const tasks = useCoState(ListOfTasks, id, [{}]); // [!code --]
|
||||
|
||||
// After
|
||||
const tasks = useCoState(ListOfTasks, id, { resolve: { $each: true } }); // [!code ++]
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
For a `CoMap.Record`:
|
||||
|
||||
<CodeGroup>
|
||||
```tsx
|
||||
class UsersByUsername extends CoMap.Record(co.ref(MyAppAccount)) {}
|
||||
|
||||
// Before
|
||||
const usersByUsername = useCoState(UsersByUsername, id, [{}]); // [!code --]
|
||||
|
||||
// After
|
||||
const usersByUsername = useCoState(UsersByUsername, id, { // [!code ++]
|
||||
resolve: { $each: true } // [!code ++]
|
||||
}); // [!code ++]
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
Nested loading — note how it's now less terse, but more readable and auto-complete friendly:
|
||||
|
||||
<CodeGroup>
|
||||
```tsx
|
||||
// Before
|
||||
const tasksWithAssigneesAndTheirOrgs = useCoState(ListOfTasks, id, [{ // [!code --]
|
||||
assignees: [{ org: {}}]} // [!code --]
|
||||
]); // [!code --]
|
||||
|
||||
// After
|
||||
const tasksWithAssigneesAndTheirOrgs = useCoState(ListOfTasks, id, { // [!code ++]
|
||||
resolve: { // [!code ++]
|
||||
$each: { // [!code ++]
|
||||
assignees: { // [!code ++]
|
||||
$each: { org: true } // [!code ++]
|
||||
} // [!code ++]
|
||||
} // [!code ++]
|
||||
} // [!code ++]
|
||||
}); // [!code ++]
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### A single, consistent load option
|
||||
|
||||
The new API works across all loading methods, and separating out the resolve query means
|
||||
other options with default values are easier to manage, for example: loading a value as a specific account instead of using the implicit current account:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
// Before
|
||||
Playlist.load(id, otherAccount, { // [!code --]
|
||||
tracks: [], // [!code --]
|
||||
}); // [!code --]
|
||||
|
||||
// After
|
||||
Playlist.load(id, { // [!code ++]
|
||||
loadAs: otherAccount, // [!code ++]
|
||||
resolve: { tracks: true } // [!code ++]
|
||||
}); // [!code ++]
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Improved permission checks on deep loading
|
||||
|
||||
Now `useCoState` will return `null` when the current user lacks permissions to load requested data.
|
||||
|
||||
Previously, `useCoState` would return `undefined` if the current user lacked permissions, making it hard to tell if the value is loading or if it's missing.
|
||||
|
||||
Now `undefined` means that the value is definitely loading, and `null` means that the value is temporarily missing.
|
||||
|
||||
We also have implemented a more granular permission checking, where if an *optional* CoValue cannot be accessed, `useCoState` will return the data stripped of that CoValue.
|
||||
|
||||
**Note:** The state handling around loading and error states will become more detailed and easy-to-handle in future releases, so this is just a small step towards consistency.
|
||||
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
```tsx
|
||||
class ListOfTracks extends CoList.Of(co.optional.ref(Track)) {}
|
||||
// Before (ambiguous states)
|
||||
const value = useCoState(ListOfTracks, id, [{}]); // [!code --]
|
||||
if (value === undefined) return <div>Loading or access denied</div>; // [!code --]
|
||||
if (value === null) return <div>Not found</div>; // [!code --]
|
||||
|
||||
// After
|
||||
const value = useCoState(ListOfTracks, id, { resolve: { $each: true } }); // [!code ++]
|
||||
if (value === undefined) return <div>Loading...</div>; // [!code ++]
|
||||
if (value === null) return <div>Not found or access denied</div>; // [!code ++]
|
||||
|
||||
// This will only show tracks that we have access to and that are loaded.
|
||||
return tracks.map(track => track && <MusicTrack track={track} />);
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## New Features
|
||||
|
||||
### The `Resolved` type helper
|
||||
|
||||
The new `Resolved` type can be used to define what kind of deeply loaded data you expect in your parameters, using the same resolve query syntax as the new loading APIs:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
type PlaylistResolved = Resolved<Playlist, {
|
||||
tracks: { $each: true }
|
||||
}>;
|
||||
|
||||
function TrackListComponent({ playlist }: { playlist: PlaylistResolved }) {
|
||||
// Safe access to resolved tracks
|
||||
return playlist.tracks.map(track => /* ... */);
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
@@ -25,9 +25,9 @@ So we decided to remove `createJazzRNApp` step and to provide the types through
|
||||
import { JazzProvider, useDemoAuth, DemoAuthBasicUI } from "jazz-react-native";
|
||||
import { MyAppAccount } from "./schema";
|
||||
|
||||
// Remove these lines // *bin*
|
||||
const Jazz = createJazzRNApp({ AccountSchema: MyAppAccount }); // *bin*
|
||||
export const { useAccount, useCoState } = Jazz; // *bin*
|
||||
// Remove these lines // [!code --]
|
||||
const Jazz = createJazzRNApp({ AccountSchema: MyAppAccount }); // [!code --]
|
||||
export const { useAccount, useCoState } = Jazz; // [!code --]
|
||||
|
||||
export function JazzAndAuth({ children }: { children: React.ReactNode }) { // old
|
||||
const [auth, state] = useDemoAuth(); // old
|
||||
@@ -66,8 +66,8 @@ This change improves IDE intellisense support and simplifies imports:
|
||||
{/* prettier-ignore */}
|
||||
```tsx
|
||||
// Replace local imports with "jazz-react-native" imports
|
||||
import { useAccount } from "./main"; // *bin*
|
||||
import { useAccount } from "jazz-react-native"; // *add*
|
||||
import { useAccount } from "./main"; // [!code --]
|
||||
import { useAccount } from "jazz-react-native"; // [!code ++]
|
||||
|
||||
export function Hello() {
|
||||
const { me } = useAccount();
|
||||
|
||||
@@ -24,9 +24,9 @@ So we decided to remove `createJazzReactApp` step and to provide the types throu
|
||||
import { JazzProvider, usePasskeyAuth, PasskeyAuthBasicUI } from "jazz-react";
|
||||
import { MyAppAccount } from "./schema";
|
||||
|
||||
// Remove these lines // *bin*
|
||||
const Jazz = createJazzReactApp({ AccountSchema: MyAppAccount }); // *bin*
|
||||
export const { useAccount, useCoState } = Jazz; // *bin*
|
||||
// Remove these lines // [!code --]
|
||||
const Jazz = createJazzReactApp({ AccountSchema: MyAppAccount }); // [!code --]
|
||||
export const { useAccount, useCoState } = Jazz; // [!code --]
|
||||
|
||||
export function JazzAndAuth({ children }: { children: React.ReactNode }) { // old
|
||||
const [passkeyAuth, passKeyState] = usePasskeyAuth({ appName }); // old
|
||||
@@ -37,7 +37,7 @@ So we decided to remove `createJazzReactApp` step and to provide the types throu
|
||||
<JazzProvider
|
||||
auth={passkeyAuth} // old
|
||||
peer="wss://cloud.jazz.tools/?key=you@example.com" // old
|
||||
AccountSchema={MyAppAccount} {/* The custom Account schema is passed here */} // *add*
|
||||
AccountSchema={MyAppAccount} {/* The custom Account schema is passed here */} // [!code ++]
|
||||
>
|
||||
{children} // old
|
||||
</JazzProvider>
|
||||
@@ -47,11 +47,11 @@ So we decided to remove `createJazzReactApp` step and to provide the types throu
|
||||
}
|
||||
|
||||
// Register the Account schema so `useAccount` returns our custom `MyAppAccount`
|
||||
declare module "jazz-react" { // *add*
|
||||
interface Register { // *add*
|
||||
Account: MyAppAccount; // *add*
|
||||
} // *add*
|
||||
} // *add*
|
||||
declare module "jazz-react" { // [!code ++]
|
||||
interface Register { // [!code ++]
|
||||
Account: MyAppAccount; // [!code ++]
|
||||
} // [!code ++]
|
||||
} // [!code ++]
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
@@ -65,8 +65,8 @@ This change improves IDE intellisense support and simplifies imports:
|
||||
{/* prettier-ignore */}
|
||||
```tsx
|
||||
// Replace local imports with "jazz-react" imports
|
||||
import { useAccount } from "./main"; // *bin*
|
||||
import { useAccount } from "jazz-react"; // *add*
|
||||
import { useAccount } from "./main"; // [!code --]
|
||||
import { useAccount } from "jazz-react"; // [!code ++]
|
||||
|
||||
export function Hello() {
|
||||
const { me } = useAccount();
|
||||
|
||||
@@ -32,7 +32,7 @@ So we decided to remove `createJazzApp` step and to provide the types through na
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { Provider } from '$lib/jazz'; // *bin*
|
||||
import { Provider } from '$lib/jazz'; // [!code --]
|
||||
import { JazzProvider } from 'jazz-svelte';
|
||||
|
||||
// Example configuration for authentication and peer connection
|
||||
@@ -62,8 +62,8 @@ So we decided to remove `createJazzApp` step and to provide the types through na
|
||||
```svelte
|
||||
|
||||
<script lang="ts">
|
||||
import { useAccount } from '$lib/jazz'; // *bin*
|
||||
import { useAccount } from 'jazz-svelte'; // *add*
|
||||
import { useAccount } from '$lib/jazz'; // [!code --]
|
||||
import { useAccount } from 'jazz-svelte'; // [!code ++]
|
||||
|
||||
const { me } = useAccount();
|
||||
</script>
|
||||
|
||||
@@ -28,10 +28,10 @@ import App from "./App.vue"; // old
|
||||
import router from "./router"; // old
|
||||
import { ToDoAccount } from "./schema"; // old
|
||||
|
||||
// Remove these lines // *bin*
|
||||
const Jazz = createJazzVueApp<ToDoAccount>({ AccountSchema: ToDoAccount }); // *bin*
|
||||
export const { useAccount, useCoState } = Jazz; // *bin*
|
||||
const { JazzProvider } = Jazz; // *bin*
|
||||
// Remove these lines // [!code --]
|
||||
const Jazz = createJazzVueApp<ToDoAccount>({ AccountSchema: ToDoAccount }); // [!code --]
|
||||
export const { useAccount, useCoState } = Jazz; // [!code --]
|
||||
const { JazzProvider } = Jazz; // [!code --]
|
||||
|
||||
const RootComponent = defineComponent({ // old
|
||||
name: "RootComponent", // old
|
||||
@@ -86,8 +86,8 @@ This change improves IDE intellisense support and simplifies imports:
|
||||
|
||||
<script setup lang="ts">
|
||||
// Replace local imports with "jazz-vue" imports
|
||||
import { useAccount } from "./main"; // *bin*
|
||||
import { useAccount } from "jazz-vue"; // *add*
|
||||
import { useAccount } from "./main"; // [!code --]
|
||||
import { useAccount } from "jazz-vue"; // [!code ++]
|
||||
|
||||
const { me, logOut } = useAccount();
|
||||
</script>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user