Compare commits
2 Commits
authv2-doc
...
worker-sto
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11f1a9d5ba | ||
|
|
91265d62dd |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"cojson": patch
|
||||
---
|
||||
|
||||
Export the coValue loading config to reduce the timeout on tests
|
||||
@@ -1,16 +0,0 @@
|
||||
---
|
||||
"jazz-react-native-auth-clerk": minor
|
||||
"jazz-react-auth-clerk": minor
|
||||
"cojson-transport-ws": minor
|
||||
"jazz-react-native": minor
|
||||
"jazz-auth-clerk": minor
|
||||
"jazz-react-core": minor
|
||||
"jazz-inspector": minor
|
||||
"jazz-browser": minor
|
||||
"jazz-nodejs": minor
|
||||
"jazz-react": minor
|
||||
"jazz-tools": minor
|
||||
"cojson": minor
|
||||
---
|
||||
|
||||
Introducing the new auth system!
|
||||
5
.changeset/breezy-cats-yawn.md
Normal file
5
.changeset/breezy-cats-yawn.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"jazz-nodejs": patch
|
||||
---
|
||||
|
||||
Add storage peer option
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"cojson": patch
|
||||
---
|
||||
|
||||
Drop node 14 polyfill for globalThis.crypto
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"jazz-tools": patch
|
||||
---
|
||||
|
||||
Make ensureLoaded throw when the resolved value is undefined
|
||||
@@ -11,7 +11,7 @@
|
||||
"cojson-storage-sqlite",
|
||||
"cojson-transport-ws",
|
||||
"jazz-browser",
|
||||
"jazz-auth-clerk",
|
||||
"jazz-browser-auth-clerk",
|
||||
"jazz-browser-media-images",
|
||||
"jazz-nodejs",
|
||||
"jazz-react",
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"create-jazz-app": patch
|
||||
---
|
||||
|
||||
Add framework and api-key param to create-jazz-app
|
||||
@@ -1,15 +0,0 @@
|
||||
---
|
||||
"jazz-nodejs": minor
|
||||
---
|
||||
|
||||
Remove ws dependency to use native WebSocket.
|
||||
|
||||
NodeJS versions prior to v22 will need to provide a WebSocket constructor from ws:
|
||||
|
||||
```ts
|
||||
import { WebSocket } from "ws"
|
||||
|
||||
const { worker } = await startWorker({ WebSocket, synServer });
|
||||
```
|
||||
|
||||
This makes it easier to run workers on every JS runtime.
|
||||
@@ -1,8 +0,0 @@
|
||||
---
|
||||
"jazz-react-core": minor
|
||||
"jazz-svelte": minor
|
||||
"jazz-tools": minor
|
||||
"jazz-vue": minor
|
||||
---
|
||||
|
||||
Return null when a coValue is not found
|
||||
9
.github/workflows/build-examples.yaml
vendored
9
.github/workflows/build-examples.yaml
vendored
@@ -27,13 +27,8 @@ jobs:
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Enable latestcorepack
|
||||
run: |
|
||||
echo "Before: corepack version => $(corepack --version || echo 'not installed')"
|
||||
npm install -g corepack@latest
|
||||
echo "After : corepack version => $(corepack --version)"
|
||||
corepack enable
|
||||
pnpm --version
|
||||
- name: Enable corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
|
||||
9
.github/workflows/build-starters.yaml
vendored
9
.github/workflows/build-starters.yaml
vendored
@@ -18,13 +18,8 @@ jobs:
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Enable latestcorepack
|
||||
run: |
|
||||
echo "Before: corepack version => $(corepack --version || echo 'not installed')"
|
||||
npm install -g corepack@latest
|
||||
echo "After : corepack version => $(corepack --version)"
|
||||
corepack enable
|
||||
pnpm --version
|
||||
- name: Enable corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
|
||||
9
.github/workflows/jazz-run.yml
vendored
9
.github/workflows/jazz-run.yml
vendored
@@ -16,13 +16,8 @@ jobs:
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Enable latestcorepack
|
||||
run: |
|
||||
echo "Before: corepack version => $(corepack --version || echo 'not installed')"
|
||||
npm install -g corepack@latest
|
||||
echo "After : corepack version => $(corepack --version)"
|
||||
corepack enable
|
||||
pnpm --version
|
||||
- name: Enable corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
|
||||
9
.github/workflows/playwright.yml
vendored
9
.github/workflows/playwright.yml
vendored
@@ -20,13 +20,8 @@ jobs:
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Enable latestcorepack
|
||||
run: |
|
||||
echo "Before: corepack version => $(corepack --version || echo 'not installed')"
|
||||
npm install -g corepack@latest
|
||||
echo "After : corepack version => $(corepack --version)"
|
||||
corepack enable
|
||||
pnpm --version
|
||||
- name: Enable corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
|
||||
49
.github/workflows/pre-release.yml
vendored
49
.github/workflows/pre-release.yml
vendored
@@ -1,49 +0,0 @@
|
||||
name: Pre-Publish tagged Pull Requests
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
jobs:
|
||||
pre-release:
|
||||
if: contains(github.event.pull_request.labels.*.name, 'pre-release')
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Enable latestcorepack
|
||||
run: |
|
||||
echo "Before: corepack version => $(corepack --version || echo 'not installed')"
|
||||
npm install -g corepack@latest
|
||||
echo "After : corepack version => $(corepack --version)"
|
||||
corepack enable
|
||||
pnpm --version
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Get pnpm store directory
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- uses: actions/cache@v4
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Pnpm Build
|
||||
run: pnpm turbo build --filter="./packages/*"
|
||||
|
||||
- name: Pre publish
|
||||
run: pnpm exec pkg-pr-new publish "./packages/*"
|
||||
9
.github/workflows/release.yml
vendored
9
.github/workflows/release.yml
vendored
@@ -22,13 +22,8 @@ jobs:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Enable latestcorepack
|
||||
run: |
|
||||
echo "Before: corepack version => $(corepack --version || echo 'not installed')"
|
||||
npm install -g corepack@latest
|
||||
echo "After : corepack version => $(corepack --version)"
|
||||
corepack enable
|
||||
pnpm --version
|
||||
- name: Enable corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
|
||||
12
.github/workflows/unit-test.yml
vendored
12
.github/workflows/unit-test.yml
vendored
@@ -15,13 +15,8 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Enable latestcorepack
|
||||
run: |
|
||||
echo "Before: corepack version => $(corepack --version || echo 'not installed')"
|
||||
npm install -g corepack@latest
|
||||
echo "After : corepack version => $(corepack --version)"
|
||||
corepack enable
|
||||
pnpm --version
|
||||
- name: Enable corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
@@ -45,9 +40,6 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: pnpm exec playwright install
|
||||
|
||||
- name: Pnpm Build
|
||||
run: pnpm turbo build --filter="./packages/*"
|
||||
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -19,5 +19,3 @@ test-results
|
||||
.husky
|
||||
|
||||
.vscode/settings.json
|
||||
|
||||
.svelte-kit
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Redirect, Stack } from "expo-router";
|
||||
import { useIsAuthenticated } from "jazz-react-native";
|
||||
import React from "react";
|
||||
import { useAuth } from "../../src/auth-context";
|
||||
|
||||
export default function HomeLayout() {
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
const { isAuthenticated } = useAuth();
|
||||
|
||||
if (isAuthenticated) {
|
||||
return <Redirect href={"/chat"} />;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Redirect, Stack } from "expo-router";
|
||||
import { useIsAuthenticated } from "jazz-react-native";
|
||||
import { useAuth } from "../../src/auth-context";
|
||||
|
||||
export default function UnAuthenticatedLayout() {
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
const { isAuthenticated } = useAuth();
|
||||
|
||||
if (isAuthenticated) {
|
||||
return <Redirect href={"/chat"} />;
|
||||
|
||||
@@ -20,15 +20,10 @@ export default function ChatScreen() {
|
||||
const navigation = useNavigation();
|
||||
const { user } = useUser();
|
||||
|
||||
function handleLogOut() {
|
||||
logOut();
|
||||
router.navigate("/");
|
||||
}
|
||||
|
||||
useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerTitle: "Chat",
|
||||
headerRight: () => <Button onPress={handleLogOut} title="Logout" />,
|
||||
headerRight: () => <Button onPress={logOut} title="Logout" />,
|
||||
});
|
||||
}, [navigation]);
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export const apiKey = "chat-rn-clerk-example-jazz@garden.co";
|
||||
@@ -1,21 +1,49 @@
|
||||
import { useClerk } from "@clerk/clerk-expo";
|
||||
import { JazzProviderWithClerk } from "jazz-react-native-auth-clerk";
|
||||
import React, { PropsWithChildren } from "react";
|
||||
import { apiKey } from "./apiKey";
|
||||
import { useClerk, useUser } from "@clerk/clerk-expo";
|
||||
import { JazzProvider, setupKvStore } from "jazz-react-native";
|
||||
import { useJazzClerkAuth } from "jazz-react-native-auth-clerk";
|
||||
import React, { createContext, PropsWithChildren, useContext } from "react";
|
||||
import { Text, View } from "react-native";
|
||||
const AuthContext = createContext<{
|
||||
isAuthenticated: boolean;
|
||||
isLoading: boolean;
|
||||
}>({
|
||||
isAuthenticated: false,
|
||||
isLoading: true,
|
||||
});
|
||||
|
||||
export function useAuth() {
|
||||
return useContext(AuthContext);
|
||||
}
|
||||
|
||||
const kvStore = setupKvStore();
|
||||
|
||||
export function JazzAndAuth({ children }: PropsWithChildren) {
|
||||
const { isSignedIn, isLoaded: isClerkLoaded } = useUser();
|
||||
const clerk = useClerk();
|
||||
const [auth, state] = useJazzClerkAuth(clerk, kvStore);
|
||||
const isAuthenticated = Boolean(isSignedIn && isClerkLoaded && auth);
|
||||
|
||||
return (
|
||||
<JazzProviderWithClerk
|
||||
clerk={clerk}
|
||||
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
|
||||
}}
|
||||
<AuthContext.Provider
|
||||
value={{ isAuthenticated, isLoading: !isClerkLoaded || !auth }}
|
||||
>
|
||||
{children}
|
||||
</JazzProviderWithClerk>
|
||||
{state?.errors?.length > 0 &&
|
||||
state.errors.map((error) => (
|
||||
<View key={error}>
|
||||
<Text style={{ color: "red" }}>{error}</Text>
|
||||
</View>
|
||||
))}
|
||||
{auth && clerk.user ? (
|
||||
<JazzProvider
|
||||
auth={auth}
|
||||
storage="sqlite"
|
||||
peer="wss://cloud.jazz.tools/?key=chat-rn-clerk-example-jazz@garden.co"
|
||||
>
|
||||
{children}
|
||||
</JazzProvider>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,8 +9,7 @@ import * as Linking from "expo-linking";
|
||||
import React, { StrictMode, useEffect, useState } from "react";
|
||||
import HandleInviteScreen from "./invite";
|
||||
|
||||
import { JazzProvider } from "jazz-react-native";
|
||||
import { apiKey } from "./apiKey";
|
||||
import { DemoAuthBasicUI, JazzProvider, useDemoAuth } from "jazz-react-native";
|
||||
import ChatScreen from "./chat";
|
||||
|
||||
const Stack = createNativeStackNavigator();
|
||||
@@ -29,6 +28,7 @@ const linking = {
|
||||
};
|
||||
|
||||
function App() {
|
||||
const [auth, state] = useDemoAuth();
|
||||
const [initialRoute, setInitialRoute] = useState<
|
||||
"ChatScreen" | "HandleInviteScreen"
|
||||
>("ChatScreen");
|
||||
@@ -43,13 +43,16 @@ function App() {
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (!auth) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<StrictMode>
|
||||
<JazzProvider
|
||||
auth={auth}
|
||||
storage="sqlite"
|
||||
sync={{
|
||||
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
|
||||
}}
|
||||
peer="wss://cloud.jazz.tools/?key=chat-rn-example-jazz@garden.co"
|
||||
>
|
||||
<NavigationContainer linking={linking} ref={navigationRef}>
|
||||
<Stack.Navigator initialRouteName={initialRoute}>
|
||||
@@ -66,6 +69,9 @@ function App() {
|
||||
</Stack.Navigator>
|
||||
</NavigationContainer>
|
||||
</JazzProvider>
|
||||
{state.state !== "signedIn" ? (
|
||||
<DemoAuthBasicUI appName="Jazz Chat" state={state} />
|
||||
) : null}
|
||||
</StrictMode>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export const apiKey = "chat-rn-example-jazz@garden.co";
|
||||
@@ -1,6 +1,6 @@
|
||||
import clsx from "clsx";
|
||||
import * as Clipboard from "expo-clipboard";
|
||||
import { Group, ID, Profile } from "jazz-tools";
|
||||
import { Group, ID } from "jazz-tools";
|
||||
import { useEffect, useState } from "react";
|
||||
import React, {
|
||||
Button,
|
||||
@@ -22,16 +22,10 @@ export default function ChatScreen({ navigation }: { navigation: any }) {
|
||||
const [chatId, setChatId] = useState<ID<Chat>>();
|
||||
const loadedChat = useCoState(Chat, chatId, [{}]);
|
||||
const [message, setMessage] = useState("");
|
||||
const profile = useCoState(Profile, me._refs.profile?.id, {});
|
||||
|
||||
function handleLogOut() {
|
||||
setChatId(undefined);
|
||||
logOut();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerRight: () => <Button onPress={handleLogOut} title="Logout" />,
|
||||
headerRight: () => <Button onPress={logOut} title="Logout" />,
|
||||
headerLeft: () =>
|
||||
loadedChat ? (
|
||||
<Button
|
||||
@@ -137,18 +131,6 @@ export default function ChatScreen({ navigation }: { navigation: any }) {
|
||||
<View className="flex flex-col h-full">
|
||||
{!loadedChat ? (
|
||||
<View className="flex flex-col h-full items-center justify-center">
|
||||
<Text className="text-m font-bold mb-6">Username</Text>
|
||||
<TextInput
|
||||
className="rounded h-12 p-2 mb-12 w-40 border border-gray-200 block"
|
||||
value={profile?.name ?? ""}
|
||||
onChangeText={(value) => {
|
||||
if (profile) {
|
||||
profile.name = value;
|
||||
}
|
||||
}}
|
||||
textAlignVertical="center"
|
||||
onSubmitEditing={sendMessage}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
onPress={createChat}
|
||||
className="bg-blue-500 p-4 rounded-md"
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"postcss": "^8.4.27",
|
||||
"tailwindcss": "^3.4.15",
|
||||
"typescript": "~5.6.2",
|
||||
"vite": "^6.0.11",
|
||||
"vite": "^5.4.10",
|
||||
"vite-plugin-vue-devtools": "^7.4.6",
|
||||
"vue-tsc": "^2.1.6"
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export const apiKey = "chat-example-jazz@garden.co";
|
||||
@@ -1,31 +1,32 @@
|
||||
import { DemoAuthBasicUI, JazzProvider } from "jazz-vue";
|
||||
import { DemoAuthBasicUI, JazzProvider, useDemoAuth } from "jazz-vue";
|
||||
import { createApp, defineComponent, h } from "vue";
|
||||
import App from "./App.vue";
|
||||
import "./index.css";
|
||||
import { apiKey } from "@/apiKey";
|
||||
import router from "./router";
|
||||
|
||||
const RootComponent = defineComponent({
|
||||
name: "RootComponent",
|
||||
setup() {
|
||||
return () =>
|
||||
const { authMethod, state } = useDemoAuth();
|
||||
|
||||
return () => [
|
||||
h(
|
||||
JazzProvider,
|
||||
{
|
||||
sync: {
|
||||
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
|
||||
},
|
||||
auth: authMethod.value,
|
||||
peer: "wss://cloud.jazz.tools/?key=chat-example-jazz@garden.co",
|
||||
},
|
||||
h(
|
||||
DemoAuthBasicUI,
|
||||
{
|
||||
appName: "Jazz Vue Chat",
|
||||
},
|
||||
{
|
||||
default: () => h(App),
|
||||
},
|
||||
),
|
||||
);
|
||||
{
|
||||
default: () => h(App),
|
||||
},
|
||||
),
|
||||
|
||||
state.state !== "signedIn" &&
|
||||
h(DemoAuthBasicUI, {
|
||||
appName: "Jazz Chat",
|
||||
state,
|
||||
}),
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.50.1",
|
||||
"@playwright/test": "^1.46.1",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||
@@ -32,6 +32,6 @@
|
||||
"postcss": "^8.4.27",
|
||||
"tailwindcss": "^3.4.15",
|
||||
"typescript": "~5.6.2",
|
||||
"vite": "^6.0.11"
|
||||
"vite": "^5.4.10"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export const apiKey = "chat-example-jazz@garden.co";
|
||||
@@ -1,11 +1,11 @@
|
||||
import { apiKey } from "@/apiKey.ts";
|
||||
import { getRandomUsername, inIframe, onChatLoad } from "@/util.ts";
|
||||
import { inIframe, onChatLoad } from "@/util.ts";
|
||||
import { useIframeHashRouter } from "hash-slash";
|
||||
import { JazzProvider, useAccount } from "jazz-react";
|
||||
import { useAccount } from "jazz-react";
|
||||
import { Group, ID } from "jazz-tools";
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { ChatScreen } from "./chatScreen.tsx";
|
||||
import { JazzAndAuth } from "./jazz.tsx";
|
||||
import { Chat } from "./schema.ts";
|
||||
import { ThemeProvider } from "./themeProvider.tsx";
|
||||
import { AppContainer, TopBar } from "./ui.tsx";
|
||||
@@ -28,15 +28,7 @@ export function App() {
|
||||
return (
|
||||
<AppContainer>
|
||||
<TopBar>
|
||||
<input
|
||||
type="text"
|
||||
value={me?.profile?.name ?? ""}
|
||||
onChange={(e) => {
|
||||
if (!me?.profile) return;
|
||||
me.profile.name = e.target.value;
|
||||
}}
|
||||
placeholder="Set username"
|
||||
/>
|
||||
<p>{me?.profile?.name}</p>
|
||||
{!inIframe && <button onClick={logOut}>Log out</button>}
|
||||
</TopBar>
|
||||
{router.route({
|
||||
@@ -47,20 +39,12 @@ export function App() {
|
||||
);
|
||||
}
|
||||
|
||||
const url = new URL(window.location.href);
|
||||
const defaultProfileName = url.searchParams.get("user") ?? getRandomUsername();
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<ThemeProvider>
|
||||
<StrictMode>
|
||||
<JazzProvider
|
||||
sync={{
|
||||
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
|
||||
}}
|
||||
defaultProfileName={defaultProfileName}
|
||||
>
|
||||
<JazzAndAuth>
|
||||
<App />
|
||||
</JazzProvider>
|
||||
</JazzAndAuth>
|
||||
</StrictMode>
|
||||
</ThemeProvider>,
|
||||
);
|
||||
|
||||
19
examples/chat/src/jazz.tsx
Normal file
19
examples/chat/src/jazz.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { DemoAuthBasicUI, JazzProvider, useDemoAuth } from "jazz-react";
|
||||
|
||||
export function JazzAndAuth({ children }: { children: React.ReactNode }) {
|
||||
const [auth, state] = useDemoAuth();
|
||||
|
||||
return (
|
||||
<>
|
||||
<JazzProvider
|
||||
auth={auth}
|
||||
peer="wss://cloud.jazz.tools/?key=chat-example-jazz@garden.co"
|
||||
>
|
||||
{children}
|
||||
</JazzProvider>
|
||||
{state.state !== "signedIn" && (
|
||||
<DemoAuthBasicUI appName="Jazz Chat" state={state} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -15,20 +15,3 @@ export function onChatLoad(chat: Chat) {
|
||||
}
|
||||
|
||||
export const inIframe = window.self !== window.top;
|
||||
|
||||
const animals = [
|
||||
"elephant",
|
||||
"penguin",
|
||||
"giraffe",
|
||||
"octopus",
|
||||
"kangaroo",
|
||||
"dolphin",
|
||||
"cheetah",
|
||||
"koala",
|
||||
"platypus",
|
||||
"pangolin",
|
||||
];
|
||||
|
||||
export function getRandomUsername() {
|
||||
return `Anonymous ${animals[Math.floor(Math.random() * animals.length)]}`;
|
||||
}
|
||||
|
||||
@@ -1,35 +1,48 @@
|
||||
import { test } from "@playwright/test";
|
||||
import { ChatPage } from "./pages/ChatPage";
|
||||
import { LoginPage } from "./pages/LoginPage";
|
||||
|
||||
test("chat between two users", async ({ page: marioPage, browser }) => {
|
||||
const context = await browser.newContext();
|
||||
const luigiPage = await context.newPage();
|
||||
test("chat between two users", async ({ page }) => {
|
||||
const loginPage = new LoginPage(page);
|
||||
|
||||
await marioPage.goto("/");
|
||||
const mario = "S. Mario";
|
||||
const luigi = "Luigi";
|
||||
|
||||
const marioChat = new ChatPage(marioPage);
|
||||
const luigiChat = new ChatPage(luigiPage);
|
||||
await loginPage.goto();
|
||||
await loginPage.fillUsername(mario);
|
||||
await loginPage.signup();
|
||||
|
||||
await marioChat.setUsername("Mario");
|
||||
const chatPage = new ChatPage(page);
|
||||
|
||||
const message1ByMario = "Hello Luigi, are you ready to save the princess?";
|
||||
|
||||
await marioChat.sendMessage(message1ByMario);
|
||||
await marioChat.expectMessageRow(message1ByMario);
|
||||
await chatPage.sendMessage(message1ByMario);
|
||||
await chatPage.expectMessageRow(message1ByMario);
|
||||
|
||||
const roomURL = marioPage.url();
|
||||
await luigiPage.goto(roomURL);
|
||||
const roomURL = page.url();
|
||||
|
||||
await luigiChat.setUsername("Luigi");
|
||||
await chatPage.logout();
|
||||
|
||||
await luigiChat.expectMessageRow(message1ByMario);
|
||||
await loginPage.expectLoaded();
|
||||
|
||||
await loginPage.fillUsername(luigi);
|
||||
await loginPage.signup();
|
||||
|
||||
await page.goto(roomURL);
|
||||
|
||||
await chatPage.expectMessageRow(message1ByMario);
|
||||
|
||||
const message2ByLuigi =
|
||||
"No, I'm not ready yet. I'm still trying to find the key to the castle.";
|
||||
|
||||
await luigiChat.sendMessage(message2ByLuigi);
|
||||
await luigiChat.expectMessageRow(message2ByLuigi);
|
||||
await chatPage.sendMessage(message2ByLuigi);
|
||||
await chatPage.expectMessageRow(message2ByLuigi);
|
||||
|
||||
await marioChat.expectMessageRow(message1ByMario);
|
||||
await luigiChat.expectMessageRow(message2ByLuigi);
|
||||
await chatPage.logout();
|
||||
await loginPage.loginAs(mario);
|
||||
|
||||
await page.goto(roomURL);
|
||||
|
||||
await chatPage.expectMessageRow(message1ByMario);
|
||||
await chatPage.expectMessageRow(message2ByLuigi);
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ export class ChatPage {
|
||||
readonly page: Page;
|
||||
readonly messageInput: Locator;
|
||||
readonly logoutButton: Locator;
|
||||
readonly usernameInput: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.messageInput = page.getByRole("textbox", {
|
||||
@@ -13,11 +13,6 @@ export class ChatPage {
|
||||
this.logoutButton = page.getByRole("button", {
|
||||
name: "Log out",
|
||||
});
|
||||
this.usernameInput = page.getByPlaceholder("Set username");
|
||||
}
|
||||
|
||||
async setUsername(username: string) {
|
||||
await this.usernameInput.fill(username);
|
||||
}
|
||||
|
||||
async sendMessage(message: string) {
|
||||
|
||||
40
examples/chat/tests/pages/LoginPage.ts
Normal file
40
examples/chat/tests/pages/LoginPage.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,6 @@
|
||||
"@vitejs/plugin-react": "^4.3.3",
|
||||
"globals": "^15.11.0",
|
||||
"typescript": "~5.6.2",
|
||||
"vite": "^6.0.11"
|
||||
"vite": "^5.4.10"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,13 @@
|
||||
import { SignInButton } from "@clerk/clerk-react";
|
||||
import { useAccount, useIsAuthenticated } from "jazz-react";
|
||||
import { useAccount } from "jazz-react";
|
||||
|
||||
function App() {
|
||||
const { me, logOut } = useAccount();
|
||||
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
|
||||
if (isAuthenticated) {
|
||||
return (
|
||||
<div className="container">
|
||||
<h1>You're logged in</h1>
|
||||
<p>Welcome back, {me?.profile?.name}</p>
|
||||
<button onClick={() => logOut()}>Logout</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<h1>You're not logged in</h1>
|
||||
<SignInButton />
|
||||
<h1>You're logged in</h1>
|
||||
<p>Welcome back, {me?.profile?.name}</p>
|
||||
<button onClick={() => logOut()}>Logout</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export const apiKey = "minimal-auth-clerk-example@garden.co";
|
||||
@@ -62,7 +62,7 @@ button {
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 400px;
|
||||
max-width: 200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 1rem;
|
||||
display: flex;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { ClerkProvider, useClerk } from "@clerk/clerk-react";
|
||||
import { ClerkProvider, SignInButton, useClerk } from "@clerk/clerk-react";
|
||||
import { useJazzClerkAuth } from "jazz-react-auth-clerk";
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import App from "./App.tsx";
|
||||
import "./index.css";
|
||||
import { JazzProviderWithClerk } from "jazz-react-auth-clerk";
|
||||
import { apiKey } from "./apiKey";
|
||||
import { JazzProvider } from "jazz-react";
|
||||
|
||||
// Import your publishable key
|
||||
const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY;
|
||||
@@ -13,28 +13,35 @@ if (!PUBLISHABLE_KEY) {
|
||||
throw new Error("Add your Clerk publishable key to the .env.local file");
|
||||
}
|
||||
|
||||
function JazzProvider({ children }: { children: React.ReactNode }) {
|
||||
function JazzAndAuth({ children }: { children: React.ReactNode }) {
|
||||
const clerk = useClerk();
|
||||
const [auth, state] = useJazzClerkAuth(clerk);
|
||||
|
||||
return (
|
||||
<JazzProviderWithClerk
|
||||
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}
|
||||
</JazzProviderWithClerk>
|
||||
<main className="container">
|
||||
{state?.errors?.map((error) => (
|
||||
<div key={error}>{error}</div>
|
||||
))}
|
||||
{clerk.user && auth ? (
|
||||
<JazzProvider
|
||||
auth={auth}
|
||||
peer="wss://cloud.jazz.tools/?key=minimal-auth-clerk-example@garden.co"
|
||||
>
|
||||
{children}
|
||||
</JazzProvider>
|
||||
) : (
|
||||
<SignInButton />
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<ClerkProvider publishableKey={PUBLISHABLE_KEY} afterSignOutUrl="/">
|
||||
<JazzProvider>
|
||||
<JazzAndAuth>
|
||||
<App />
|
||||
</JazzProvider>
|
||||
</JazzAndAuth>
|
||||
</ClerkProvider>
|
||||
</StrictMode>,
|
||||
);
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-vercel": "^5.5.0",
|
||||
"@sveltejs/kit": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^4.0.1",
|
||||
"@types/is-ci": "^3.0.4",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9.7.0",
|
||||
@@ -35,7 +35,7 @@
|
||||
"tailwindcss": "^3.4.15",
|
||||
"typescript": "~5.6.2",
|
||||
"typescript-eslint": "^8.0.0",
|
||||
"vite": "^6.0.11"
|
||||
"vite": "^5.4.10"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export const apiKey = "file-share-svelte@garden.co"
|
||||
@@ -17,6 +17,6 @@ export function formatFileSize(bytes: number): string {
|
||||
* @param createdAt The creation date
|
||||
* @returns A unique file ID string
|
||||
*/
|
||||
export function generateTempFileId(fileName: string | undefined, createdAt: Date | undefined): string {
|
||||
return `file-${fileName ?? 'unknown'}-${createdAt?.getTime() ?? 0}`;
|
||||
export function generateTempFileId(fileName: string, createdAt: Date): string {
|
||||
return `file-${fileName}-${createdAt.getTime()}`;
|
||||
}
|
||||
|
||||
@@ -12,9 +12,11 @@
|
||||
import { Toaster } from 'svelte-sonner';
|
||||
import '../app.css';
|
||||
import { FileShareAccount } from '$lib/schema';
|
||||
import {apiKey} from '../apiKey';
|
||||
|
||||
let { children } = $props();
|
||||
const auth = usePasskeyAuth({
|
||||
appName: 'File Share'
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -23,16 +25,21 @@
|
||||
|
||||
<Toaster richColors />
|
||||
|
||||
<JazzProvider
|
||||
AccountSchema={FileShareAccount}
|
||||
sync={{
|
||||
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
|
||||
when: "signedUp",
|
||||
}}
|
||||
>
|
||||
<PasskeyAuthBasicUI appName="File Share">
|
||||
{#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}
|
||||
<JazzProvider
|
||||
AccountSchema={FileShareAccount}
|
||||
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>
|
||||
</PasskeyAuthBasicUI>
|
||||
</JazzProvider>
|
||||
</JazzProvider>
|
||||
{/if}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
const input = event.target as HTMLInputElement;
|
||||
const files = input.files;
|
||||
|
||||
if (!files || !files.length || !me.root?.sharedFiles || !me.root.publicGroup) return;
|
||||
if (!files || !files.length || !me?.root?.sharedFiles || !me.root.publicGroup) return;
|
||||
|
||||
const file = files[0];
|
||||
const fileName = file.name;
|
||||
@@ -129,14 +129,12 @@
|
||||
{#if sharedFiles.current}
|
||||
{#if !(sharedFiles.current.length === 0 && uploadingFiles.size === 0)}
|
||||
{#each [...sharedFiles.current, ...uploadingFiles.values()] as file (generateTempFileId(file?.name, file?.createdAt))}
|
||||
{#if file}
|
||||
<FileItem
|
||||
{file}
|
||||
loading={uploadingFiles.has(generateTempFileId(file?.name, file?.createdAt))}
|
||||
onShare={shareFile}
|
||||
onDelete={deleteFile}
|
||||
/>
|
||||
{/if}
|
||||
<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>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.9.4",
|
||||
"@playwright/test": "^1.50.1",
|
||||
"@playwright/test": "^1.46.1",
|
||||
"@tailwindcss/forms": "^0.5.9",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
@@ -31,6 +31,6 @@
|
||||
"postcss": "^8.4.27",
|
||||
"tailwindcss": "^3.4.15",
|
||||
"typescript": "~5.6.2",
|
||||
"vite": "^6.0.11"
|
||||
"vite": "^5.4.10"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export const apiKey = "form-example@garden.co";
|
||||
@@ -1,11 +1,30 @@
|
||||
import { JazzProvider } from "jazz-react";
|
||||
import { DemoAuthBasicUI, JazzProvider, useDemoAuth } from "jazz-react";
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import App from "./App.tsx";
|
||||
import "./index.css";
|
||||
import { apiKey } from "./apiKey";
|
||||
import { JazzAccount } from "./schema.ts";
|
||||
|
||||
function JazzAndAuth({ children }: { children: React.ReactNode }) {
|
||||
const [auth, authState] = useDemoAuth();
|
||||
|
||||
return (
|
||||
<>
|
||||
<JazzProvider
|
||||
auth={auth}
|
||||
peer="wss://cloud.jazz.tools/?key=form-example@garden.co"
|
||||
AccountSchema={JazzAccount}
|
||||
>
|
||||
{children}
|
||||
</JazzProvider>
|
||||
|
||||
{authState.state !== "signedIn" && (
|
||||
<DemoAuthBasicUI appName="Form" state={authState} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
declare module "jazz-react" {
|
||||
interface Register {
|
||||
Account: JazzAccount;
|
||||
@@ -14,13 +33,8 @@ declare module "jazz-react" {
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<JazzProvider
|
||||
sync={{
|
||||
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
|
||||
}}
|
||||
AccountSchema={JazzAccount}
|
||||
>
|
||||
<JazzAndAuth>
|
||||
<App />
|
||||
</JazzProvider>
|
||||
</JazzAndAuth>
|
||||
</StrictMode>,
|
||||
);
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { LoginPage } from "./pages/LoginPage";
|
||||
|
||||
test("create and edit an order", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
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");
|
||||
|
||||
40
examples/form/tests/pages/LoginPage.ts
Normal file
40
examples/form/tests/pages/LoginPage.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,6 @@
|
||||
"@vitejs/plugin-react": "^4.3.3",
|
||||
"globals": "^15.11.0",
|
||||
"typescript": "~5.6.2",
|
||||
"vite": "^6.0.11"
|
||||
"vite": "^5.4.10"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export const apiKey = "image-upload-example@garden.co";
|
||||
@@ -1,11 +1,30 @@
|
||||
import { JazzProvider } from "jazz-react";
|
||||
import { DemoAuthBasicUI, JazzProvider, useDemoAuth } from "jazz-react";
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import App from "./App.tsx";
|
||||
import "./index.css";
|
||||
import { apiKey } from "./apiKey.ts";
|
||||
import { JazzAccount } from "./schema.ts";
|
||||
|
||||
function JazzAndAuth({ children }: { children: React.ReactNode }) {
|
||||
const [auth, authState] = useDemoAuth();
|
||||
|
||||
return (
|
||||
<>
|
||||
<JazzProvider
|
||||
auth={auth}
|
||||
peer="wss://cloud.jazz.tools/?key=image-upload-example@garden.co"
|
||||
AccountSchema={JazzAccount}
|
||||
>
|
||||
{children}
|
||||
</JazzProvider>
|
||||
|
||||
{authState.state !== "signedIn" && (
|
||||
<DemoAuthBasicUI appName="Image upload" state={authState} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
declare module "jazz-react" {
|
||||
interface Register {
|
||||
Account: JazzAccount;
|
||||
@@ -14,13 +33,8 @@ declare module "jazz-react" {
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<JazzProvider
|
||||
sync={{
|
||||
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
|
||||
}}
|
||||
AccountSchema={JazzAccount}
|
||||
>
|
||||
<JazzAndAuth>
|
||||
<App />
|
||||
</JazzProvider>
|
||||
</JazzAndAuth>
|
||||
</StrictMode>,
|
||||
);
|
||||
|
||||
@@ -39,6 +39,6 @@
|
||||
"postcss": "^8.4.27",
|
||||
"tailwindcss": "^3.4.15",
|
||||
"typescript": "~5.6.2",
|
||||
"vite": "^6.0.11"
|
||||
"vite": "^5.4.10"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export const apiKey = "";
|
||||
@@ -13,12 +13,9 @@
|
||||
"test:ui": "playwright test --ui"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-dialog": "^1.1.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||
"@radix-ui/react-label": "^2.1.1",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-toast": "^1.1.4",
|
||||
"@radix-ui/react-tooltip": "^1.1.6",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"jazz-react": "workspace:0.9.22",
|
||||
@@ -33,7 +30,7 @@
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.50.1",
|
||||
"@playwright/test": "^1.46.1",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||
@@ -41,6 +38,6 @@
|
||||
"postcss": "^8.4.27",
|
||||
"tailwindcss": "^3.4.15",
|
||||
"typescript": "~5.6.2",
|
||||
"vite": "^6.0.11"
|
||||
"vite": "^5.4.10"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,8 +36,6 @@ export class MusicTrack extends CoMap {
|
||||
*/
|
||||
file = co.ref(FileStream);
|
||||
waveform = co.ref(MusicTrackWaveform);
|
||||
|
||||
isExampleTrack = co.optional.boolean;
|
||||
}
|
||||
|
||||
export class MusicTrackWaveform extends CoMap {
|
||||
@@ -88,20 +86,28 @@ export class MusicaAccount extends Account {
|
||||
* You can use it to set up the account root and any other initial CoValues you need.
|
||||
*/
|
||||
migrate() {
|
||||
if (this.root === undefined) {
|
||||
const tracks = ListOfTracks.create([]);
|
||||
const rootPlaylist = Playlist.create({
|
||||
tracks,
|
||||
title: "",
|
||||
});
|
||||
if (!this._refs.root) {
|
||||
const ownership = { owner: this };
|
||||
|
||||
this.root = MusicaAccountRoot.create({
|
||||
rootPlaylist,
|
||||
playlists: ListOfPlaylists.create([]),
|
||||
activeTrack: null,
|
||||
activePlaylist: rootPlaylist,
|
||||
exampleDataLoaded: false,
|
||||
});
|
||||
const tracks = ListOfTracks.create([], ownership);
|
||||
const rootPlaylist = Playlist.create(
|
||||
{
|
||||
tracks,
|
||||
title: "",
|
||||
},
|
||||
ownership,
|
||||
);
|
||||
|
||||
this.root = MusicaAccountRoot.create(
|
||||
{
|
||||
rootPlaylist,
|
||||
playlists: ListOfPlaylists.create([], ownership),
|
||||
activeTrack: null,
|
||||
activePlaylist: rootPlaylist,
|
||||
exampleDataLoaded: false,
|
||||
},
|
||||
ownership,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,7 @@ import { PlayerControls } from "./components/PlayerControls";
|
||||
import "./index.css";
|
||||
|
||||
import { MusicaAccount } from "@/1_schema";
|
||||
import { apiKey } from "@/apiKey.ts";
|
||||
import { JazzProvider } from "jazz-react";
|
||||
import { onAnonymousAccountDiscarded } from "./4_actions";
|
||||
import { DemoAuthBasicUI, JazzProvider, useDemoAuth } from "jazz-react";
|
||||
import { useUploadExampleData } from "./lib/useUploadExampleData";
|
||||
|
||||
/**
|
||||
@@ -56,10 +54,30 @@ function Main() {
|
||||
);
|
||||
}
|
||||
|
||||
const peer =
|
||||
(new URL(window.location.href).searchParams.get(
|
||||
"peer",
|
||||
) as `ws://${string}`) ?? `wss://cloud.jazz.tools/?key=${apiKey}`;
|
||||
function JazzAndAuth({ children }: { children: React.ReactNode }) {
|
||||
const [auth, state] = useDemoAuth();
|
||||
|
||||
const peer =
|
||||
(new URL(window.location.href).searchParams.get(
|
||||
"peer",
|
||||
) as `ws://${string}`) ??
|
||||
"wss://cloud.jazz.tools/?key=music-player-example-jazz@garden.co";
|
||||
|
||||
return (
|
||||
<>
|
||||
<JazzProvider
|
||||
storage="indexedDB"
|
||||
auth={auth}
|
||||
peer={peer}
|
||||
AccountSchema={MusicaAccount}
|
||||
>
|
||||
{children}
|
||||
<JazzInspector />
|
||||
</JazzProvider>
|
||||
<DemoAuthBasicUI appName="Jazz Music Player" state={state} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
declare module "jazz-react" {
|
||||
interface Register {
|
||||
@@ -69,18 +87,8 @@ declare module "jazz-react" {
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<JazzProvider
|
||||
sync={{
|
||||
peer,
|
||||
when: "signedUp", // This makes the app work in local mode when the user is anonymous
|
||||
}}
|
||||
storage="indexedDB"
|
||||
AccountSchema={MusicaAccount}
|
||||
defaultProfileName="Anonymous unicorn"
|
||||
onAnonymousAccountDiscarded={onAnonymousAccountDiscarded}
|
||||
>
|
||||
<JazzAndAuth>
|
||||
<Main />
|
||||
<JazzInspector />
|
||||
</JazzProvider>
|
||||
</JazzAndAuth>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import {
|
||||
createInviteLink,
|
||||
useAccount,
|
||||
useCoState,
|
||||
useIsAuthenticated,
|
||||
} from "jazz-react";
|
||||
import { createInviteLink, useAccount, useCoState } from "jazz-react";
|
||||
import { ID } from "jazz-tools";
|
||||
import { useNavigate, useParams } from "react-router";
|
||||
import { Playlist } from "./1_schema";
|
||||
import { createNewPlaylist, uploadMusicTracks } from "./4_actions";
|
||||
import { MediaPlayer } from "./5_useMediaPlayer";
|
||||
import { AuthButton } from "./components/AuthButton";
|
||||
import { FileUploadButton } from "./components/FileUploadButton";
|
||||
import { LogoutButton } from "./components/LogoutButton";
|
||||
import { MusicTrackRow } from "./components/MusicTrackRow";
|
||||
import { PlaylistTitleInput } from "./components/PlaylistTitleInput";
|
||||
import { SidePanel } from "./components/SidePanel";
|
||||
@@ -71,8 +66,6 @@ export function HomePage({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
|
||||
});
|
||||
};
|
||||
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen text-gray-800 bg-blue-50">
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
@@ -93,12 +86,12 @@ export function HomePage({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
|
||||
<Button onClick={handleCreatePlaylist}>New playlist</Button>
|
||||
</>
|
||||
)}
|
||||
{!isRootPlaylist && isAuthenticated && (
|
||||
{!isRootPlaylist && (
|
||||
<Button onClick={handlePlaylistShareClick}>
|
||||
Share playlist
|
||||
</Button>
|
||||
)}
|
||||
<AuthButton />
|
||||
<LogoutButton />
|
||||
</div>
|
||||
</div>
|
||||
<ul className="flex flex-col">
|
||||
|
||||
@@ -22,18 +22,18 @@ import {
|
||||
* pattern that best fits your app.
|
||||
*/
|
||||
|
||||
export async function uploadMusicTracks(
|
||||
files: Iterable<File>,
|
||||
isExampleTrack: boolean = false,
|
||||
) {
|
||||
const { root } = await MusicaAccount.getMe().ensureLoaded({
|
||||
export async function uploadMusicTracks(files: Iterable<File>) {
|
||||
const me = await MusicaAccount.getMe().ensureLoaded({
|
||||
root: {
|
||||
rootPlaylist: {
|
||||
tracks: [],
|
||||
},
|
||||
playlists: [],
|
||||
},
|
||||
});
|
||||
|
||||
if (!me) return;
|
||||
|
||||
for (const file of files) {
|
||||
// The ownership object defines the user that owns the created coValues
|
||||
// We are creating a group for each CoValue in order to be able to share them via Playlist
|
||||
@@ -52,24 +52,25 @@ export async function uploadMusicTracks(
|
||||
duration: data.duration,
|
||||
waveform: MusicTrackWaveform.create({ data: data.waveform }, group),
|
||||
title: file.name,
|
||||
isExampleTrack,
|
||||
},
|
||||
group,
|
||||
);
|
||||
|
||||
// The newly created musicTrack can be associated to the
|
||||
// user track list using a simple push call
|
||||
root.rootPlaylist.tracks.push(musicTrack);
|
||||
me.root.rootPlaylist.tracks.push(musicTrack);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createNewPlaylist() {
|
||||
const { root } = await MusicaAccount.getMe().ensureLoaded({
|
||||
const me = await MusicaAccount.getMe().ensureLoaded({
|
||||
root: {
|
||||
playlists: [],
|
||||
},
|
||||
});
|
||||
|
||||
if (!me) throw new Error("Current playlist not resolved");
|
||||
|
||||
// Since playlists are meant to be shared we associate them
|
||||
// to a group which will contain the keys required to get
|
||||
// access to the "owned" values
|
||||
@@ -85,7 +86,7 @@ export async function createNewPlaylist() {
|
||||
|
||||
// Again, we associate the new playlist to the
|
||||
// user by pushing it into the playlists CoList
|
||||
root.playlists.push(playlist);
|
||||
me.root.playlists.push(playlist);
|
||||
|
||||
return playlist;
|
||||
}
|
||||
@@ -151,49 +152,24 @@ export async function updateMusicTrackTitle(track: MusicTrack, title: string) {
|
||||
}
|
||||
|
||||
export async function updateActivePlaylist(playlist?: Playlist) {
|
||||
const { root } = await MusicaAccount.getMe().ensureLoaded({
|
||||
const me = await MusicaAccount.getMe().ensureLoaded({
|
||||
root: {
|
||||
activePlaylist: {},
|
||||
rootPlaylist: {},
|
||||
},
|
||||
});
|
||||
|
||||
root.activePlaylist = playlist ?? root.rootPlaylist;
|
||||
if (!me) return;
|
||||
|
||||
me.root.activePlaylist = playlist ?? me.root.rootPlaylist;
|
||||
}
|
||||
|
||||
export async function updateActiveTrack(track: MusicTrack) {
|
||||
const { root } = await MusicaAccount.getMe().ensureLoaded({
|
||||
const me = await MusicaAccount.getMe().ensureLoaded({
|
||||
root: {},
|
||||
});
|
||||
|
||||
root.activeTrack = track;
|
||||
}
|
||||
|
||||
export async function onAnonymousAccountDiscarded(
|
||||
anonymousAccount: MusicaAccount,
|
||||
) {
|
||||
const { root: anonymousAccountRoot } = await anonymousAccount.ensureLoaded({
|
||||
root: {
|
||||
rootPlaylist: {
|
||||
tracks: [{}],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const me = await MusicaAccount.getMe().ensureLoaded({
|
||||
root: {
|
||||
rootPlaylist: {
|
||||
tracks: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
for (const track of anonymousAccountRoot.rootPlaylist.tracks) {
|
||||
if (track.isExampleTrack) continue;
|
||||
|
||||
const trackGroup = track._owner.castAs(Group);
|
||||
trackGroup.addMember(me, "admin");
|
||||
|
||||
me.root.rootPlaylist.tracks.push(track);
|
||||
}
|
||||
if (!me) return;
|
||||
|
||||
me.root.activeTrack = track;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ export function InvitePage() {
|
||||
},
|
||||
});
|
||||
|
||||
if (!me) return;
|
||||
|
||||
if (
|
||||
playlist &&
|
||||
!me.root.playlists.some((item) => playlist.id === item?.id)
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export const apiKey = "music-player-example-jazz@garden.co";
|
||||
@@ -1,40 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useAccount, useIsAuthenticated } from "jazz-react";
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { AuthModal } from "./AuthModal";
|
||||
|
||||
export function AuthButton() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const { logOut } = useAccount();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
|
||||
function handleSignOut() {
|
||||
logOut();
|
||||
navigate("/");
|
||||
}
|
||||
|
||||
if (isAuthenticated) {
|
||||
return (
|
||||
<Button variant="outline" onClick={handleSignOut}>
|
||||
Sign out
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => setOpen(true)}
|
||||
className="bg-white text-black hover:bg-gray-100"
|
||||
>
|
||||
Sign up
|
||||
</Button>
|
||||
<AuthModal open={open} onOpenChange={setOpen} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { useAccount, usePasskeyAuth } from "jazz-react";
|
||||
import { useState } from "react";
|
||||
|
||||
interface AuthModalProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export function AuthModal({ open, onOpenChange }: AuthModalProps) {
|
||||
const [username, setUsername] = useState("");
|
||||
const [isSignUp, setIsSignUp] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const { me } = useAccount({
|
||||
root: {
|
||||
rootPlaylist: {
|
||||
tracks: [{}],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const auth = usePasskeyAuth({
|
||||
appName: "Jazz Music Player",
|
||||
});
|
||||
|
||||
const handleViewChange = () => {
|
||||
setIsSignUp(!isSignUp);
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
if (isSignUp) {
|
||||
await auth.signUp(username);
|
||||
} else {
|
||||
await auth.logIn();
|
||||
}
|
||||
onOpenChange(false);
|
||||
} catch (error) {
|
||||
setError(error instanceof Error ? error.message : "Unknown error");
|
||||
}
|
||||
};
|
||||
|
||||
const shouldShowTransferRootPlaylist =
|
||||
!isSignUp &&
|
||||
me?.root.rootPlaylist.tracks.some((track) => !track.isExampleTrack);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl font-bold">
|
||||
{isSignUp ? "Create account" : "Welcome back"}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{isSignUp
|
||||
? "Sign up to enable network sync and share your playlists with others"
|
||||
: "Changes done before logging in will be lost"}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{isSignUp && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="username">Username</Label>
|
||||
<Input
|
||||
id="username"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
placeholder="Enter your username"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{error && <div className="text-sm text-red-500">{error}</div>}
|
||||
{shouldShowTransferRootPlaylist && (
|
||||
<div className="text-sm text-red-500">
|
||||
You have tracks in your root playlist that are not example tracks.
|
||||
If you log in with a passkey, your playlists will be transferred
|
||||
to your logged account.
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-4">
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
{isSignUp ? "Sign up with passkey" : "Login with passkey"}
|
||||
</Button>
|
||||
<div className="text-center text-sm">
|
||||
{isSignUp ? "Already have an account?" : "Don't have an account?"}{" "}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleViewChange}
|
||||
className="text-blue-600 hover:underline"
|
||||
>
|
||||
{isSignUp ? "Login" : "Sign up"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { useIsAuthenticated } from "jazz-react";
|
||||
import { Info } from "lucide-react";
|
||||
|
||||
export function LocalOnlyTag() {
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
|
||||
if (isAuthenticated) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="inline-flex items-center gap-1.5 cursor-help">
|
||||
<Badge variant="default" className="h-5 text-xs font-normal">
|
||||
Local only
|
||||
</Badge>
|
||||
<Info className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-[250px]">
|
||||
<p>
|
||||
Sign up to enable network sync and share your playlists with others
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useAccount } from "jazz-react";
|
||||
import { useNavigate, useParams } from "react-router";
|
||||
import { LocalOnlyTag } from "./LocalOnlyTag";
|
||||
|
||||
export function SidePanel() {
|
||||
const { playlistId } = useParams();
|
||||
@@ -26,7 +25,7 @@ export function SidePanel() {
|
||||
|
||||
return (
|
||||
<aside className="w-64 p-6 bg-white overflow-y-auto">
|
||||
<div className="flex items-center mb-1">
|
||||
<div className="flex items-center mb-6">
|
||||
<svg
|
||||
className="w-8 h-8 mr-2"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -47,9 +46,6 @@ export function SidePanel() {
|
||||
</svg>
|
||||
<span className="text-xl font-bold text-blue-600">Music Player</span>
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<LocalOnlyTag />
|
||||
</div>
|
||||
<nav>
|
||||
<h2 className="mb-2 text-sm font-semibold text-gray-600">Playlists</h2>
|
||||
<ul className="space-y-1">
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { type VariantProps, cva } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants };
|
||||
@@ -1,120 +0,0 @@
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { X } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Dialog = DialogPrimitive.Root;
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger;
|
||||
|
||||
const DialogPortal = DialogPrimitive.Portal;
|
||||
|
||||
const DialogClose = DialogPrimitive.Close;
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
));
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DialogHeader.displayName = "DialogHeader";
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DialogFooter.displayName = "DialogFooter";
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogClose,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Input.displayName = "Input";
|
||||
|
||||
export { Input };
|
||||
@@ -1,24 +0,0 @@
|
||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||
import { type VariantProps, cva } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
||||
);
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Label.displayName = LabelPrimitive.Root.displayName;
|
||||
|
||||
export { Label };
|
||||
@@ -1,30 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider;
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root;
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||
@@ -9,6 +9,8 @@ export async function getNextTrack() {
|
||||
},
|
||||
});
|
||||
|
||||
if (!me) return;
|
||||
|
||||
const tracks = me.root.activePlaylist.tracks;
|
||||
const activeTrack = me.root._refs.activeTrack;
|
||||
|
||||
@@ -28,6 +30,8 @@ export async function getPrevTrack() {
|
||||
},
|
||||
});
|
||||
|
||||
if (!me) return;
|
||||
|
||||
const tracks = me.root.activePlaylist.tracks;
|
||||
const activeTrack = me.root._refs.activeTrack;
|
||||
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import { MusicaAccount } from "@/1_schema";
|
||||
import { useAccount } from "jazz-react";
|
||||
import { useEffect } from "react";
|
||||
import { uploadMusicTracks } from "../4_actions";
|
||||
|
||||
export function useUploadExampleData() {
|
||||
const { me } = useAccount();
|
||||
|
||||
useEffect(() => {
|
||||
uploadOnboardingData();
|
||||
}, [me.id]);
|
||||
}, []);
|
||||
}
|
||||
|
||||
async function uploadOnboardingData() {
|
||||
@@ -16,6 +13,8 @@ async function uploadOnboardingData() {
|
||||
root: {},
|
||||
});
|
||||
|
||||
if (!me) throw new Error("Me not resolved");
|
||||
|
||||
if (me.root.exampleDataLoaded) return;
|
||||
|
||||
me.root.exampleDataLoaded = true;
|
||||
@@ -23,7 +22,7 @@ async function uploadOnboardingData() {
|
||||
try {
|
||||
const trackFile = await (await fetch("/example.mp3")).blob();
|
||||
|
||||
await uploadMusicTracks([new File([trackFile], "Example song")], true);
|
||||
await uploadMusicTracks([new File([trackFile], "Example song")]);
|
||||
} catch (error) {
|
||||
me.root.exampleDataLoaded = false;
|
||||
throw error;
|
||||
|
||||
@@ -1,84 +1,44 @@
|
||||
import { BrowserContext, test } from "@playwright/test";
|
||||
import { test } from "@playwright/test";
|
||||
import { HomePage } from "./pages/HomePage";
|
||||
import { LoginPage } from "./pages/LoginPage";
|
||||
|
||||
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
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,
|
||||
});
|
||||
});
|
||||
}
|
||||
test("create a new playlist and share", async ({ page }) => {
|
||||
const loginPage = new LoginPage(page);
|
||||
|
||||
// Configure the authenticator
|
||||
test.beforeEach(async ({ context }) => {
|
||||
// Enable virtual authenticator environment
|
||||
await mockAuthenticator(context);
|
||||
});
|
||||
await loginPage.goto();
|
||||
await loginPage.fillUsername("S. Mario");
|
||||
await loginPage.signup();
|
||||
|
||||
test("create a new playlist and share", async ({
|
||||
page: marioPage,
|
||||
browser,
|
||||
}) => {
|
||||
await marioPage.goto("/");
|
||||
|
||||
const marioHome = new HomePage(marioPage);
|
||||
const homePage = new HomePage(page);
|
||||
|
||||
// The example song should be loaded
|
||||
await marioHome.expectMusicTrack("Example song");
|
||||
await marioHome.editTrackTitle("Example song", "Super Mario World");
|
||||
await homePage.expectMusicTrack("Example song");
|
||||
await homePage.editTrackTitle("Example song", "Super Mario World");
|
||||
|
||||
await marioHome.createPlaylist();
|
||||
await marioHome.editPlaylistTitle("Save the princess");
|
||||
await homePage.createPlaylist();
|
||||
await homePage.editPlaylistTitle("Save the princess");
|
||||
|
||||
await marioHome.navigateToPlaylist("All tracks");
|
||||
await marioHome.addTrackToPlaylist("Super Mario World", "Save the princess");
|
||||
await homePage.navigateToPlaylist("All tracks");
|
||||
await homePage.addTrackToPlaylist("Super Mario World", "Save the princess");
|
||||
|
||||
await marioHome.navigateToPlaylist("Save the princess");
|
||||
await marioHome.expectMusicTrack("Super Mario World");
|
||||
await homePage.navigateToPlaylist("Save the princess");
|
||||
await homePage.expectMusicTrack("Super Mario World");
|
||||
|
||||
await marioHome.signUp("Mario");
|
||||
|
||||
const url = await marioHome.getShareLink();
|
||||
const url = await homePage.getShareLink();
|
||||
|
||||
await sleep(4000); // Wait for the sync to complete
|
||||
|
||||
const luigiContext = await browser.newContext();
|
||||
await mockAuthenticator(luigiContext);
|
||||
await homePage.logout();
|
||||
|
||||
const luigiPage = await luigiContext.newPage();
|
||||
await luigiPage.goto("/");
|
||||
await loginPage.goto();
|
||||
await loginPage.fillUsername("Luigi");
|
||||
await loginPage.signup();
|
||||
|
||||
const luigiHome = new HomePage(luigiPage);
|
||||
await page.goto(url);
|
||||
|
||||
await luigiHome.signUp("Luigi");
|
||||
|
||||
await luigiPage.goto(url);
|
||||
|
||||
await luigiHome.expectMusicTrack("Super Mario World");
|
||||
await luigiHome.playMusicTrack("Super Mario World");
|
||||
await luigiHome.expectActiveTrackPlaying();
|
||||
await homePage.expectMusicTrack("Super Mario World");
|
||||
await homePage.playMusicTrack("Super Mario World");
|
||||
await homePage.expectActiveTrackPlaying();
|
||||
});
|
||||
|
||||
@@ -95,17 +95,6 @@ export class HomePage {
|
||||
.click();
|
||||
}
|
||||
|
||||
async signUp(name: string) {
|
||||
await this.page.getByRole("button", { name: "Sign up" }).click();
|
||||
await this.page.getByRole("textbox", { name: "Username" }).fill(name);
|
||||
await this.page
|
||||
.getByRole("button", { name: "Sign up with passkey" })
|
||||
.click();
|
||||
await expect(
|
||||
this.page.getByRole("button", { name: "Sign out" }),
|
||||
).toBeVisible();
|
||||
}
|
||||
|
||||
async logout() {
|
||||
await this.logoutButton.click();
|
||||
}
|
||||
|
||||
40
examples/music-player/tests/pages/LoginPage.ts
Normal file
40
examples/music-player/tests/pages/LoginPage.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.50.1",
|
||||
"@playwright/test": "^1.46.1",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@vitejs/plugin-react": "^4.3.3",
|
||||
@@ -31,6 +31,6 @@
|
||||
"postcss": "^8.4.27",
|
||||
"tailwindcss": "^3.4.15",
|
||||
"typescript": "~5.6.2",
|
||||
"vite": "^6.0.11"
|
||||
"vite": "^5.4.10"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export const apiKey = "onboarding-example-jazz@garden.co";
|
||||
@@ -1,15 +1,29 @@
|
||||
import App from "@/App.tsx";
|
||||
import "@/index.css";
|
||||
import { HRAccount } from "@/schema.ts";
|
||||
import { JazzProvider } from "jazz-react";
|
||||
import { DemoAuthBasicUI, JazzProvider, useDemoAuth } from "jazz-react";
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { apiKey } from "./apiKey";
|
||||
|
||||
const peer =
|
||||
(new URL(window.location.href).searchParams.get(
|
||||
"peer",
|
||||
) as `ws://${string}`) ?? `wss://cloud.jazz.tools/?key=${apiKey}`;
|
||||
) as `ws://${string}`) ??
|
||||
"wss://cloud.jazz.tools/?key=onboarding-example-jazz@garden.co";
|
||||
|
||||
function JazzAndAuth({ children }: { children: React.ReactNode }) {
|
||||
const [auth, authState] = useDemoAuth();
|
||||
return (
|
||||
<>
|
||||
<JazzProvider AccountSchema={HRAccount} auth={auth} peer={peer}>
|
||||
{children}
|
||||
</JazzProvider>
|
||||
{authState.state !== "signedIn" && (
|
||||
<DemoAuthBasicUI appName="Jazz Onboarding" state={authState} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
declare module "jazz-react" {
|
||||
interface Register {
|
||||
@@ -19,13 +33,8 @@ declare module "jazz-react" {
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<JazzProvider
|
||||
AccountSchema={HRAccount}
|
||||
sync={{
|
||||
peer,
|
||||
}}
|
||||
>
|
||||
<JazzAndAuth>
|
||||
<App />
|
||||
</JazzProvider>
|
||||
</JazzAndAuth>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
||||
@@ -16,6 +16,25 @@ async function scrollToBottom(page: Page) {
|
||||
});
|
||||
}
|
||||
|
||||
const login = async ({
|
||||
page,
|
||||
userName,
|
||||
loginAs = false,
|
||||
}: {
|
||||
page: Page;
|
||||
userName: string;
|
||||
loginAs?: boolean;
|
||||
}) => {
|
||||
const loginPage = new LoginPage(page);
|
||||
await loginPage.goto("/");
|
||||
if (loginAs) {
|
||||
await loginPage.loginAs(userName);
|
||||
} else {
|
||||
await loginPage.fillUsername(userName);
|
||||
await loginPage.signup();
|
||||
}
|
||||
};
|
||||
|
||||
test.describe("Admin onboarding flow", () => {
|
||||
let browser: Browser;
|
||||
let adminContext: BrowserContext;
|
||||
@@ -35,7 +54,7 @@ test.describe("Admin onboarding flow", () => {
|
||||
|
||||
test("Create and delete flow", async () => {
|
||||
const adminPage = await adminContext.newPage();
|
||||
await adminPage.goto("/");
|
||||
await login({ page: adminPage, userName: "HR specialist" });
|
||||
const adminHomePage = new HomePage(adminPage);
|
||||
await adminHomePage.createEmployee("Paul");
|
||||
await adminHomePage.createEmployee("Sean");
|
||||
@@ -51,8 +70,10 @@ test.describe("Admin onboarding flow", () => {
|
||||
const adminPage = await adminContext.newPage();
|
||||
const writerPage = await writerContext.newPage();
|
||||
|
||||
await adminPage.goto("/");
|
||||
await writerPage.goto("/");
|
||||
const adminUser = "HR specialist";
|
||||
const writerUser = "Invitee";
|
||||
await login({ page: adminPage, userName: adminUser });
|
||||
await login({ page: writerPage, userName: writerUser });
|
||||
|
||||
const adminHomePage = new HomePage(adminPage);
|
||||
await adminHomePage.createEmployee("Paul");
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
// vite.config.ts
|
||||
import path from "path";
|
||||
import react from "file:///Users/brad/dev/jazz/node_modules/@vitejs/plugin-react-swc/index.mjs";
|
||||
import topLevelAwait from "file:///Users/brad/dev/jazz/node_modules/vite-plugin-top-level-await/exports/import.mjs";
|
||||
import { defineConfig } from "file:///Users/brad/dev/jazz/node_modules/vite/dist/node/index.js";
|
||||
var __vite_injected_original_dirname =
|
||||
"/Users/brad/dev/jazz/examples/onboarding";
|
||||
var vite_config_default = defineConfig({
|
||||
plugins: [react(), topLevelAwait()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__vite_injected_original_dirname, "./src"),
|
||||
},
|
||||
},
|
||||
build: {
|
||||
minify: false,
|
||||
},
|
||||
});
|
||||
export { vite_config_default as default };
|
||||
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvYnJhZC9kZXYvamF6ei9leGFtcGxlcy9vbmJvYXJkaW5nXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvYnJhZC9kZXYvamF6ei9leGFtcGxlcy9vbmJvYXJkaW5nL3ZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy9icmFkL2Rldi9qYXp6L2V4YW1wbGVzL29uYm9hcmRpbmcvdml0ZS5jb25maWcudHNcIjtpbXBvcnQgcGF0aCBmcm9tIFwicGF0aFwiO1xuaW1wb3J0IHJlYWN0IGZyb20gXCJAdml0ZWpzL3BsdWdpbi1yZWFjdC1zd2NcIjtcbmltcG9ydCB7IGRlZmluZUNvbmZpZyB9IGZyb20gXCJ2aXRlXCI7XG5pbXBvcnQgdG9wTGV2ZWxBd2FpdCBmcm9tIFwidml0ZS1wbHVnaW4tdG9wLWxldmVsLWF3YWl0XCI7XG5cbi8vIGh0dHBzOi8vdml0ZWpzLmRldi9jb25maWcvXG5leHBvcnQgZGVmYXVsdCBkZWZpbmVDb25maWcoe1xuICBwbHVnaW5zOiBbcmVhY3QoKSwgdG9wTGV2ZWxBd2FpdCgpXSxcbiAgcmVzb2x2ZToge1xuICAgIGFsaWFzOiB7XG4gICAgICBcIkBcIjogcGF0aC5yZXNvbHZlKF9fZGlybmFtZSwgXCIuL3NyY1wiKSxcbiAgICB9LFxuICB9LFxuICBidWlsZDoge1xuICAgIG1pbmlmeTogZmFsc2UsXG4gIH0sXG59KTtcbiJdLAogICJtYXBwaW5ncyI6ICI7QUFBMFMsT0FBTyxVQUFVO0FBQzNULE9BQU8sV0FBVztBQUNsQixTQUFTLG9CQUFvQjtBQUM3QixPQUFPLG1CQUFtQjtBQUgxQixJQUFNLG1DQUFtQztBQU16QyxJQUFPLHNCQUFRLGFBQWE7QUFBQSxFQUMxQixTQUFTLENBQUMsTUFBTSxHQUFHLGNBQWMsQ0FBQztBQUFBLEVBQ2xDLFNBQVM7QUFBQSxJQUNQLE9BQU87QUFBQSxNQUNMLEtBQUssS0FBSyxRQUFRLGtDQUFXLE9BQU87QUFBQSxJQUN0QztBQUFBLEVBQ0Y7QUFBQSxFQUNBLE9BQU87QUFBQSxJQUNMLFFBQVE7QUFBQSxFQUNWO0FBQ0YsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K
|
||||
@@ -30,6 +30,6 @@
|
||||
"postcss": "^8.4.27",
|
||||
"tailwindcss": "^3.4.15",
|
||||
"typescript": "~5.6.2",
|
||||
"vite": "^6.0.11"
|
||||
"vite": "^5.4.10"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export const apiKey = "organization-example@garden.co";
|
||||
@@ -1,8 +1,7 @@
|
||||
import { JazzProvider } from "jazz-react";
|
||||
import { DemoAuthBasicUI, JazzProvider, useDemoAuth } from "jazz-react";
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "./index.css";
|
||||
import { apiKey } from "@/apiKey.ts";
|
||||
import { RouterProvider, createHashRouter } from "react-router-dom";
|
||||
import { AcceptInvitePage } from "./AcceptInvitePage.tsx";
|
||||
import { HomePage } from "./HomePage.tsx";
|
||||
@@ -28,6 +27,26 @@ function Router() {
|
||||
return <RouterProvider router={router}></RouterProvider>;
|
||||
}
|
||||
|
||||
function JazzAndAuth({ children }: { children: React.ReactNode }) {
|
||||
const [auth, authState] = useDemoAuth();
|
||||
|
||||
return (
|
||||
<>
|
||||
<JazzProvider
|
||||
AccountSchema={JazzAccount}
|
||||
auth={auth}
|
||||
peer="wss://cloud.jazz.tools/?key=organization-example@garden.co"
|
||||
>
|
||||
{children}
|
||||
</JazzProvider>
|
||||
|
||||
{authState.state !== "signedIn" && (
|
||||
<DemoAuthBasicUI appName="Organization" state={authState} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
declare module "jazz-react" {
|
||||
interface Register {
|
||||
Account: JazzAccount;
|
||||
@@ -36,13 +55,8 @@ declare module "jazz-react" {
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<JazzProvider
|
||||
AccountSchema={JazzAccount}
|
||||
sync={{
|
||||
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
|
||||
}}
|
||||
>
|
||||
<JazzAndAuth>
|
||||
<Router />
|
||||
</JazzProvider>
|
||||
</JazzAndAuth>
|
||||
</StrictMode>,
|
||||
);
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^3.0.0",
|
||||
"@sveltejs/kit": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^4.0.1",
|
||||
"@types/eslint": "^9.6.0",
|
||||
"eslint": "^9.7.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
@@ -29,7 +29,7 @@
|
||||
"svelte-check": "^4.0.0",
|
||||
"typescript": "~5.6.2",
|
||||
"typescript-eslint": "^8.0.0",
|
||||
"vite": "^6.0.11"
|
||||
"vite": "^5.4.10"
|
||||
},
|
||||
"dependencies": {
|
||||
"jazz-svelte": "workspace:*"
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export const apiKey = "minimal-svelte-auth-passkey@garden.co"
|
||||
@@ -1,20 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { JazzProvider, PasskeyAuthBasicUI } from 'jazz-svelte';
|
||||
import {apiKey} from '../apiKey';
|
||||
import { JazzProvider, PasskeyAuthBasicUI, usePasskeyAuth } from 'jazz-svelte';
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
let auth = usePasskeyAuth({ appName: 'minimal-svelte-auth-passkey' });
|
||||
|
||||
$inspect(auth.state);
|
||||
</script>
|
||||
|
||||
<JazzProvider
|
||||
sync={{
|
||||
peer: `wss://cloud.jazz.tools/?key={apiKey}`,
|
||||
when: "signedUp",
|
||||
}}
|
||||
<div
|
||||
style="width: 100vw; height: 100vh; display: flex; align-items: center; justify-content: center;"
|
||||
>
|
||||
<PasskeyAuthBasicUI appName="minimal-svelte-auth-passkey">
|
||||
{@render children?.()}
|
||||
</PasskeyAuthBasicUI>
|
||||
</JazzProvider>
|
||||
<PasskeyAuthBasicUI state={auth.state} />
|
||||
|
||||
{#if auth.current}
|
||||
<JazzProvider
|
||||
auth={auth.current}
|
||||
peer="wss://cloud.jazz.tools/?key=minimal-svelte-auth-passkey@garden.co"
|
||||
>
|
||||
{@render children?.()}
|
||||
</JazzProvider>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
:global(html, body) {
|
||||
|
||||
@@ -23,6 +23,6 @@
|
||||
"@vitejs/plugin-react": "^4.3.3",
|
||||
"globals": "^15.11.0",
|
||||
"typescript": "~5.6.2",
|
||||
"vite": "^6.0.11"
|
||||
"vite": "^5.4.10"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export const apiKey = "minimal-auth-passkey-example@garden.co";
|
||||
@@ -1,22 +1,24 @@
|
||||
import { JazzProvider, PasskeyAuthBasicUI } from "jazz-react";
|
||||
import { JazzProvider, PasskeyAuthBasicUI, usePasskeyAuth } from "jazz-react";
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import App from "./App.tsx";
|
||||
import "./index.css";
|
||||
import { apiKey } from "./apiKey.ts";
|
||||
|
||||
function JazzAndAuth({ children }: { children: React.ReactNode }) {
|
||||
const [auth, state] = usePasskeyAuth({
|
||||
appName: "Jazz Minimal Auth Passkey Example",
|
||||
});
|
||||
|
||||
return (
|
||||
<JazzProvider
|
||||
sync={{
|
||||
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
|
||||
when: "signedUp",
|
||||
}}
|
||||
>
|
||||
<PasskeyAuthBasicUI appName="Jazz Minimal Auth Passkey Example">
|
||||
<>
|
||||
<JazzProvider
|
||||
auth={auth}
|
||||
peer="wss://cloud.jazz.tools/?key=minimal-auth-passkey-example@garden.co"
|
||||
>
|
||||
{children}
|
||||
</PasskeyAuthBasicUI>
|
||||
</JazzProvider>
|
||||
</JazzProvider>
|
||||
<PasskeyAuthBasicUI state={state} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
24
examples/passphrase/.gitignore
vendored
24
examples/passphrase/.gitignore
vendored
@@ -1,24 +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?
|
||||
@@ -1,56 +0,0 @@
|
||||
# Passphrase authentication example with Jazz and React
|
||||
|
||||
This is an example of how to use passphrase authentication with Jazz.
|
||||
|
||||
## Getting started
|
||||
|
||||
You can either
|
||||
1. Clone the jazz repository, and run the app within the monorepo.
|
||||
2. Or create a new Jazz project using this example as a template.
|
||||
|
||||
### Using the example as a template
|
||||
|
||||
Create a new Jazz project, and use this example as a template.
|
||||
```bash
|
||||
npx create-jazz-app@latest --example passphrase --project-name passphrase
|
||||
```
|
||||
|
||||
Go to the new project directory.
|
||||
```bash
|
||||
cd passphrase
|
||||
```
|
||||
|
||||
Run the dev server.
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Using the monorepo
|
||||
|
||||
This requires `pnpm` to be installed, see [https://pnpm.io/installation](https://pnpm.io/installation).
|
||||
|
||||
Clone the jazz repository.
|
||||
```bash
|
||||
git clone https://github.com/garden-co/jazz.git
|
||||
```
|
||||
|
||||
Install and build dependencies.
|
||||
```bash
|
||||
pnpm i && npx turbo build
|
||||
```
|
||||
|
||||
Go to the example directory.
|
||||
```bash
|
||||
cd jazz/examples/passphrase/
|
||||
```
|
||||
|
||||
Start the dev server.
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
Open [http://localhost:5173](http://localhost:5173) with your browser to see the result.
|
||||
## Questions / problems / feedback
|
||||
|
||||
If you have feedback, let us know on [Discord](https://discord.gg/utDMjHYg42) or open an issue or PR to fix something that seems wrong.
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Jazz | Minimal Auth Passphrase Example</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"name": "passphrase",
|
||||
"private": true,
|
||||
"version": "0.0.39",
|
||||
"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": {
|
||||
"jazz-react": "workspace:*",
|
||||
"jazz-tools": "workspace:*",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.9.4",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@vitejs/plugin-react": "^4.3.3",
|
||||
"globals": "^15.11.0",
|
||||
"typescript": "~5.6.2",
|
||||
"vite": "^6.0.11"
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { useAccount } from "jazz-react";
|
||||
|
||||
function App() {
|
||||
const { logOut } = useAccount();
|
||||
|
||||
return (
|
||||
<main>
|
||||
<h1>You're logged in</h1>
|
||||
<button onClick={() => logOut()}>Logout</button>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -1,68 +0,0 @@
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 0;
|
||||
padding: 0.6em 1.2em;
|
||||
font-weight: 500;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 1rem;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { JazzProvider, PassphraseAuthBasicUI } from "jazz-react";
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import App from "./App.tsx";
|
||||
import "./index.css";
|
||||
import { wordlist } from "./wordlist.ts";
|
||||
|
||||
function JazzAndAuth({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<JazzProvider
|
||||
sync={{
|
||||
peer: "wss://cloud.jazz.tools/?key=minimal-auth-passphrase-example@garden.co",
|
||||
when: "signedUp",
|
||||
}}
|
||||
>
|
||||
<PassphraseAuthBasicUI
|
||||
appName="Jazz Minimal Auth Passphrase Example"
|
||||
wordlist={wordlist}
|
||||
>
|
||||
{children}
|
||||
</PassphraseAuthBasicUI>
|
||||
</JazzProvider>
|
||||
);
|
||||
}
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<JazzAndAuth>
|
||||
<App />
|
||||
</JazzAndAuth>
|
||||
</StrictMode>,
|
||||
);
|
||||
1
examples/passphrase/src/vite-env.d.ts
vendored
1
examples/passphrase/src/vite-env.d.ts
vendored
@@ -1 +0,0 @@
|
||||
/// <reference types="vite/client" />
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user