Compare commits

..

43 Commits

Author SHA1 Message Date
Marina Orlova
0589bf275f Add config for cojson 2025-01-08 10:37:12 +01:00
Marina Orlova
2f6ca4cdf4 Sync if not found in peer 2025-01-07 00:58:39 +01:00
Marina Orlova
cfbe745de3 add todo 2025-01-06 13:42:23 +01:00
Marina Orlova
7b96fd719b Fix peers bug 2025-01-05 02:14:56 +01:00
Marina Orlova
c391180093 Add queue runner 2025-01-05 01:12:32 +01:00
Marina Orlova
8ea0858571 Fix build errors 2025-01-04 12:18:37 +01:00
Marina Orlova
d1c0981024 add anaware peers 2025-01-04 11:52:06 +01:00
Marina Orlova
bb0158ec51 Fix dependencies not loaing from another peer 2025-01-03 15:53:22 +01:00
Marina Orlova
8a40c02c52 Fix unloading dependencies 2025-01-02 23:58:43 +01:00
Marina Orlova
b95937511f Send content fix 2025-01-01 22:21:09 +01:00
Marina Orlova
1967c736c5 Add dependency service 2024-12-31 21:46:43 +01:00
Marina Orlova
b84edecb50 Fix data handler for dependencies 2024-12-31 14:34:08 +01:00
Marina Orlova
5631d53e3f Order initial sync messages by dependencies 2024-12-31 01:07:59 +01:00
Marina Orlova
244fd84a88 Send dependencies when unknown covalue requested 2024-12-30 21:55:36 +01:00
Marina Orlova
d4e93afdf9 dont sync from the data action 2024-12-29 23:04:55 +01:00
Marina Orlova
26d4fa985c SQLite selects protocol conditionally 2024-12-29 22:41:57 +01:00
Marina Orlova
cefc6e27de Copy entry upload state when entry is copied 2024-12-29 21:04:29 +01:00
Marina Orlova
452c284a01 Fix sync bugs 2024-12-28 23:10:06 +01:00
Marina Orlova
191a7f33b1 Refactor storage and storage-sqlite 2024-12-27 22:19:15 +01:00
Marina Orlova
d7be246f75 tiny fixes 2024-12-27 18:20:13 +01:00
Marina Orlova
5c8717543c tweaks 2024-12-26 23:11:59 +01:00
Marina Orlova
02cd6fe4b7 Fix Imports 2024-12-26 22:17:30 +01:00
Marina Orlova
526a26a39d Delete subscriptionManager 2024-12-26 21:34:00 +01:00
Marina Orlova
60adbffc26 Refactor sync.ts 2024-12-26 21:30:34 +01:00
Marina Orlova
d8cabe3fa6 Introduce message handlers 2024-12-26 15:14:44 +01:00
Marina Orlova
928ac67a06 Tweaks 2024-12-23 23:11:24 +01:00
Marina Orlova
0458e12721 Refactor handle pull 2024-12-23 20:56:57 +01:00
Marina Orlova
df59b53000 Move all response logic into PeerOperations 2024-12-23 17:54:08 +01:00
Marina Orlova
891baf2053 Move load logic into sync 2024-12-23 11:52:02 +01:00
Marina Orlova
3a55c8a627 Move getServerAndStorage to Peers 2024-12-22 20:59:26 +01:00
Marina Orlova
5a9770242f Create Peers class 2024-12-22 20:48:50 +01:00
Marina Orlova
f6bbe18a53 Rearrange logic between local node and sync 2024-12-21 15:15:52 +01:00
Marina Orlova
0712546277 Move peers tracking into node 2024-12-20 23:28:56 +01:00
Marina Orlova
14b70aa445 add peer ops and refactor sync to make use of them 2024-12-20 22:48:13 +01:00
Marina Orlova
47275a1340 Track upload state in coValueState 2024-12-19 20:03:39 +01:00
Marina Orlova
ca54b4c1a8 tweaks 2024-12-15 22:42:03 +01:00
Marina Orlova
86a2c914d3 implement all actions 2024-12-15 09:05:53 +01:00
Marina Orlova
d9c250386e replace content with push 2024-12-15 02:20:03 +01:00
Marina Orlova
73d5f18cb8 handlePush instead handleLoad 2024-12-15 01:56:53 +01:00
Marina Orlova
84e17a9189 load from peers with pull 2024-12-14 23:57:11 +01:00
Marina Orlova
0e8b04579a rebase storage on new actions 2024-12-14 23:16:52 +01:00
Marina Orlova
56a967cce6 Get rid of peer states 2024-12-14 22:06:13 +01:00
Marina Orlova
f823b2a307 get rid of statefullness 2024-12-14 21:18:51 +01:00
288 changed files with 5244 additions and 8755 deletions

View File

@@ -0,0 +1,5 @@
---
"cojson": patch
---
Apply time travel when checking the roles on a parent group

View File

@@ -13,7 +13,7 @@ jobs:
continue-on-error: true
strategy:
matrix:
project: ["tests/e2e", "examples/chat", "examples/file-share-svelte", "examples/form", "examples/music-player", "examples/pets", "examples/onboarding"]
project: ["tests/e2e", "examples/chat", "examples/music-player", "examples/pets", "examples/onboarding"]
steps:
- uses: actions/checkout@v3

View File

@@ -41,7 +41,7 @@ jobs:
run: pnpm install --frozen-lockfile
- name: Pnpm Build
run: pnpm turbo build --filter="./packages/*"
run: pnpm turbo build
- name: Unit Tests
run: pnpm test:ci

1
.gitignore vendored
View File

@@ -5,7 +5,6 @@ docsTmp
.DS_Store
.turbo
coverage
.direnv
# Next.js
**/.next

View File

@@ -1 +1 @@
22
20

1
.nvmrc
View File

@@ -1 +0,0 @@
22

View File

@@ -25,53 +25,7 @@
"linter": {
"enabled": false,
"rules": {
"recommended": true,
"correctness": {
"useImportExtensions": {
"level": "error",
"options": {
"suggestedExtensions": {
"ts": {
"module": "js",
"component": "jsx"
}
}
}
}
}
"recommended": true
}
},
"overrides": [
{
"include": ["packages/**/src/**"],
"linter": {
"enabled": true,
"rules": {
"recommended": false
}
}
},
{
"include": ["packages/**/src/tests/**", "packages/**/src/test/**"],
"linter": {
"rules": {
"correctness": {
"useImportExtensions": "off"
}
}
}
},
{
"include": ["packages/cojson-storage-indexeddb/**"],
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"suspicious": {
"noExplicitAny": "info"
}
}
}
}
]
}
}

View File

@@ -1,22 +1,5 @@
# jazz-example-book-shelf
## 0.1.31
### Patch Changes
- Updated dependencies [249eecb]
- jazz-tools@0.8.39
- jazz-browser-media-images@0.8.39
- jazz-react@0.8.39
## 0.1.30
### Patch Changes
- jazz-react@0.8.38
- jazz-tools@0.8.38
- jazz-browser-media-images@0.8.38
## 0.1.29
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "jazz-example-book-shelf",
"version": "0.1.31",
"version": "0.1.29",
"private": true,
"scripts": {
"dev": "next dev",
@@ -11,19 +11,19 @@
},
"dependencies": {
"clsx": "^2.0.0",
"jazz-browser-media-images": "workspace:0.8.39",
"jazz-react": "workspace:0.8.39",
"jazz-tools": "workspace:0.8.39",
"jazz-browser-media-images": "workspace:0.8.37",
"jazz-react": "workspace:0.8.37",
"jazz-tools": "workspace:0.8.37",
"next": "14.2.5",
"react": "^18.3.1",
"react-dom": "^18.3.1"
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/node": "^22.5.1",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@types/react": "^18.2.19",
"@types/react-dom": "^18.2.7",
"postcss": "^8.4.27",
"tailwindcss": "^3.4.9",
"typescript": "~5.6.2"
"tailwindcss": "3.3.2",
"typescript": "^5.3.3"
}
}

View File

@@ -1,25 +1,5 @@
# chat-rn-clerk
## 1.0.31
### Patch Changes
- Updated dependencies [0c6b0f3]
- Updated dependencies [249eecb]
- jazz-react-native@0.8.39
- jazz-tools@0.8.39
- jazz-react-native-auth-clerk@0.8.39
- jazz-react-native-media-images@0.8.39
## 1.0.30
### Patch Changes
- jazz-react-native@0.8.38
- jazz-react-native-auth-clerk@0.8.38
- jazz-tools@0.8.38
- jazz-react-native-media-images@0.8.38
## 1.0.29
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "chat-rn-clerk",
"main": "index.js",
"version": "1.0.31",
"version": "1.0.29",
"scripts": {
"build": "expo export -p ios",
"start": "expo start",
@@ -46,8 +46,8 @@
"jazz-react-native-media-images": "workspace:*",
"jazz-tools": "workspace:*",
"nativewind": "^2.0.11",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-native": "~0.76.3",
"react-native-fetch-api": "^3.0.0",
"react-native-gesture-handler": "~2.20.2",
@@ -70,8 +70,8 @@
"jest": "^29.2.1",
"jest-expo": "~52.0.2",
"react-test-renderer": "18.2.0",
"tailwindcss": "^3.4.9",
"typescript": "~5.6.2"
"tailwindcss": "3.3.2",
"typescript": "^5.3.3"
},
"private": true
}

View File

@@ -1,6 +1,5 @@
import type { Config } from "tailwindcss";
const config: Config = {
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./app/**/*.{js,jsx,ts,tsx}",
"./components/**/*.{js,jsx,ts,tsx}",
@@ -11,5 +10,3 @@ const config: Config = {
},
plugins: [],
};
export default config;

View File

@@ -1,21 +1,5 @@
# chat-rn
## 1.0.29
### Patch Changes
- Updated dependencies [0c6b0f3]
- Updated dependencies [249eecb]
- jazz-react-native@0.8.39
- jazz-tools@0.8.39
## 1.0.28
### Patch Changes
- jazz-react-native@0.8.38
- jazz-tools@0.8.38
## 1.0.27
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "chat-rn",
"version": "1.0.29",
"version": "1.0.27",
"main": "index.js",
"scripts": {
"build": "expo export -p ios",
@@ -14,8 +14,8 @@
"dependencies": {
"@azure/core-asynciterator-polyfill": "^1.0.2",
"@react-native-community/netinfo": "^11.4.1",
"@react-navigation/native": "^7.0.13",
"@react-navigation/native-stack": "^7.1.14",
"@react-navigation/native": "^6.1.18",
"@react-navigation/native-stack": "^6.11.0",
"base-64": "^1.0.0",
"clsx": "^2.0.0",
"expo": "^52.0.0",
@@ -31,7 +31,7 @@
"jazz-react-native": "workspace:*",
"jazz-tools": "workspace:*",
"nativewind": "^2.0.11",
"react": "^18.3.1",
"react": "18.2.0",
"react-native": "~0.76.3",
"react-native-fetch-api": "^3.0.0",
"react-native-get-random-values": "^1.11.0",
@@ -45,8 +45,8 @@
"devDependencies": {
"@babel/core": "^7.20.0",
"@types/react": "^18.3.12",
"tailwindcss": "^3.4.9",
"typescript": "~5.6.2"
"tailwindcss": "3.3.2",
"typescript": "^5.3.3"
},
"private": true
}

View File

@@ -0,0 +1,12 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./app/**/*.{js,jsx,ts,tsx}",
"./components/**/*.{js,jsx,ts,tsx}",
"./src/**/*.{js,jsx,ts,tsx}",
],
theme: {
extend: {},
},
plugins: [],
};

View File

@@ -1,23 +1,5 @@
# chat-vue
## 0.0.22
### Patch Changes
- Updated dependencies [e386f2b]
- Updated dependencies [249eecb]
- jazz-browser@0.8.39
- jazz-tools@0.8.39
- jazz-vue@0.8.39
## 0.0.21
### Patch Changes
- jazz-browser@0.8.38
- jazz-tools@0.8.38
- jazz-vue@0.8.38
## 0.0.20
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "chat-vue",
"version": "0.0.22",
"version": "0.0.20",
"private": true,
"type": "module",
"scripts": {
@@ -25,14 +25,14 @@
"@vitejs/plugin-vue": "^5.1.4",
"@vitejs/plugin-vue-jsx": "^4.0.1",
"@vue/tsconfig": "^0.5.1",
"autoprefixer": "^10.4.20",
"eslint": "^9.7.0",
"autoprefixer": "^10.4.14",
"eslint": "^8.46.0",
"eslint-plugin-vue": "^9.28.0",
"npm-run-all2": "^6.2.3",
"postcss": "^8.4.27",
"tailwindcss": "^3.4.9",
"typescript": "~5.6.2",
"vite": "^5.4.10",
"tailwindcss": "3.3.2",
"typescript": "^5.3.3",
"vite": "^5.0.10",
"vite-plugin-vue-devtools": "^7.4.6",
"vue-tsc": "^2.1.6"
}

View File

@@ -0,0 +1,8 @@
/** @type {import('tailwindcss').Config} */
export default {
purge: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"],
theme: {
extend: {},
},
plugins: [],
};

View File

@@ -1,11 +0,0 @@
import type { Config } from "tailwindcss";
const config: Config = {
content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"],
theme: {
extend: {},
},
plugins: [],
};
export default config;

View File

@@ -1,26 +1,5 @@
# jazz-example-chat
## 0.0.117
### Patch Changes
- Updated dependencies [249eecb]
- Updated dependencies [3121551]
- jazz-tools@0.8.39
- cojson@0.8.39
- jazz-browser-media-images@0.8.39
- jazz-react@0.8.39
## 0.0.116
### Patch Changes
- Updated dependencies [b00ee91]
- Updated dependencies [f488c09]
- cojson@0.8.38
- jazz-react@0.8.38
- jazz-tools@0.8.38
## 0.0.115
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-chat",
"private": true,
"version": "0.0.117",
"version": "0.0.115",
"type": "module",
"scripts": {
"dev": "vite",
@@ -18,15 +18,14 @@
"@radix-ui/react-toast": "^1.1.4",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"cojson": "workspace:0.8.39",
"cojson": "workspace:0.8.37",
"hash-slash": "workspace:0.2.1",
"jazz-browser-media-images": "workspace:0.8.39",
"jazz-react": "workspace:0.8.39",
"jazz-tools": "workspace:0.8.39",
"jazz-react": "workspace:0.8.37",
"jazz-tools": "workspace:0.8.37",
"lucide-react": "^0.274.0",
"qrcode": "^1.5.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router": "^6.16.0",
"react-router-dom": "^6.16.0",
"react-use": "^17.4.0",
@@ -37,14 +36,14 @@
"devDependencies": {
"@playwright/test": "^1.46.1",
"@types/qrcode": "^1.5.1",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@types/react": "^18.2.19",
"@types/react-dom": "^18.2.7",
"@vitejs/plugin-react-swc": "^3.3.2",
"autoprefixer": "^10.4.20",
"autoprefixer": "^10.4.14",
"is-ci": "^3.0.1",
"postcss": "^8.4.27",
"tailwindcss": "^3.4.9",
"typescript": "~5.6.2",
"vite": "^5.4.10"
"tailwindcss": "3.3.2",
"typescript": "^5.3.3",
"vite": "^5.0.10"
}
}

View File

@@ -18,7 +18,7 @@ export function App() {
router.navigate("/#/chat/" + chat.id);
// for https://jazz.tools marketing site demo only
onChatLoad(chat);
onChatLoad(chat, me);
};
return (

View File

@@ -1,54 +1,21 @@
import { createImage } from "jazz-browser-media-images";
import { ID } from "jazz-tools";
import { useState } from "react";
import { useAccount, useCoState } from "./main.tsx";
import { useCoState } from "./main.tsx";
import { Chat, Message } from "./schema.ts";
import {
BubbleBody,
BubbleContainer,
BubbleImage,
BubbleInfo,
BubbleText,
ChatBody,
ChatInput,
EmptyChatMessage,
ImageInput,
InputBar,
TextInput,
} from "./ui.tsx";
export function ChatScreen(props: { chatID: ID<Chat> }) {
const chat = useCoState(Chat, props.chatID, [{}]);
const { me } = useAccount();
const [showNLastMessages, setShowNLastMessages] = useState(30);
if (!chat)
return (
<div className="flex-1 flex justify-center items-center">Loading...</div>
);
const sendImage = (event: React.ChangeEvent<HTMLInputElement>) => {
if (!me?.profile) return;
const file = event.currentTarget.files?.[0];
if (!file) return;
if (file.size > 5000000) {
alert("Please upload an image less than 5MB.");
return;
}
createImage(file, { owner: chat._owner }).then((image) => {
chat.push(
Message.create(
{ text: file.name, image: image },
{ owner: chat._owner },
),
);
});
};
return (
return chat ? (
<>
<ChatBody>
{chat.length > 0 ? (
@@ -68,31 +35,24 @@ export function ChatScreen(props: { chatID: ID<Chat> }) {
</button>
)}
</ChatBody>
<InputBar>
<ImageInput onImageChange={sendImage} />
<TextInput
onSubmit={(text) => {
chat.push(Message.create({ text }, { owner: chat._owner }));
}}
/>
</InputBar>
<ChatInput
onSubmit={(text) => {
chat.push(Message.create({ text }, { owner: chat._owner }));
}}
/>
</>
) : (
<div className="flex-1 flex justify-center items-center">Loading...</div>
);
}
function ChatBubble(props: { msg: Message }) {
const lastEdit = props.msg._edits.text;
const fromMe = lastEdit.by?.isMe;
const { text, image } = props.msg;
return (
<BubbleContainer fromMe={fromMe}>
<BubbleBody fromMe={fromMe}>
{image && <BubbleImage image={image} />}
<BubbleText text={text} />
</BubbleBody>
<BubbleBody fromMe={fromMe}>{props.msg.text}</BubbleBody>
<BubbleInfo by={lastEdit.by?.profile?.name} madeAt={lastEdit.madeAt} />
</BubbleContainer>
);

View File

@@ -1,8 +1,7 @@
import { CoList, CoMap, ImageDefinition, co } from "jazz-tools";
import { CoList, CoMap, co } from "jazz-tools";
export class Message extends CoMap {
text = co.string;
image = co.optional.ref(ImageDefinition);
}
export class Chat extends CoList.Of(co.ref(Message)) {}

View File

@@ -1,8 +1,5 @@
import clsx from "clsx";
import { ProgressiveImg } from "jazz-react";
import { ImageDefinition } from "jazz-tools";
import { ImageIcon } from "lucide-react";
import { useId, useRef } from "react";
import { useId } from "react";
export function AppContainer(props: { children: React.ReactNode }) {
return (
@@ -59,7 +56,7 @@ export function BubbleBody(props: {
<div
className={clsx(
"line-clamp-10 text-ellipsis whitespace-pre-wrap",
"rounded-2xl overflow-hidden max-w-[calc(100%-5rem)] shadow-sm p-1",
"rounded-2xl max-w-full py-1 px-3 shadow-sm",
props.fromMe
? "bg-white dark:bg-stone-700 dark:text-white"
: "bg-blue text-white",
@@ -70,23 +67,6 @@ export function BubbleBody(props: {
);
}
export function BubbleText(props: { text: string }) {
return <p className="px-2 leading-relaxed">{props.text}</p>;
}
export function BubbleImage(props: { image: ImageDefinition }) {
return (
<ProgressiveImg image={props.image}>
{({ src }) => (
<img
className="h-auto max-h-[20rem] max-w-full rounded-t-xl mb-1"
src={src}
/>
)}
</ProgressiveImg>
);
}
export function BubbleInfo(props: { by: string | undefined; madeAt: Date }) {
return (
<div className="text-xs text-neutral-500 mt-1.5">
@@ -95,59 +75,17 @@ export function BubbleInfo(props: { by: string | undefined; madeAt: Date }) {
);
}
export function InputBar(props: { children: React.ReactNode }) {
return (
<div className="p-3 bg-white border-t shadow-2xl mt-auto flex gap-1 dark:bg-transparent dark:border-stone-800">
{props.children}
</div>
);
}
export function ImageInput({
onImageChange,
}: { onImageChange?: (event: React.ChangeEvent<HTMLInputElement>) => void }) {
const inputRef = useRef<HTMLInputElement>(null);
const onUploadClick = () => {
inputRef.current?.click();
};
return (
<>
<button
type="button"
aria-label="Send image"
title="Send image"
onClick={onUploadClick}
className="text-stone-500 p-1.5 rounded-full hover:bg-stone-100 hover:text-stone-800 dark:hover:bg-stone-800 dark:hover:text-stone-200 transition-colors"
>
<ImageIcon size={24} strokeWidth={1.5} />
</button>
<label className="sr-only">
Image
<input
ref={inputRef}
type="file"
accept="image/png, image/jpeg, image/gif"
onChange={onImageChange}
/>
</label>
</>
);
}
export function TextInput(props: { onSubmit: (text: string) => void }) {
export function ChatInput(props: { onSubmit: (text: string) => void }) {
const inputId = useId();
return (
<div className="flex-1">
<div className="p-3 bg-white border-t shadow-2xl mt-auto dark:bg-transparent dark:border-stone-800">
<label className="sr-only" htmlFor={inputId}>
Type a message and press Enter
</label>
<input
id={inputId}
className="rounded-full py-1 px-3 border block w-full placeholder:text-stone-500 dark:bg-black dark:text-white dark:border-stone-700"
className="rounded-full py-2 px-4 border block w-full dark:bg-black dark:text-white dark:border-stone-700"
placeholder="Type a message and press Enter"
maxLength={2048}
onKeyDown={({ key, currentTarget: input }) => {

View File

@@ -2,10 +2,20 @@
// This is NOT needed to make the chat work
import { Chat } from "@/schema.ts";
import { Account, CoValue, ID } from "jazz-tools";
export function onChatLoad(chat: Chat) {
export function waitForUpload(id: ID<CoValue>, me: Account) {
const syncManager = me._raw.core.node.syncManager;
const peers = me._raw.core.node.peers.getAll();
return Promise.all(
peers.map((peer) => syncManager.waitForUploadIntoPeer(peer.id, id)),
);
}
export function onChatLoad(chat: Chat, me: Account) {
if (window.parent) {
chat.waitForSync().then(() => {
waitForUpload(chat.id, me).then(() => {
window.parent.postMessage(
{ type: "chat-load", id: "/chat/" + chat.id },
"*",

View File

@@ -1,8 +1,5 @@
import type { Config } from "tailwindcss";
import animate from "tailwindcss-animate";
const config: Config = {
darkMode: ["class"],
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./pages/**/*.{ts,tsx}",
"./components/**/*.{ts,tsx}",
@@ -19,6 +16,10 @@ const config: Config = {
},
extend: {
colors: {
blue: {
700: "#3313F7",
DEFAULT: "#3313F7",
},
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
@@ -60,21 +61,22 @@ const config: Config = {
},
keyframes: {
"accordion-down": {
from: { height: "0" },
from: { height: 0 },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
to: { height: 0 },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
lineClamp: {
10: "10",
},
},
},
plugins: [animate],
plugins: [require("tailwindcss-animate")],
};
export default config;

View File

@@ -1,22 +1,5 @@
# minimal-auth-clerk
## 0.0.16
### Patch Changes
- Updated dependencies [249eecb]
- jazz-tools@0.8.39
- jazz-react@0.8.39
- jazz-react-auth-clerk@0.8.39
## 0.0.15
### Patch Changes
- jazz-react@0.8.38
- jazz-react-auth-clerk@0.8.38
- jazz-tools@0.8.38
## 0.0.14
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "clerk",
"private": true,
"version": "0.0.16",
"version": "0.0.14",
"type": "module",
"scripts": {
"dev": "vite",
@@ -12,9 +12,9 @@
},
"dependencies": {
"@clerk/clerk-react": "^5.4.1",
"jazz-react": "workspace:*",
"jazz-react-auth-clerk": "workspace:0.8.39",
"jazz-tools": "workspace:*",
"jazz-react": "workspace:*",
"jazz-react-auth-clerk": "workspace:0.8.37",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},

View File

@@ -1,23 +0,0 @@
node_modules
# Output
.output
.vercel
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
playwright-report

View File

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

View File

@@ -1,4 +0,0 @@
# Package Managers
package-lock.json
pnpm-lock.yaml
yarn.lock

View File

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

View File

@@ -1,10 +0,0 @@
# file-share-svelte
## 0.0.2
### Patch Changes
- Updated dependencies [249eecb]
- Updated dependencies [aa21072]
- jazz-tools@0.8.39
- jazz-svelte@0.8.39

View File

@@ -1,56 +0,0 @@
# File share example with Jazz and Svelte
This example app demonstrates how to implement secure file sharing in a Svelte application using Jazz.
## Features
This example showcases how to:
- Upload files securely with end-to-end encryption
- Generate and manage sharing links
- Handle file downloads with decryption
- Manage file access permissions
- Authenticate users using passkeys
## Getting Started
1. Clone the repository:
```sh
git clone https://github.com/garden-co/jazz.git
```
2. Navigate to the example directory:
```sh
cd examples/file-share-svelte
```
3. Install dependencies:
```sh
pnpm install
```
4. Run the development server:
```sh
turbo dev
```
5. Open your browser and visit [http://localhost:5173](http://localhost:5173)
---
Alternatively, you can build and preview the app:
```sh
turbo build
pnpm preview
```
## Learn More
- [Jazz Documentation](https://jazz.tools/docs/svelte)
- [Svelte Documentation](https://svelte.dev)
- [WebAuthn API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API)

View File

@@ -1,33 +0,0 @@
import js from '@eslint/js';
import prettier from 'eslint-config-prettier';
import svelte from 'eslint-plugin-svelte';
import globals from 'globals';
import ts from 'typescript-eslint';
export default ts.config(
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs['flat/recommended'],
prettier,
...svelte.configs['flat/prettier'],
{
languageOptions: {
globals: {
...globals.browser,
...globals.node
}
}
},
{
files: ['**/*.svelte'],
languageOptions: {
parserOptions: {
parser: ts.parser
}
}
},
{
ignores: ['build/', '.svelte-kit/', 'dist/']
}
);

View File

@@ -1,47 +0,0 @@
{
"name": "file-share-svelte",
"version": "0.0.2",
"private": true,
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"format": "prettier --write .",
"lint": "prettier --check . && eslint .",
"format-and-lint": "pnpm run format && pnpm run lint",
"format-and-lint:fix": "pnpm run format --write && pnpm run lint --fix",
"test": "playwright test",
"test:ui": "playwright test --ui"
},
"devDependencies": {
"@sveltejs/adapter-vercel": "^5.5.0",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^4.0.1",
"@types/is-ci": "^3.0.4",
"autoprefixer": "^10.4.20",
"eslint": "^9.7.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.36.0",
"globals": "^15.11.0",
"is-ci": "^3.0.1",
"prettier": "^3.3.2",
"prettier-plugin-svelte": "^3.2.6",
"prettier-plugin-tailwindcss": "^0.6.5",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"tailwindcss": "^3.4.9",
"typescript": "~5.6.2",
"typescript-eslint": "^8.0.0",
"vite": "^5.4.10"
},
"dependencies": {
"@tailwindcss/typography": "^0.5.15",
"jazz-svelte": "workspace:*",
"jazz-tools": "workspace:*",
"lucide-svelte": "^0.463.0",
"svelte-sonner": "^0.3.28"
}
}

View File

@@ -1,53 +0,0 @@
import { defineConfig, devices } from "@playwright/test";
import isCI from "is-ci";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// import dotenv from 'dotenv';
// dotenv.config({ path: path.resolve(__dirname, '.env') });
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: "./tests",
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: isCI,
/* Retry on CI only */
retries: isCI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: isCI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: "http://localhost:5173/",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
permissions: ["clipboard-read", "clipboard-write"],
},
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
],
/* Run your local dev server before starting the tests */
webServer: [
{
command: "pnpm preview --port 5173",
url: "http://localhost:5173/",
reuseExistingServer: !isCI,
},
],
});

View File

@@ -1,6 +0,0 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};

View File

@@ -1,3 +0,0 @@
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';

View File

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

View File

@@ -1,12 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

@@ -1,117 +0,0 @@
<script lang="ts">
import { slide } from 'svelte/transition';
import { SharedFile } from '$lib/schema';
import { FileStream } from 'jazz-tools';
import { File, FileDown, Trash2, Link2 } from 'lucide-svelte';
import { useAccount } from '$lib/jazz';
import { toast } from 'svelte-sonner';
import { formatFileSize } from '$lib/utils';
const {
file,
loading = false,
onDelete
}: {
file: SharedFile;
loading?: boolean;
onDelete: (file: SharedFile) => void;
} = $props();
const { me } = useAccount();
const isAdmin = $derived(me && file._owner?.myRole() === 'admin');
async function downloadFile() {
if (!file._refs.file?.id || !me) {
toast.error('Failed to download file');
return;
}
try {
const fileId = file._refs.file.id;
// Load the file as a blob, can take a while
const blob = await FileStream.loadAsBlob(fileId, me, {});
if (!blob) {
toast.error('Failed to download file');
return;
}
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = file.name;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
toast.success('File downloaded successfully');
} catch (error) {
console.error('Error downloading file:', error);
toast.error('Failed to download file');
}
}
async function shareFile() {
try {
const fileUrl = `${window.location.origin}/file/${file.id}`;
await navigator.clipboard.writeText(fileUrl);
toast.success('Share link copied to clipboard');
} catch (error) {
console.error('Error sharing file:', error);
toast.error('Failed to create share link');
}
}
</script>
<div
class="flex items-center justify-between rounded-lg border border-gray-200 bg-white p-4"
transition:slide={{ duration: 200 }}
>
<div class="flex items-center space-x-4">
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-blue-100 text-blue-600">
<File class="h-6 w-6" />
</div>
<div>
<a href="/file/{file.id}" class="hover:text-blue-600 hover:underline">
<h3 class="font-medium text-gray-900">{file.name}</h3>
</a>
<p class="text-sm text-gray-500">
{isAdmin ? 'Owned by you' : ''} • Uploaded {new Date(
file.createdAt || 0
).toLocaleDateString()}
{formatFileSize(file.size || 0)}
</p>
</div>
</div>
<div class="flex items-center space-x-2">
{#if loading}
<div class="text-sm text-gray-500">Uploading...</div>
{:else}
<button
onclick={downloadFile}
class="rounded-lg p-2 text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700"
aria-label="Download file"
>
<FileDown class="h-5 w-5" />
</button>
{#if isAdmin}
<button
onclick={shareFile}
class="rounded-lg p-2 text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700"
aria-label="Share file"
>
<Link2 class="h-5 w-5" />
</button>
<button
onclick={() => onDelete(file)}
class="rounded-lg p-2 text-gray-500 transition-colors hover:bg-gray-100 hover:text-red-600"
aria-label="Delete file"
>
<Trash2 class="h-5 w-5" />
</button>
{/if}
{/if}
</div>
</div>

View File

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

View File

@@ -1,7 +0,0 @@
import { createJazzApp } from 'jazz-svelte';
import { FileShareAccount } from './schema';
export const { useAccount, useCoState, useAcceptInvite, useAccountOrGuest, Provider } =
createJazzApp({
AccountSchema: FileShareAccount
});

View File

@@ -1,51 +0,0 @@
import { Account, CoList, CoMap, FileStream, Profile, co, Group } from 'jazz-tools';
export class SharedFile extends CoMap {
name = co.string;
file = co.ref(FileStream);
createdAt = co.Date;
uploadedAt = co.Date;
size = co.number;
}
export class FileShareProfile extends Profile {
name = co.string;
}
export class ListOfSharedFiles extends CoList.Of(co.ref(SharedFile)) {}
export class FileShareAccountRoot extends CoMap {
type = co.string;
sharedFiles = co.ref(ListOfSharedFiles);
publicGroup = co.ref(Group);
}
export class FileShareAccount extends Account {
profile = co.ref(FileShareProfile);
root = co.ref(FileShareAccountRoot);
/** The account migration is run on account creation and on every log-in.
* You can use it to set up the account root and any other initial CoValues you need.
*/
async migrate(creationProps?: { name: string }) {
super.migrate(creationProps);
await this._refs.root?.load();
// Initialize root if it doesn't exist
if (!this.root || this.root.type !== 'file-share-account') {
// Create a group that will own all shared files
const publicGroup = Group.create({ owner: this });
publicGroup.addMember('everyone', 'reader');
this.root = FileShareAccountRoot.create(
{
type: 'file-share-account',
sharedFiles: ListOfSharedFiles.create([], { owner: publicGroup }),
publicGroup
},
{ owner: this }
);
}
}
}

View File

@@ -1,22 +0,0 @@
/**
* Formats a file size in bytes to a human readable string
* @param bytes The size in bytes
* @returns A formatted string like "1.5 MB"
*/
export function formatFileSize(bytes: number): string {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;
}
/**
* Generates a temporary file ID based on the file name and creation time
* @param fileName The name of the file
* @param createdAt The creation date
* @returns A unique file ID string
*/
export function generateTempFileId(fileName: string, createdAt: Date): string {
return `file-${fileName}-${createdAt.getTime()}`;
}

View File

@@ -1,32 +0,0 @@
<script lang="ts">
import { Provider } from '$lib/jazz';
import { PasskeyAuthBasicUI, usePasskeyAuth } from 'jazz-svelte';
import { Toaster } from 'svelte-sonner';
import '../app.css';
let { children } = $props();
const auth = usePasskeyAuth({
appName: 'File Share'
});
</script>
<svelte:head>
<title>File Share</title>
</svelte:head>
<Toaster richColors />
{#if auth.state.state === 'ready'}
<div class="fixed inset-0 flex items-center justify-center bg-gray-50/80">
<div class="rounded-lg bg-white p-8 shadow-lg">
<PasskeyAuthBasicUI state={auth.state} />
</div>
</div>
{/if}
{#if auth.current}
<Provider auth={auth.current} peer="wss://cloud.jazz.tools/?key=file-share-svelte@garden.co">
<div class="min-h-screen bg-gray-100">
{@render children()}
</div>
</Provider>
{/if}

View File

@@ -1,145 +0,0 @@
<script lang="ts">
import { useAccount, useCoState } from '$lib/jazz';
import { SharedFile, ListOfSharedFiles } from '$lib/schema';
import { createInviteLink } from 'jazz-svelte';
import { FileStream } from 'jazz-tools';
import FileItem from '$lib/components/FileItem.svelte';
import { SvelteMap } from 'svelte/reactivity';
import { generateTempFileId } from '$lib/utils';
import { CloudUpload } from 'lucide-svelte';
const { me, logOut } = useAccount();
const mySharedFilesId = me?.root?._refs.sharedFiles.id;
const sharedFiles = $derived(useCoState(ListOfSharedFiles, mySharedFilesId, [{}]));
let fileInput: HTMLInputElement;
type PendingSharedFile = {
name: string;
id: string;
createdAt: Date;
};
// Track files that are currently uploading
const uploadingFiles = new SvelteMap<string, PendingSharedFile>();
async function handleFileUpload(event: Event) {
const input = event.target as HTMLInputElement;
const files = input.files;
if (!files || !files.length || !me?.root?.sharedFiles || !me.root.publicGroup) return;
const file = files[0];
const fileName = file.name;
const createdAt = new Date();
const fileId = generateTempFileId(fileName, createdAt);
const tempFile: PendingSharedFile = {
name: fileName,
id: fileId,
createdAt
};
// Add to uploading files
uploadingFiles.set(fileId, tempFile);
try {
const ownership = { owner: me.root.publicGroup };
// Create a FileStream from the uploaded file
const fileStream = await FileStream.createFromBlob(file, ownership);
// Create the shared file entry
const sharedFile = SharedFile.create(
{
name: fileName,
file: fileStream,
createdAt,
uploadedAt: new Date(),
size: file.size
},
ownership
);
// Add the file to the user's files list
me.root.sharedFiles.push(sharedFile);
} finally {
uploadingFiles.delete(fileId);
fileInput.value = ''; // reset input
}
}
async function shareFile(file: SharedFile) {
const inviteLink = createInviteLink(file, 'reader');
await navigator.clipboard.writeText(inviteLink);
alert('Share link copied to clipboard!');
}
async function deleteFile(file: SharedFile) {
if (!me?.root?.sharedFiles || !sharedFiles.current) return;
const index = sharedFiles.current.indexOf(file);
if (index > -1) {
me.root.sharedFiles.splice(index, 1);
}
}
</script>
<div class="min-h-screen bg-gray-50">
<div class="container mx-auto max-w-4xl px-4 py-8">
<div class="mb-12 flex items-center justify-between">
<div>
<h1 class="mb-2 text-4xl font-bold text-gray-900">File Share</h1>
<h2 class="text-xl text-gray-600">Welcome back, {me?.profile?.name}</h2>
</div>
<button
onclick={logOut}
class="rounded-lg bg-red-500 px-6 py-2.5 text-sm font-medium text-white transition-colors hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2"
>
Log Out
</button>
</div>
<!-- Upload Section -->
<div class="mb-8 rounded-xl bg-white p-6 shadow-sm">
<div
class="group relative flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-white p-12 text-center hover:border-blue-500 hover:bg-blue-50"
onclick={() => fileInput.click()}
onkeydown={(e) => e.key === 'Enter' && fileInput.click()}
role="button"
tabindex="0"
>
<CloudUpload class="mb-2 h-8 w-8 text-gray-400 group-hover:text-blue-600" />
<h3 class="mb-1 text-lg font-medium text-gray-900">Upload a new file</h3>
<p class="text-sm text-gray-500">Click to select a file from your computer</p>
<input
type="file"
bind:this={fileInput}
onchange={handleFileUpload}
class="hidden"
accept="*/*"
/>
</div>
</div>
<!-- Files List -->
<div class="space-y-4">
{#if sharedFiles.current}
{#if !(sharedFiles.current.length === 0 && uploadingFiles.size === 0)}
{#each [...sharedFiles.current, ...uploadingFiles.values()] as file (generateTempFileId(file?.name, file?.createdAt))}
<FileItem
{file}
loading={uploadingFiles.has(generateTempFileId(file?.name, file?.createdAt))}
onShare={shareFile}
onDelete={deleteFile}
/>
{/each}
{:else}
<p class="text-center text-gray-500">No files yet</p>
{/if}
{/if}
</div>
</div>
</div>

View File

@@ -1,103 +0,0 @@
<script lang="ts">
import { page } from '$app/stores';
import { useAccount, useCoState } from '$lib/jazz';
import { SharedFile } from '$lib/schema';
import { File, FileDown, Link2 } from 'lucide-svelte';
import type { ID } from 'jazz-tools';
import { FileStream } from 'jazz-tools';
import { toast } from 'svelte-sonner';
const { me } = useAccount();
const fileId = $page.params.fileId;
const file = $state(useCoState(SharedFile, fileId as ID<SharedFile>, {}));
const isAdmin = $derived(me && file.current?._owner?.myRole() === 'admin');
async function downloadFile() {
if (!file.current?._refs.file?.id || !me) {
toast.error('Failed to download file');
return;
}
try {
const fileId = file.current._refs.file.id;
const blob = await FileStream.loadAsBlob(fileId, me, {});
if (!blob) {
toast.error('Failed to download file');
return;
}
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = file.current.name;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
toast.success('File downloaded successfully');
} catch (error) {
console.error('Error downloading file:', error);
toast.error('Failed to download file');
}
}
async function shareFile() {
try {
const fileUrl = `${window.location.origin}/file/${file.current?.id}`;
await navigator.clipboard.writeText(fileUrl);
toast.success('Share link copied to clipboard');
} catch (error) {
console.error('Error sharing file:', error);
toast.error('Failed to copy share link');
}
}
</script>
<svelte:head>
<title>{file.current?.name} | File Share</title>
</svelte:head>
{#if file.current}
<div class="container mx-auto max-w-3xl p-4">
<div class="rounded-lg bg-white p-6 shadow-md">
<div class="mb-6 flex items-center justify-between">
<div class="flex items-center gap-3">
<File class="h-6 w-6" />
<h1 class="text-2xl font-semibold">{file.current.name}</h1>
</div>
<div class="flex gap-2">
{#if isAdmin}
<button
onclick={shareFile}
class="flex items-center gap-2 rounded-md bg-gray-100 px-4 py-2 text-gray-700 hover:bg-gray-200"
>
<Link2 class="h-4 w-4" />
Share
</button>
{/if}
{#if file.current._refs.file}
<button
onclick={downloadFile}
class="flex items-center gap-2 rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
>
<FileDown class="h-4 w-4" />
Download
</button>
{/if}
</div>
</div>
<p class="text-gray-600">
{isAdmin ? 'You own this file' : 'Shared with you'} • Uploaded {new Date(
file.current.createdAt || 0
).toLocaleDateString()}
</p>
</div>
</div>
{:else}
<div class="container mx-auto max-w-3xl p-4">
<div class="rounded-lg bg-white p-6 shadow-md">
<p class="text-gray-600">Loading file...</p>
</div>
</div>
{/if}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

View File

@@ -1,15 +0,0 @@
import adapter from '@sveltejs/adapter-vercel';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://svelte.dev/docs/kit/integrations
// for more information about preprocessors
preprocess: vitePreprocess(),
kit: {
adapter: adapter()
}
};
export default config;

View File

@@ -1,12 +0,0 @@
import typography from '@tailwindcss/typography';
import type { Config } from 'tailwindcss';
export default {
content: ['./src/**/*.{html,js,svelte,ts}'],
theme: {
extend: {}
},
plugins: [typography]
} satisfies Config;

View File

@@ -1 +0,0 @@
This is a test file for e2e testing

View File

@@ -1,82 +0,0 @@
import { expect, test } from '@playwright/test';
import type { BrowserContext } from 'playwright-core';
import path from 'path';
async function mockAuthenticator(context: BrowserContext) {
await context.addInitScript(() => {
Object.defineProperty(window.navigator, 'credentials', {
value: {
...window.navigator.credentials,
create: async () => ({
type: 'public-key',
id: new Uint8Array([1, 2, 3, 4]),
rawId: new Uint8Array([1, 2, 3, 4]),
response: {
clientDataJSON: new Uint8Array([1]),
attestationObject: new Uint8Array([2])
}
}),
get: async () => ({
type: 'public-key',
id: new Uint8Array([1, 2, 3, 4]),
rawId: new Uint8Array([1, 2, 3, 4]),
response: {
authenticatorData: new Uint8Array([1]),
clientDataJSON: new Uint8Array([2]),
signature: new Uint8Array([3])
}
})
},
configurable: true
});
});
}
// Configure the authenticator
test.beforeEach(async ({ context }) => {
// Enable virtual authenticator environment
await mockAuthenticator(context);
});
test('can login with passkey and upload file', async ({ page, browser }) => {
// Navigate to the home page
await page.goto('/');
// Click login and handle the passkey authentication
await page.getByRole('textbox').fill('Capitan Hook');
await page.getByRole('button', { name: "Sign up" }).click();
// Verify successful login by checking for user-specific element
await expect(page.getByText("File Share")).toBeVisible();
// Prepare file upload
const filePath = path.join(import.meta.dirname, 'fixtures/test-file.txt');
// Click upload button and handle file selection
const fileChooserPromise = page.waitForEvent('filechooser');
await page.getByRole('button', { name: /upload|add file/i }).click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(filePath);
// Verify the uploaded file appears in the list
await expect(page.getByText('test-file.txt')).toBeVisible();
await page.getByRole('button', { name: 'Share file' }).click();
const inviteLink = await page.evaluate(() => navigator.clipboard.readText());
// Create a new incognito instance and try to load the shared file
const newContext = await browser.newContext();
await mockAuthenticator(newContext);
const newUserPage = await newContext.newPage();
await newUserPage.goto(`/`);
await newUserPage.getByRole('textbox').fill('Mr. Smee');
await newUserPage.getByRole('button', { name: "Sign up" }).click();
await expect(newUserPage.getByText("File Share")).toBeVisible();
await newUserPage.goto(inviteLink);
await expect(newUserPage.getByText("test-file.txt")).toBeVisible();
});

View File

@@ -1,19 +0,0 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}

View File

@@ -1,6 +0,0 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vitest/config';
export default defineConfig({
plugins: [sveltekit()],
});

View File

@@ -1,27 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
/test-results/
/playwright-report/

View File

@@ -1,50 +0,0 @@
# Form example with Jazz and React
Live version: [https://form-demo.jazz.tools](https://form-demo.jazz.tools)
This is a simple form example that shows you how to make a form for creating and editing a `CoValue`,
called `BubbleTeaOrder`, with fields of different types such
as single-select, multi-select, date, text, and boolean.
To create a new `BubbleTeaOrder`, we create an empty order. Because `BubbleTeaOrder` has some
required fields, we can't create an empty `BubbleTeaOrder` directly.
Instead, we create a `DraftBubbleTeaOrder`,
which has the same structure as `BubbleTeaOrder`, but with all fields set to `optional`.
When the user is ready to submit the order, we treat `DraftBubbleTeaOrder` as a "real order" by
converting it into a `BubbleTeaOrder`.
## Installing & running the example locally
(This requires `pnpm` to be installed, see [https://pnpm.io/installation](https://pnpm.io/installation))
Start by downloading the [jazz repository](https://github.com/garden-co/jazz):
```bash
npx degit gardencmp/jazz jazz
```
Go to the form example directory:
```bash
cd jazz/examples/form
```
Install and build dependencies:
```bash
pnpm i && npx turbo build
```
Start the dev server:
```bash
pnpm dev
```
## Questions / problems / feedback
If you have feedback, let us know on [Discord](https://discord.gg/utDMjHYg42) or open an issue or PR to fix something that seems wrong.
## Configuration: sync server
By default, the example app uses [Jazz Cloud](https://jazz.tools/cloud) (`wss://cloud.jazz.tools`) - so cross-device use, invites and collaboration should just work.
You can also run a local sync server by running `npx jazz-run sync` and adding the query param `?sync=ws://localhost:4200` to the URL of the example app (for example: `http://localhost:5173/?peer=ws://localhost:4200`), or by setting the `sync` parameter of the `<Jazz.Provider>` provider component in [./src/main.tsx](./src/main.tsx).

View File

@@ -1,12 +0,0 @@
<!doctype html>
<html lang="en" class="h-full">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Jazz | Form example</title>
</head>
<body class="h-full flex flex-col bg-white text-stone-700 dark:text-stone-400 dark:bg-stone-925">
<div id="root" class="align-self-center flex-1"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -1,37 +0,0 @@
{
"name": "form",
"private": true,
"version": "0.0.12",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview",
"format-and-lint": "biome check .",
"format-and-lint:fix": "biome check . --write"
},
"dependencies": {
"hash-slash": "workspace:*",
"jazz-browser-media-images": "workspace:*",
"jazz-react": "workspace:*",
"jazz-tools": "workspace:*",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.41.5"
},
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@playwright/test": "^1.46.1",
"@tailwindcss/forms": "^0.5.9",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.3",
"autoprefixer": "^10.4.20",
"is-ci": "^3.0.1",
"globals": "^15.11.0",
"postcss": "^8.4.27",
"tailwindcss": "^3.4.9",
"typescript": "~5.6.2",
"vite": "^5.4.10"
}
}

View File

@@ -1,46 +0,0 @@
import { defineConfig, devices } from "@playwright/test";
import isCI from "is-ci";
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: "./tests",
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: isCI,
/* Retry on CI only */
retries: isCI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: isCI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: "http://localhost:5173/",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
permissions: ["clipboard-read", "clipboard-write"],
},
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
],
/* Run your local dev server before starting the tests */
webServer: [
{
command: "pnpm preview --port 5173",
url: "http://localhost:5173/",
reuseExistingServer: !isCI,
},
],
});

View File

@@ -1,6 +0,0 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@@ -1,40 +0,0 @@
import { useIframeHashRouter } from "hash-slash";
import { ID } from "jazz-tools";
import { CreateOrder } from "./CreateOrder.tsx";
import { EditOrder } from "./EditOrder.tsx";
import { Orders } from "./Orders.tsx";
import { useAccount } from "./main";
import { BubbleTeaOrder } from "./schema.ts";
function App() {
const { me, logOut } = useAccount();
const router = useIframeHashRouter();
return (
<>
<header>
<nav className="container py-2 border-b flex items-center justify-between">
<span>
You're logged in as <strong>{me?.profile?.name}</strong>
</span>
<button
className="bg-stone-100 py-1.5 px-3 text-sm rounded-md dark:bg-stone-900 dark:text-white"
onClick={() => logOut()}
>
Log out
</button>
</nav>
</header>
<main className="container py-8 space-y-8">
{router.route({
"/": () => <Orders />,
"/order": () => <CreateOrder />,
"/order/:id": (id) => <EditOrder id={id as ID<BubbleTeaOrder>} />,
})}
</main>
</>
);
}
export default App;

View File

@@ -1,77 +0,0 @@
import { useIframeHashRouter } from "hash-slash";
import { ID } from "jazz-tools";
import { useState } from "react";
import { Errors } from "./Errors.tsx";
import { LinkToHome } from "./LinkToHome.tsx";
import { OrderForm } from "./OrderForm.tsx";
import { useAccount, useCoState } from "./main.tsx";
import {
BubbleTeaOrder,
DraftBubbleTeaOrder,
ListOfBubbleTeaAddOns,
} from "./schema.ts";
export function CreateOrder() {
const { me } = useAccount({ profile: { draft: {}, orders: [] } });
const router = useIframeHashRouter();
const [errors, setErrors] = useState<string[]>([]);
if (!me?.profile) return;
const onSave = (draft: DraftBubbleTeaOrder) => {
// validate if the draft is a valid order
const validation = draft.validate();
setErrors(validation.errors);
if (validation.errors.length > 0) {
return;
}
// turn the draft into a real order
me.profile.orders.push(draft as BubbleTeaOrder);
// reset the draft
me.profile.draft = DraftBubbleTeaOrder.create(
{
addOns: ListOfBubbleTeaAddOns.create([], { owner: me.profile._owner }),
},
{ owner: me.profile._owner },
);
router.navigate("/");
};
return (
<>
<LinkToHome />
<h1 className="text-lg">
<strong>Make a new bubble tea order 🧋</strong>
</h1>
<Errors errors={errors} />
<CreateOrderForm id={me?.profile?.draft.id} onSave={onSave} />
</>
);
}
function CreateOrderForm({
id,
onSave,
}: {
id: ID<DraftBubbleTeaOrder>;
onSave: (draft: DraftBubbleTeaOrder) => void;
}) {
const draft = useCoState(DraftBubbleTeaOrder, id, {
addOns: [],
});
if (!draft) return;
const addOrder = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
onSave(draft);
};
return <OrderForm order={draft} onSave={addOrder} />;
}

View File

@@ -1,15 +0,0 @@
import { useAccount } from "./main.tsx";
export function DraftIndicator() {
const { me } = useAccount({
profile: { draft: {} },
});
if (me?.profile.draft?.hasChanges) {
return (
<div className="absolute -top-1 -right-1 bg-blue-500 border-2 border-white w-3 h-3 rounded-full dark:border-stone-925">
<span className="sr-only">You have a draft</span>
</div>
);
}
}

View File

@@ -1,26 +0,0 @@
import { ID } from "jazz-tools";
import { LinkToHome } from "./LinkToHome.tsx";
import { OrderForm } from "./OrderForm.tsx";
import { OrderThumbnail } from "./OrderThumbnail.tsx";
import { useCoState } from "./main.tsx";
import { BubbleTeaOrder } from "./schema.ts";
export function EditOrder(props: { id: ID<BubbleTeaOrder> }) {
const order = useCoState(BubbleTeaOrder, props.id, []);
if (!order) return;
return (
<>
<LinkToHome />
<OrderThumbnail order={order} />
<h1 className="text-lg">
<strong>Edit your bubble tea order 🧋</strong>
</h1>
<OrderForm order={order} />
</>
);
}

View File

@@ -1,13 +0,0 @@
export function Errors({ errors }: { errors: string[] }) {
if (errors.length === 0) return null;
return (
<div className="rounded-md bg-red-50 border border-red-50 p-4 text-sm text-red-600 dark:bg-transparent dark:border-red-500 dark:text-red-500">
<ul role="list" className="list-disc space-y-1 pl-5">
{errors.map((error, i) => (
<li key={i}>{error}</li>
))}
</ul>
</div>
);
}

View File

@@ -1,7 +0,0 @@
export function LinkToHome() {
return (
<a href="/#" className="text-sm text-stone-600">
&lt; Back to all orders
</a>
);
}

View File

@@ -1,107 +0,0 @@
import {
BubbleTeaAddOnTypes,
BubbleTeaBaseTeaTypes,
BubbleTeaOrder,
DraftBubbleTeaOrder,
} from "./schema.ts";
export function OrderForm({
order,
onSave,
}: {
order: BubbleTeaOrder | DraftBubbleTeaOrder;
onSave?: (e: React.FormEvent<HTMLFormElement>) => void;
}) {
return (
<form onSubmit={onSave} className="grid gap-5">
<div className="flex flex-col gap-2">
<label htmlFor="baseTea">Base tea</label>
<select
name="baseTea"
id="baseTea"
value={order.baseTea || ""}
className="dark:bg-transparent"
onChange={(e) => (order.baseTea = e.target.value as any)}
required
>
<option value="" disabled>
Please select your preferred base tea
</option>
{BubbleTeaBaseTeaTypes.map((teaType) => (
<option key={teaType} value={teaType}>
{teaType}
</option>
))}
</select>
</div>
<fieldset>
<legend className="mb-2">Add-ons</legend>
{BubbleTeaAddOnTypes.map((addOn) => (
<div key={addOn} className="flex items-center gap-2">
<input
type="checkbox"
value={addOn}
name={addOn}
id={addOn}
checked={order.addOns?.includes(addOn) || false}
onChange={(e) => {
if (e.target.checked) {
order.addOns?.push(addOn);
} else {
order.addOns?.splice(order.addOns?.indexOf(addOn), 1);
}
}}
/>
<label htmlFor={addOn}>{addOn}</label>
</div>
))}
</fieldset>
<div className="flex flex-col gap-2">
<label htmlFor="deliveryDate">Delivery date</label>
<input
type="date"
name="deliveryDate"
id="deliveryDate"
className="dark:bg-transparent"
value={order.deliveryDate?.toISOString().split("T")[0] || ""}
onChange={(e) => (order.deliveryDate = new Date(e.target.value))}
required
/>
</div>
<div className="flex items-center gap-2">
<input
type="checkbox"
name="withMilk"
id="withMilk"
checked={order.withMilk}
onChange={(e) => (order.withMilk = e.target.checked)}
/>
<label htmlFor="withMilk">With milk?</label>
</div>
<div className="flex flex-col gap-2">
<label htmlFor="instructions">Special instructions</label>
<textarea
name="instructions"
id="instructions"
value={order.instructions}
className="dark:bg-transparent"
onChange={(e) => (order.instructions = e.target.value)}
></textarea>
</div>
{onSave && (
<button
type="submit"
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
>
Submit
</button>
)}
</form>
);
}

View File

@@ -1,28 +0,0 @@
import { BubbleTeaOrder } from "./schema.ts";
export function OrderThumbnail({ order }: { order: BubbleTeaOrder }) {
const { id, baseTea, addOns, instructions, deliveryDate, withMilk } = order;
const date = deliveryDate.toLocaleDateString();
return (
<a
href={`/#/order/${id}`}
className="border p-3 flex justify-between items-start gap-3"
>
<div>
<strong>
{baseTea} {withMilk ? "milk " : ""} tea
</strong>
{addOns && addOns?.length > 0 && (
<p className="text-sm text-stone-600">
with {addOns?.join(", ").toLowerCase()}
</p>
)}
{instructions && (
<p className="text-sm text-stone-600 italic">{instructions}</p>
)}
</div>
<div className="text-sm text-stone-600">{date}</div>
</a>
);
}

View File

@@ -1,37 +0,0 @@
import { DraftIndicator } from "./DraftIndicator.tsx";
import { OrderThumbnail } from "./OrderThumbnail.tsx";
import { useAccount } from "./main.tsx";
export function Orders() {
const { me } = useAccount({
profile: { orders: [] },
});
return (
<>
<section className="space-y-5">
<a
href={`/#/order`}
className="block relative p-3 bg-white border text-center rounded-md dark:bg-stone-900"
>
<strong>Add new order</strong>
<DraftIndicator />
</a>
<div className="space-y-3">
<h1 className="text-lg pb-2 border-b mb-3">
<strong>Your orders 🧋</strong>
</h1>
{me?.profile?.orders?.length ? (
me?.profile?.orders.map((order) =>
order ? <OrderThumbnail key={order.id} order={order} /> : null,
)
) : (
<p>You have no orders yet.</p>
)}
</div>
</section>
</>
);
}

View File

@@ -1,9 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer components {
strong {
@apply font-semibold text-stone-900 dark:text-white;
}
}

View File

@@ -1,39 +0,0 @@
import { DemoAuthBasicUI, createJazzReactApp, useDemoAuth } from "jazz-react";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
import { JazzAccount } from "./schema.ts";
const Jazz = createJazzReactApp({
AccountSchema: JazzAccount,
});
export const { useAccount, useCoState } = Jazz;
function JazzAndAuth({ children }: { children: React.ReactNode }) {
const [auth, authState] = useDemoAuth();
return (
<>
<Jazz.Provider
auth={auth}
peer="wss://cloud.jazz.tools/?key=form-example@garden.co"
>
{children}
</Jazz.Provider>
{authState.state !== "signedIn" && (
<DemoAuthBasicUI appName="Form" state={authState} />
)}
</>
);
}
createRoot(document.getElementById("root")!).render(
<StrictMode>
<JazzAndAuth>
<App />
</JazzAndAuth>
</StrictMode>,
);

View File

@@ -1,86 +0,0 @@
import { Account, CoList, CoMap, Profile, co } from "jazz-tools";
export const BubbleTeaAddOnTypes = [
"Pearl",
"Lychee jelly",
"Red bean",
"Brown sugar",
"Taro",
] as const;
export const BubbleTeaBaseTeaTypes = [
"Black",
"Oolong",
"Jasmine",
"Thai",
] as const;
export class ListOfBubbleTeaAddOns extends CoList.Of(
co.literal(...BubbleTeaAddOnTypes),
) {
get hasChanges() {
return Object.entries(this._raw.insertions).length > 0;
}
}
export class BubbleTeaOrder extends CoMap {
baseTea = co.literal(...BubbleTeaBaseTeaTypes);
addOns = co.ref(ListOfBubbleTeaAddOns);
deliveryDate = co.Date;
withMilk = co.boolean;
instructions = co.optional.string;
}
export class DraftBubbleTeaOrder extends CoMap {
baseTea = co.optional.literal(...BubbleTeaBaseTeaTypes);
addOns = co.optional.ref(ListOfBubbleTeaAddOns);
deliveryDate = co.optional.Date;
withMilk = co.optional.boolean;
instructions = co.optional.string;
get hasChanges() {
return Object.keys(this._edits).length > 1 || this.addOns?.hasChanges;
}
// validate if the draft is a valid order
validate() {
const errors: string[] = [];
if (!this.baseTea) {
errors.push("Please select your preferred base tea.");
}
if (!this.deliveryDate) {
errors.push("Plese select a delivery date.");
}
return { errors };
}
}
export class ListOfBubbleTeaOrders extends CoList.Of(co.ref(BubbleTeaOrder)) {}
/** The profile is an app-specific per-user public `CoMap`
* where you can store top-level objects for that user */
export class JazzProfile extends Profile {
draft = co.ref(DraftBubbleTeaOrder);
orders = co.ref(ListOfBubbleTeaOrders);
}
export class JazzAccount extends Account {
profile = co.ref(JazzProfile)!;
migrate(this: JazzAccount, creationProps?: { name: string }) {
super.migrate(creationProps);
if (!this.profile._refs.orders) {
const owner = this.profile._owner;
this.profile.orders = ListOfBubbleTeaOrders.create([], { owner });
this.profile.draft = DraftBubbleTeaOrder.create(
{
addOns: ListOfBubbleTeaAddOns.create([], { owner }),
},
{ owner },
);
}
}
}

View File

@@ -1 +0,0 @@
/// <reference types="vite/client" />

View File

@@ -1,75 +0,0 @@
import formsPlugin from "@tailwindcss/forms";
import type { Config } from "tailwindcss";
import plugin from "tailwindcss/plugin";
const stonePalette = {
50: "oklch(0.988281 0.002 75)",
75: "oklch(0.980563 0.002 75)",
100: "oklch(0.964844 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)",
} as const;
const stonePaletteWithAlpha = { ...stonePalette };
Object.keys(stonePalette).forEach((key) => {
stonePaletteWithAlpha[key] = stonePaletteWithAlpha[key].replace(
")",
"/ <alpha-value>)",
);
});
const config: Config = {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {
colors: {
stone: stonePaletteWithAlpha,
},
container: {
center: true,
padding: {
DEFAULT: "0.75rem",
sm: "1rem",
},
screens: {
md: "500px",
lg: "500px",
xl: "500px",
},
},
},
},
plugins: [
formsPlugin,
plugin(({ addBase }) =>
addBase({
":root": {
"--gcmp-border-color": stonePalette[200],
"--gcmp-invert-border-color": stonePalette[900],
},
"*": {
borderColor: "var(--gcmp-border-color)",
},
"@media (prefers-color-scheme: dark)": {
"*": {
borderColor: "var(--gcmp-invert-border-color)",
},
},
"*:focus": {
outline: "none",
},
}),
),
],
} as const;
export default config;

View File

@@ -1,56 +0,0 @@
import { expect, test } from "@playwright/test";
import { LoginPage } from "./pages/LoginPage";
test("create and edit an order", async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.fillUsername("Alice");
await loginPage.signup();
// start an order
await page.getByRole("link", { name: "Add new order" }).click();
await page.getByLabel("Base tea").selectOption("Oolong");
// test draft indicator
await page.getByRole("link", { name: /Back to all orders/ }).click();
await expect(page.getByText("You have a draft")).toBeVisible();
// fill out the rest of order form
await page.getByRole("link", { name: "Add new order" }).click();
await page.getByLabel("Pearl").check();
await page.getByLabel("Taro").check();
await page.getByLabel("Delivery date").fill("2024-12-21");
await page.getByLabel("With milk?").check();
await page.getByLabel("Special instructions").fill("25% sugar");
await page.getByRole("button", { name: "Submit" }).click();
await page.waitForURL("/");
// the draft indicator should be gone because the order was submitted
await expect(page.getByText("You have a draft")).toHaveCount(0);
// check if order was created correctly
const firstOrder = page.getByRole("link", { name: "Oolong milk tea" });
await expect(firstOrder).toHaveText(/25% sugar/);
await expect(firstOrder).toHaveText(/12\/21\/2024/);
await expect(firstOrder).toHaveText(/with pearl, taro/);
// edit order
await firstOrder.click();
await page.getByLabel("Base tea").selectOption("Jasmine");
await page.getByLabel("Red bean").check();
await page.getByLabel("Brown sugar").check();
await page.getByLabel("Delivery date").fill("2024-12-25");
await page.getByLabel("With milk?").uncheck();
await page.getByLabel("Special instructions").fill("10% sugar");
await page.getByRole("link", { name: /Back to all orders/ }).click();
// check if order was edited correctly
const editedOrder = page.getByRole("link", { name: "Jasmine tea" });
await expect(editedOrder).toHaveText(/10% sugar/);
await expect(editedOrder).toHaveText(/12\/25\/2024/);
await expect(editedOrder).toHaveText(
/with pearl, taro, red bean, brown sugar/,
);
});

View File

@@ -1,40 +0,0 @@
import { Locator, Page, expect } from "@playwright/test";
export class LoginPage {
readonly page: Page;
readonly usernameInput: Locator;
readonly signupButton: Locator;
constructor(page: Page) {
this.page = page;
this.usernameInput = page.getByRole("textbox");
this.signupButton = page.getByRole("button", {
name: "Sign up",
});
}
async goto() {
this.page.goto("/");
}
async fillUsername(value: string) {
await this.usernameInput.clear();
await this.usernameInput.fill(value);
}
async loginAs(value: string) {
await this.page
.getByRole("button", {
name: value,
})
.click();
}
async signup() {
await this.signupButton.click();
}
async expectLoaded() {
await expect(this.signupButton).toBeVisible();
}
}

View File

@@ -1,24 +0,0 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2023", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}

View File

@@ -1,29 +0,0 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2023", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@@ -1,10 +0,0 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View File

@@ -1,7 +0,0 @@
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
});

View File

@@ -1,22 +1,5 @@
# image-upload
## 0.0.14
### Patch Changes
- Updated dependencies [249eecb]
- jazz-tools@0.8.39
- jazz-browser-media-images@0.8.39
- jazz-react@0.8.39
## 0.0.13
### Patch Changes
- jazz-react@0.8.38
- jazz-tools@0.8.38
- jazz-browser-media-images@0.8.38
## 0.0.12
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "image-upload",
"private": true,
"version": "0.0.14",
"version": "0.0.12",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,24 +1,5 @@
# jazz-example-inspector
## 0.0.86
### Patch Changes
- e386f2b: Automatically set up the Cmd+J listener if 'allowJazzInspector' is present in the URL. Cmd+J opens inspector.jazz.tools with the current user's account.
- Updated dependencies [249eecb]
- Updated dependencies [3121551]
- cojson@0.8.39
- cojson-transport-ws@0.8.39
## 0.0.85
### Patch Changes
- Updated dependencies [b00ee91]
- Updated dependencies [f488c09]
- cojson@0.8.38
- cojson-transport-ws@0.8.38
## 0.0.84
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-inspector",
"private": true,
"version": "0.0.86",
"version": "0.0.84",
"type": "module",
"scripts": {
"dev": "vite",
@@ -16,13 +16,13 @@
"@radix-ui/react-toast": "^1.1.4",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"cojson": "workspace:0.8.39",
"cojson-transport-ws": "workspace:0.8.39",
"cojson": "workspace:0.8.37",
"cojson-transport-ws": "workspace:0.8.37",
"hash-slash": "workspace:0.2.1",
"lucide-react": "^0.274.0",
"qrcode": "^1.5.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router": "^6.16.0",
"react-router-dom": "^6.16.0",
"react-use": "^17.4.0",
@@ -32,13 +32,13 @@
},
"devDependencies": {
"@types/qrcode": "^1.5.1",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@types/react": "^18.2.19",
"@types/react-dom": "^18.2.7",
"@vitejs/plugin-react-swc": "^3.3.2",
"autoprefixer": "^10.4.20",
"autoprefixer": "^10.4.14",
"postcss": "^8.4.27",
"tailwindcss": "^3.4.9",
"typescript": "~5.6.2",
"vite": "^5.4.10"
"tailwindcss": "3.3.2",
"typescript": "^5.3.3",
"vite": "^5.0.10"
}
}

View File

@@ -53,14 +53,12 @@ export default function CoJsonViewerApp() {
}, [currentAccount]);
useEffect(() => {
if (!currentAccount && path.length > 0) {
if (!currentAccount) {
setLocalNode(null);
goToIndex(-1);
return;
}
if (!currentAccount) return;
WasmCrypto.create().then(async (crypto) => {
const wsPeer = createWebSocketPeer({
id: "cloud",
@@ -79,7 +77,7 @@ export default function CoJsonViewerApp() {
});
setLocalNode(node);
});
}, [currentAccount, goToIndex, path]);
}, [currentAccount, goToIndex]);
const addAccount = (id: RawAccountID, secret: AgentSecret) => {
const newAccount = { id, secret };
@@ -104,18 +102,6 @@ export default function CoJsonViewerApp() {
}
};
if (
path?.[0]?.coId.toString() === "import" &&
path?.[1]?.coId !== undefined &&
path?.[2]?.coId !== undefined
) {
addAccount(
path?.[1]?.coId as RawAccountID,
atob(path?.[2]?.coId as string) as AgentSecret,
);
goToIndex(-1);
}
return (
<div className="w-full h-screen bg-gray-100 p-4 overflow-hidden flex flex-col">
<div className="flex justify-between items-center mb-4">

View File

@@ -1,7 +1,5 @@
import type { Config } from "tailwindcss";
import animate from "tailwindcss-animate";
const config: Config = {
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./pages/**/*.{ts,tsx}",
"./components/**/*.{ts,tsx}",
@@ -59,12 +57,12 @@ const config: Config = {
},
keyframes: {
"accordion-down": {
from: { height: "0" },
from: { height: 0 },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
to: { height: 0 },
},
},
animation: {
@@ -73,7 +71,5 @@ const config: Config = {
},
},
},
plugins: [animate],
plugins: [require("tailwindcss-animate")],
};
export default config;

View File

@@ -1,20 +1,5 @@
# jazz-example-musicplayer
## 0.0.37
### Patch Changes
- Updated dependencies [249eecb]
- jazz-tools@0.8.39
- jazz-react@0.8.39
## 0.0.36
### Patch Changes
- jazz-react@0.8.38
- jazz-tools@0.8.38
## 0.0.35
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-music-player",
"private": true,
"version": "0.0.37",
"version": "0.0.35",
"type": "module",
"scripts": {
"dev": "vite",
@@ -18,11 +18,11 @@
"@radix-ui/react-toast": "^1.1.4",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"jazz-react": "workspace:0.8.39",
"jazz-tools": "workspace:0.8.39",
"jazz-react": "workspace:0.8.37",
"jazz-tools": "workspace:0.8.37",
"lucide-react": "^0.274.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router": "^6.16.0",
"react-router-dom": "^6.16.0",
"tailwind-merge": "^1.14.0",
@@ -30,13 +30,13 @@
},
"devDependencies": {
"@playwright/test": "^1.46.1",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@types/react": "^18.2.19",
"@types/react-dom": "^18.2.7",
"@vitejs/plugin-react-swc": "^3.3.2",
"autoprefixer": "^10.4.20",
"autoprefixer": "^10.4.14",
"postcss": "^8.4.27",
"tailwindcss": "^3.4.9",
"typescript": "~5.6.2",
"vite": "^5.4.10"
"tailwindcss": "3.3.2",
"typescript": "^5.3.3",
"vite": "^5.0.10"
}
}

View File

@@ -1,7 +1,5 @@
import type { Config } from "tailwindcss";
import animate from "tailwindcss-animate";
const config: Config = {
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: [
"./pages/**/*.{ts,tsx}",
@@ -60,12 +58,12 @@ const config: Config = {
},
keyframes: {
"accordion-down": {
from: { height: "0" },
from: { height: 0 },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
to: { height: 0 },
},
},
animation: {
@@ -74,7 +72,5 @@ const config: Config = {
},
},
},
plugins: [animate],
plugins: [require("tailwindcss-animate")],
};
export default config;

View File

@@ -1,22 +1,5 @@
# jazz-example-onboarding
## 0.0.18
### Patch Changes
- Updated dependencies [249eecb]
- jazz-tools@0.8.39
- jazz-browser-media-images@0.8.39
- jazz-react@0.8.39
## 0.0.17
### Patch Changes
- jazz-react@0.8.38
- jazz-tools@0.8.38
- jazz-browser-media-images@0.8.38
## 0.0.16
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-onboarding",
"private": true,
"version": "0.0.18",
"version": "0.0.16",
"type": "module",
"scripts": {
"dev": "vite",
@@ -17,20 +17,20 @@
"jazz-browser-media-images": "workspace:*",
"jazz-react": "workspace:*",
"jazz-tools": "workspace:*",
"react": "^18.3.1",
"react-dom": "^18.3.1"
"react": "18.3.1",
"react-dom": "18.3.1"
},
"devDependencies": {
"@playwright/test": "^1.46.1",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.3",
"autoprefixer": "^10.4.20",
"is-ci": "^3.0.1",
"jazz-run": "workspace:*",
"@playwright/test": "^1.46.1",
"@types/react": "^18.2.19",
"@types/react-dom": "^18.2.7",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.14",
"postcss": "^8.4.27",
"tailwindcss": "^3.4.9",
"typescript": "~5.6.2",
"vite": "^5.4.10"
"tailwindcss": "^3.3.2",
"typescript": "^5.3.3",
"is-ci": "^3.0.1",
"vite": "^5.0.10"
}
}

View File

@@ -1,11 +1,8 @@
import type { Config } from "tailwindcss";
const config: Config = {
/** @type {import('tailwindcss').Config} */
export default {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {},
},
plugins: [],
};
export default config;

View File

@@ -1,11 +0,0 @@
import type { Config } from "tailwindcss";
const config: Config = {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {},
},
plugins: [],
};
export default config;

View File

@@ -1,18 +1,5 @@
# passkey-svelte
## 0.0.6
### Patch Changes
- Updated dependencies [aa21072]
- jazz-svelte@0.8.39
## 0.0.5
### Patch Changes
- jazz-svelte@0.8.38
## 0.0.4
### Patch Changes

View File

@@ -1,46 +1,38 @@
# Passkey Authentication (Svelte)
# sv
This example app demonstrates how to implement passkey authentication in a Svelte application using Jazz.
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
## Features
## Creating a project
This example showcases how to:
- Set up passkey authentication in a Svelte application
- Handle user registration with passkeys
- Manage authentication state
- Implement secure login/logout flows
If you're seeing this, you've probably already done this step. Congrats!
## Getting Started
```bash
# create a new project in the current directory
npx sv create
1. Clone the repository:
```sh
git clone https://github.com/garden-co/jazz.git
# create a new project in my-app
npx sv create my-app
```
2. Navigate to the example directory:
## Developing
```sh
cd examples/passkey-svelte
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
3. Install dependencies:
## Building
```sh
pnpm install
To create a production version of your app:
```bash
npm run build
```
4. Start the development server:
You can preview the production build with `npm run preview`.
```sh
pnpm dev
```
5. Open your browser and visit [http://localhost:5173](http://localhost:5173)
## Learn More
- [Jazz Documentation](https://jazz.tools/docs/svelte)
- [WebAuthn API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API)
- [Svelte Documentation](https://svelte.dev)
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.

View File

@@ -1,6 +1,6 @@
{
"name": "passkey-svelte",
"version": "0.0.6",
"version": "0.0.4",
"type": "module",
"private": true,
"scripts": {
@@ -17,19 +17,19 @@
"devDependencies": {
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^4.0.1",
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"@types/eslint": "^9.6.0",
"eslint": "^9.7.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.36.0",
"globals": "^15.11.0",
"globals": "^15.0.0",
"prettier": "^3.3.2",
"prettier-plugin-svelte": "^3.2.6",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"typescript": "~5.6.2",
"typescript": "^5.0.0",
"typescript-eslint": "^8.0.0",
"vite": "^5.4.10"
"vite": "^5.0.3"
},
"dependencies": {
"jazz-svelte": "workspace:*"

Some files were not shown because too many files have changed in this diff Show More