@@ -15,8 +46,12 @@ export default function Page() {
Sign in with ...
- {Object.values(accounts).map(({ name, avatarUrl }) => (
-
+ {Object.values(accounts).map(({ id, name, avatarUrl }) => (
+ login(id)}
+ />
))}
diff --git a/apps/playground/src/app/(platform)/(core)/home/[workspaceId]/page.tsx b/apps/playground/src/app/(platform)/(core)/home/[workspaceId]/page.tsx
new file mode 100644
index 00000000..301c44d7
--- /dev/null
+++ b/apps/playground/src/app/(platform)/(core)/home/[workspaceId]/page.tsx
@@ -0,0 +1,51 @@
+"use client";
+
+import Image from "next/image";
+import { PlusCircle } from "lucide-react";
+
+import { useBoundStore } from "@swy/notion";
+import { Button } from "@swy/ui/shadcn";
+
+interface Params {
+ params: { workspaceId: string };
+}
+
+const HomePage = ({ params: _ }: Params) => {
+ const activeWorkspace = useBoundStore(
+ (state) => state.workspaces[state.activeWorkspace ?? ""],
+ );
+ /** Action */
+ const onSubmit = () => {
+ // TODO create doc
+ };
+
+ return (
+
+
+
+
+ Welcome to {activeWorkspace?.name ?? "WorXpace"}
+
+
+
+ );
+};
+
+export default HomePage;
diff --git a/apps/playground/src/app/(platform)/(core)/layout.tsx b/apps/playground/src/app/(platform)/(core)/layout.tsx
new file mode 100644
index 00000000..3b597752
--- /dev/null
+++ b/apps/playground/src/app/(platform)/(core)/layout.tsx
@@ -0,0 +1,102 @@
+"use client";
+
+import React, { useEffect } from "react";
+import { useRouter } from "next/navigation";
+
+import {
+ Navbar,
+ PageProvider,
+ SettingsStore,
+ Sidebar2,
+ useBoundStore,
+ WorkspaceSwitcher2,
+} from "@swy/notion";
+import { useSidebarLayout } from "@swy/ui/hooks";
+import { cn } from "@swy/ui/lib";
+import {
+ ResizableHandle,
+ ResizablePanel,
+ ResizablePanelGroup,
+} from "@swy/ui/shadcn";
+
+import { fallbackUser } from "~/constants";
+import { useAppActions, useAppState } from "~/hooks";
+
+export default function CoreLayout({ children }: React.PropsWithChildren) {
+ const router = useRouter();
+ const { minSize, ref, collapse, expand, isResizing, isMobile, isCollapsed } =
+ useSidebarLayout("group", "sidebar", 240);
+
+ const { isSignedIn } = useAppState();
+ const { goToOnboarding, selectWorkspace, logout } = useAppActions();
+ const user = useBoundStore((state) => state.user);
+ const workspaces = useBoundStore((state) => state.workspaces);
+ const activeWorkspace = useBoundStore((state) => state.activeWorkspace);
+
+ useEffect(() => {
+ if (!isSignedIn || !activeWorkspace) router.push("/sign-in");
+ }, [activeWorkspace, isSignedIn, router]);
+
+ return (
+ activeWorkspace && (
+
+
+
+
+
+
+ {children}
+
+
+
+ )
+ );
+}
diff --git a/apps/playground/src/app/(platform)/layout.tsx b/apps/playground/src/app/(platform)/layout.tsx
index 12c23dc3..ac44c52f 100644
--- a/apps/playground/src/app/(platform)/layout.tsx
+++ b/apps/playground/src/app/(platform)/layout.tsx
@@ -1,19 +1,27 @@
"use client";
import React, { useEffect } from "react";
+import { Toaster } from "sonner";
+
+import { ModalProvider } from "@swy/ui/shared";
-import { mockDB } from "~/db";
import { useMockDB } from "~/hooks";
-export default function Layout({ children }: React.PropsWithChildren) {
- const { update } = useMockDB();
+export default function PlatformLayout({ children }: React.PropsWithChildren) {
+ const { setupDB } = useMockDB();
useEffect(() => {
- update(mockDB);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
+ setupDB();
+ }, [setupDB]);
return (
-
{children}
+ <>
+
+
+
+ {children}
+
+
+ >
);
}
diff --git a/apps/playground/src/constants.ts b/apps/playground/src/constants.ts
new file mode 100644
index 00000000..df72d30d
--- /dev/null
+++ b/apps/playground/src/constants.ts
@@ -0,0 +1,19 @@
+import type { IconInfo } from "@swy/ui/shared";
+import { type User } from "@swy/validators";
+
+export const fallbackIcon: IconInfo = { type: "text", text: "W" };
+export const fallbackUser: User = {
+ id: "",
+ name: "",
+ email: "",
+ avatarUrl: "",
+};
+
+// export const fallbackWorkspace: Workspace = {
+// id: "",
+// role: Role.GUEST,
+// name: "",
+// icon: fallbackIcon,
+// members: 0,
+// plan: Plan.FREE,
+// };
diff --git a/apps/playground/src/db/actions/index.ts b/apps/playground/src/db/actions/index.ts
new file mode 100644
index 00000000..5178c366
--- /dev/null
+++ b/apps/playground/src/db/actions/index.ts
@@ -0,0 +1 @@
+export * from "./workspace";
diff --git a/apps/playground/src/db/actions/workspace.ts b/apps/playground/src/db/actions/workspace.ts
new file mode 100644
index 00000000..f6278243
--- /dev/null
+++ b/apps/playground/src/db/actions/workspace.ts
@@ -0,0 +1,35 @@
+import type { Workspace } from "@swy/notion";
+
+import { delay } from "~/lib/utils";
+import type { MockDB } from "../mock-db";
+
+export const findAccount = async (
+ db: Pick
,
+ accountId: string,
+) => {
+ await delay(500);
+ return await Promise.resolve(db.accounts[accountId] ?? null);
+};
+
+export const findAccountMemberships = async (
+ db: Pick,
+ accountId: string,
+): Promise => {
+ await delay(500);
+ const mems = db.memberships.filter((mem) => mem.accountId === accountId);
+ const ws = mems.map((mem) => {
+ const w = db.workspaces[mem.workspaceId]!;
+ const membersCount = db.memberships.filter(
+ (mem) => mem.workspaceId === w.id,
+ ).length;
+ return {
+ id: w.id,
+ role: mem.role,
+ name: w.name,
+ icon: w.icon ?? { type: "text", text: w.name },
+ members: membersCount,
+ plan: w.plan,
+ };
+ });
+ return await Promise.resolve(ws);
+};
diff --git a/apps/playground/src/db/accounts.ts b/apps/playground/src/db/data/accounts.ts
similarity index 96%
rename from apps/playground/src/db/accounts.ts
rename to apps/playground/src/db/data/accounts.ts
index 93c279fe..07ae84c7 100644
--- a/apps/playground/src/db/accounts.ts
+++ b/apps/playground/src/db/data/accounts.ts
@@ -1,4 +1,4 @@
-import { AccountModel } from "./types";
+import type { AccountModel } from "../types";
export enum _USER {
U1 = "a97f4e50-0b72-44f4-a95a-aba319534af5",
diff --git a/apps/playground/src/db/data/index.ts b/apps/playground/src/db/data/index.ts
new file mode 100644
index 00000000..cb56ceb3
--- /dev/null
+++ b/apps/playground/src/db/data/index.ts
@@ -0,0 +1,3 @@
+export * from "./accounts";
+export * from "./memberships";
+export * from "./workspaces";
diff --git a/apps/playground/src/db/memberships.ts b/apps/playground/src/db/data/memberships.ts
similarity index 96%
rename from apps/playground/src/db/memberships.ts
rename to apps/playground/src/db/data/memberships.ts
index c7a9b6a7..7a499a50 100644
--- a/apps/playground/src/db/memberships.ts
+++ b/apps/playground/src/db/data/memberships.ts
@@ -1,7 +1,7 @@
import { Role } from "@swy/validators";
+import type { MembershipModel } from "../types";
import { _USER } from "./accounts";
-import type { MembershipModel } from "./types";
import { _WORKSPACE } from "./workspaces";
export const memberships: MembershipModel[] = [
diff --git a/apps/playground/src/db/workspaces.ts b/apps/playground/src/db/data/workspaces.ts
similarity index 97%
rename from apps/playground/src/db/workspaces.ts
rename to apps/playground/src/db/data/workspaces.ts
index 1e9d5115..b9aea613 100644
--- a/apps/playground/src/db/workspaces.ts
+++ b/apps/playground/src/db/data/workspaces.ts
@@ -1,7 +1,7 @@
import { Plan } from "@swy/validators";
+import type { WorkspaceModel } from "../types";
import { _USER } from "./accounts";
-import type { WorkspaceModel } from "./types";
export enum _WORKSPACE {
W1 = "f12d4c5b-2d3b-4d2b-aef3-8f7319c5d481",
diff --git a/apps/playground/src/db/index.ts b/apps/playground/src/db/index.ts
index 3bbf07b2..7f38a064 100644
--- a/apps/playground/src/db/index.ts
+++ b/apps/playground/src/db/index.ts
@@ -1,16 +1,3 @@
-import { accounts } from "./accounts";
-import { memberships } from "./memberships";
-import type { AccountModel, MembershipModel, WorkspaceModel } from "./types";
-import { workspaces } from "./workspaces";
-
-export interface MockDB {
- accounts: Record;
- workspaces: Record;
- memberships: MembershipModel[];
-}
-
-export const mockDB: MockDB = {
- accounts,
- workspaces,
- memberships,
-};
+export * as actions from "./actions";
+export * from "./mock-db";
+export type * from "./types";
diff --git a/apps/playground/src/db/mock-db.ts b/apps/playground/src/db/mock-db.ts
new file mode 100644
index 00000000..b0c80c0f
--- /dev/null
+++ b/apps/playground/src/db/mock-db.ts
@@ -0,0 +1,14 @@
+import { accounts, memberships, workspaces } from "./data";
+import type { AccountModel, MembershipModel, WorkspaceModel } from "./types";
+
+export interface MockDB {
+ accounts: Record;
+ workspaces: Record;
+ memberships: MembershipModel[];
+}
+
+export const mockDB: MockDB = {
+ accounts,
+ workspaces,
+ memberships,
+};
diff --git a/apps/playground/src/hooks/index.ts b/apps/playground/src/hooks/index.ts
index 2183ad53..7241986e 100644
--- a/apps/playground/src/hooks/index.ts
+++ b/apps/playground/src/hooks/index.ts
@@ -1 +1,3 @@
+export * from "./useAppActions";
+export * from "./useAppControl";
export * from "./useMockDB";
diff --git a/apps/playground/src/hooks/useAppActions.ts b/apps/playground/src/hooks/useAppActions.ts
new file mode 100644
index 00000000..432aeb8f
--- /dev/null
+++ b/apps/playground/src/hooks/useAppActions.ts
@@ -0,0 +1,42 @@
+"use client";
+
+import { useCallback } from "react";
+import { useRouter } from "next/navigation";
+
+import { useBoundStore } from "@swy/notion";
+
+import { useAppState } from "./useAppControl";
+
+export const useAppActions = () => {
+ const router = useRouter();
+ const { signOut } = useAppState();
+ /** Store */
+ const setActiveWorkspace = useBoundStore((state) => state.setActiveWorkspace);
+ const resetStore = useBoundStore((state) => () => {
+ state.resetWorkspaces();
+ state.resetUser();
+ });
+ /** Actions */
+ const goToOnboarding = useCallback(
+ () => router.push("/onboarding"),
+ [router],
+ );
+ const selectWorkspace = useCallback(
+ (workspaceId: string) => {
+ setActiveWorkspace(workspaceId);
+ router.push(`/home/${workspaceId}`);
+ },
+ [router, setActiveWorkspace],
+ );
+
+ const logout = useCallback(() => {
+ resetStore();
+ signOut();
+ }, [resetStore, signOut]);
+
+ return {
+ goToOnboarding,
+ selectWorkspace,
+ logout,
+ };
+};
diff --git a/apps/playground/src/hooks/useAppControl.ts b/apps/playground/src/hooks/useAppControl.ts
new file mode 100644
index 00000000..e4921239
--- /dev/null
+++ b/apps/playground/src/hooks/useAppControl.ts
@@ -0,0 +1,22 @@
+import { create } from "zustand";
+import { persist } from "zustand/middleware";
+
+type AppState = {
+ signIn: (userId: string) => void;
+ signOut: () => void;
+} & (
+ | { isSignedIn: false; userId: null }
+ | { isSignedIn: true; userId: string }
+);
+
+export const useAppState = create()(
+ persist(
+ (set) => ({
+ isSignedIn: false,
+ userId: null,
+ signIn: (userId) => set({ isSignedIn: true, userId }),
+ signOut: () => set({ isSignedIn: false, userId: null }),
+ }),
+ { name: "app" },
+ ),
+);
diff --git a/apps/playground/src/hooks/useMockDB.ts b/apps/playground/src/hooks/useMockDB.ts
index 2bf080db..e57515f3 100644
--- a/apps/playground/src/hooks/useMockDB.ts
+++ b/apps/playground/src/hooks/useMockDB.ts
@@ -1,8 +1,10 @@
"use client";
+import { useCallback } from "react";
import { useLocalStorage } from "usehooks-ts";
-import type { MockDB } from "~/db";
+import { actions, mockDB, type MockDB } from "~/db";
+import { delay } from "~/lib/utils";
const initial: MockDB = {
accounts: {},
@@ -10,12 +12,66 @@ const initial: MockDB = {
memberships: [],
};
+function determineType(input: unknown): "object" | "array" | "else" {
+ return Array.isArray(input)
+ ? "array"
+ : typeof input === "object" && input !== null
+ ? "object"
+ : "else";
+}
+
export const useMockDB = () => {
- const [value, update] = useLocalStorage("mock:db", initial);
+ const [db, update] = useLocalStorage("mock:db", initial);
+ const setupDB = useCallback(() => update(mockDB), [update]);
+ const updateDB = useCallback(
+ async (collection: K, data: MockDB[K]) => {
+ await delay(500);
+ let ok = false;
+ try {
+ update((prev) => {
+ const storeType = determineType(prev[collection]);
+ if (storeType === "else") return prev;
+ return {
+ ...prev,
+ [collection]:
+ storeType === "object"
+ ? { ...prev[collection], ...data }
+ : [...(prev[collection] as unknown[]), ...(data as unknown[])],
+ };
+ });
+ ok = true;
+ } catch (error) {
+ console.error("update db error", error);
+ }
+ return Promise.resolve(ok);
+ },
+ [update],
+ );
+ const resetDB = useCallback(() => update(initial), [update]);
+ /** fetchers */
+ const findAccount = useCallback(
+ (accountId: string) =>
+ actions.findAccount({ accounts: db.accounts }, accountId),
+ [db.accounts],
+ );
+ const findAccountMemberships = useCallback(
+ (accountId: string) =>
+ actions.findAccountMemberships(
+ {
+ workspaces: db.workspaces,
+ memberships: db.memberships,
+ },
+ accountId,
+ ),
+ [db.memberships, db.workspaces],
+ );
return {
- ...value,
- update,
- reset: () => update(initial),
+ setupDB,
+ updateDB,
+ resetDB,
+ /** fetchers */
+ findAccount,
+ findAccountMemberships,
};
};
diff --git a/apps/playground/src/lib/utils.ts b/apps/playground/src/lib/utils.ts
new file mode 100644
index 00000000..86ac6261
--- /dev/null
+++ b/apps/playground/src/lib/utils.ts
@@ -0,0 +1,2 @@
+export const delay = async (timeout: number) =>
+ await new Promise((resolve) => setTimeout(resolve, timeout));
diff --git a/packages/notion/src/slices/use-bound-store.ts b/packages/notion/src/slices/use-bound-store.ts
index de0b9509..a9f5fe9c 100644
--- a/packages/notion/src/slices/use-bound-store.ts
+++ b/packages/notion/src/slices/use-bound-store.ts
@@ -1,4 +1,5 @@
import { create } from "zustand";
+import { persist } from "zustand/middleware";
import { useShallow } from "zustand/react/shallow";
import { createUserSlice, type UserSlice } from "./account";
@@ -7,11 +8,16 @@ import { createWorkspaceSlice, type WorkspaceSlice } from "./workspace";
type Store = UserSlice & WorkspaceSlice & PageSlice;
-const useStore = create((...a) => ({
- ...createUserSlice(...a),
- ...createWorkspaceSlice(...a),
- ...createPageSlice(...a),
-}));
+const useStore = create(
+ persist(
+ (...a) => ({
+ ...createUserSlice(...a),
+ ...createWorkspaceSlice(...a),
+ ...createPageSlice(...a),
+ }),
+ { name: "platform" },
+ ),
+);
export const useBoundStore = (selector: (state: Store) => T) =>
useStore(useShallow(selector));