Skip to content

Commit

Permalink
🧩 ui Add notion component: settings panel
Browse files Browse the repository at this point in the history
  • Loading branch information
steeeee0223 committed Feb 20, 2024
1 parent c625716 commit c1f1b60
Show file tree
Hide file tree
Showing 30 changed files with 983 additions and 5 deletions.
19 changes: 19 additions & 0 deletions apps/storybook/src/stories/notion/settings-panel/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { Meta, StoryObj } from "@storybook/react";

import { SettingsPanel } from "@acme/ui/components";

import { Settings } from "./settings";

const meta = {
title: "notion/Settings Panel",
component: SettingsPanel,
parameters: { layout: "centered" },
tags: ["autodocs"],
} satisfies Meta<typeof SettingsPanel>;
export default meta;

type Story = StoryObj<typeof meta>;

export const Default: Story = {
render: () => <Settings />,
};
24 changes: 24 additions & 0 deletions apps/storybook/src/stories/notion/settings-panel/settings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"use client";

import { useEffect, useState } from "react";
import { SettingsIcon } from "lucide-react";

import { Button, SettingsPanel, useSettings } from "@acme/ui/components";

export const Settings = () => {
const settings = useSettings();
const [isMounted, setIsMounted] = useState(false);

useEffect(() => {
setIsMounted(true);
}, []);

return (
<>
<Button variant="outline" size="icon" onClick={settings.onOpen}>
<SettingsIcon />
</Button>
{isMounted && <SettingsPanel />}
</>
);
};
75 changes: 75 additions & 0 deletions apps/worxpace/src/components/collab-editor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"use client";

import { useEffect, useState } from "react";
import { BlockNoteEditor } from "@blocknote/core";
import { BlockNoteView, useBlockNote } from "@blocknote/react";
import LiveblocksProvider from "@liveblocks/yjs";
import { useTheme } from "next-themes";
import * as Y from "yjs";

import "@blocknote/core/style.css";

import { useRoom, useSelf } from "~/liveblocks.config";

// Collaborative text editor with simple rich text, live cursors, and live avatars
export default function CollaborativeEditor() {
const room = useRoom();
const [doc, setDoc] = useState<Y.Doc>();
const [provider, setProvider] =
useState<LiveblocksProvider<never, never, never, never>>();

// Set up Liveblocks Yjs provider
useEffect(() => {
const yDoc = new Y.Doc();
const yProvider = new LiveblocksProvider(room, yDoc);
setDoc(yDoc);
setProvider(yProvider);

return () => {
yDoc?.destroy();
yProvider?.destroy();
};
}, [room]);

if (!doc || !provider) {
return null;
}

return <BlockNote doc={doc} provider={provider} />;
}

interface EditorProps {
doc: Y.Doc;
provider: LiveblocksProvider<never, never, never, never>;
}

function BlockNote({ doc, provider }: EditorProps) {
// Get user info from Liveblocks authentication endpoint
const userInfo = useSelf((me) => me.info);

const editor: BlockNoteEditor = useBlockNote({
collaboration: {
provider,

// Where to store BlockNote data in the Y.Doc:
fragment: doc.getXmlFragment("document-store"),

// Information for this user:
user: {
name: userInfo?.name ?? "User",
color: userInfo?.picture ?? "",
},
},
});

const { resolvedTheme } = useTheme();

return (
<div>
<BlockNoteView
editor={editor}
theme={resolvedTheme === "dark" ? "dark" : "light"}
/>
</div>
);
}
36 changes: 36 additions & 0 deletions apps/worxpace/src/components/custom-blocks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {
// defaultBlockSchema,
// defaultBlockSpecs,
defaultProps,
} from "@blocknote/core";
import { createReactBlockSpec } from "@blocknote/react";

export const TodoBlock = createReactBlockSpec(
{
type: "todo",
propSchema: {
...defaultProps,
checked: {
default: false,
},
},
content: "none",
},
{
render: ({ block, contentRef }) => {
console.log(block);

return (
<div>
<input type="checkbox" />
<p ref={contentRef}>{block.content}</p>
</div>
);
},
parse: (_element) => {
return {
checked: false,
};
},
},
);
2 changes: 1 addition & 1 deletion apps/worxpace/src/components/room.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ interface RoomProps extends PropsWithChildren {

export function Room({ children, roomId, fallback }: RoomProps) {
return (
<RoomProvider id={roomId} initialPresence={{}}>
<RoomProvider id={roomId} initialPresence={{ cursor: null }}>
<ClientSideSuspense fallback={fallback}>
{() => children}
</ClientSideSuspense>
Expand Down
2 changes: 1 addition & 1 deletion apps/worxpace/src/liveblocks.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const client = createClient({
// and that will automatically be kept in sync. Accessible through the
// `user.presence` property. Must be JSON-serializable.
type Presence = {
// cursor: { x: number, y: number } | null,
cursor: { x: number; y: number } | null;
// ...
};

Expand Down
2 changes: 2 additions & 0 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-tooltip": "^1.0.7",
"class-variance-authority": "^0.7.0",
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/components/notion/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from "./settings-panel";
export * from "./workspace-switcher";
10 changes: 10 additions & 0 deletions packages/ui/src/components/notion/settings-panel/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { render } from "@testing-library/react";

import { SettingsPanel } from ".";

describe("<SettingsPanel />", () => {
it("should render the default text", () => {
const { getByText } = render(<SettingsPanel />);
expect(getByText("Settings Panel")).toBeDefined();
});
});
2 changes: 2 additions & 0 deletions packages/ui/src/components/notion/settings-panel/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./settings-panel";
export * from "./use-settings";
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { PanelData } from "./utils";

const $account = [
"email",
"password",
"verification",
"support",
"logout",
"del",
] as const;

export const myAccount: PanelData<typeof $account> = {
email: { title: "Email", description: "email" },
password: {
title: "Password",
description:
"If you lose access to your school email address, you'll be able to log in using your password.",
},
verification: {
title: "2-step verification",
description:
"Add an additional layer of security to your account during login.",
},
support: {
title: "Support access",
description:
"Grant Notion support temporary access to your account so we can troubleshoot problems or recover content on your behalf. You can revoke access at any time.",
},
logout: {
title: "Log out of all devices",
description:
"Log out of all other active sessions on other devices besides this one.",
},
del: {
title: "Delete my account",
description:
"Permanently delete the account and remove access from all workspaces.",
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { ChangeEvent } from "react";
import { ChevronRight } from "lucide-react";

import {
Avatar,
AvatarFallback,
AvatarImage,
Button,
Input,
Label,
Switch,
} from "@/components/ui";
import { mockUser } from "../../workspace-switcher/mock";
import { Section, SectionItem, SectionSeparator } from "../section";
import { myAccount } from "./account.data";

export const Account = () => {
const user = mockUser;
const handleUpdateName = (e: ChangeEvent<HTMLInputElement>) =>
console.log(`Changed preffered name to ${e.currentTarget.value}`);

return (
<>
<Section title="My profile">
<div className="flex flex-col">
<div className="flex items-center">
<Avatar className="size-[60px]">
<AvatarImage src="https://github.com/shadcn.png" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<div className="ml-5 w-[250px]">
<Label
className="mb-1 block text-xs text-primary/65"
htmlFor="username"
>
Preferred name
</Label>
<Input
type="username"
id="username"
value={user.name}
onChange={handleUpdateName}
className="bg-primary/6 h-7 select-none rounded-sm text-sm"
/>
</div>
</div>
</div>
</Section>
<SectionSeparator />
<Section title="Account security">
<SectionItem title={myAccount.email.title} description={user.email}>
<Button
variant="outline"
size="sm"
className="select-none hover:bg-primary/10"
>
Change email
</Button>
</SectionItem>
<SectionSeparator size="sm" />
<SectionItem {...myAccount.password}>
<Switch />
</SectionItem>
<SectionSeparator size="sm" />
<SectionItem {...myAccount.verification}>
<Switch disabled />
</SectionItem>
</Section>
<SectionSeparator />
<Section title="Support">
<SectionItem {...myAccount.support}>
<Switch />
</SectionItem>
<SectionSeparator size="sm" />
<SectionItem {...myAccount.logout}>
<Button variant="ghost" size="icon-sm" className="text-primary/35">
<ChevronRight className="h-4 w-4" />
</Button>
</SectionItem>
<SectionSeparator size="sm" />
<SectionItem {...myAccount.del} titleProps="text-[#eb5757]">
<Button variant="ghost" size="icon-sm" className="text-primary/35">
<ChevronRight className="h-4 w-4" />
</Button>
</SectionItem>
</Section>
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./account";
export * from "./settings";
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { Option } from "../section";
import { PanelData } from "./utils";

const $settings = [
"appearance",
"openStart",
"openLinks",
"setTimezone",
"timezone",
"cookie",
"viewHistory",
"profile",
] as const;

export const mySettings: PanelData<typeof $settings> = {
appearance: {
title: "Appearance",
description: "Customize how Notion looks on your device.",
},
openStart: {
title: "Open on start",
description:
"Choose what to show when Notion starts or when you switch workspaces.",
},
openLinks: {
title: "Open links in desktop app",
description: "You must have the Mac or Windows app installed",
},
setTimezone: {
title: "Set timezone automatically using your location",
description:
"Reminders, notifications and emails are delivered based on your time zone.",
},
timezone: { title: "Time Zone", description: "Current time zone setting." },
cookie: {
title: "Cookie settings",
description: "Customize cookies. See Cookie Notice for details.",
},
viewHistory: {
title: "Show my view history",
description:
"People with edit or full access will be able to see when you’ve viewed a page. Leran more.",
},
profile: {
title: "Profile discoverability",
description:
"Users with your email can see your name and profile picture when inviting you to a new workspace. Learn more.",
},
};

export const appearanceOptions: Option[] = [
{ label: "Use system setting", value: "system" },
{ label: "Light", value: "light" },
{ label: "Dark", value: "dark" },
];
export const openStartOptions: Option[] = [
{ label: "Last visited page", value: "last" },
{ label: "Top page in sidebar", value: "top" },
];
Loading

0 comments on commit c1f1b60

Please sign in to comment.