Skip to content

Commit

Permalink
cluster creation and editing
Browse files Browse the repository at this point in the history
  • Loading branch information
Southclaws committed Jan 14, 2024
1 parent 38c10f4 commit 46feee9
Show file tree
Hide file tree
Showing 44 changed files with 763 additions and 186 deletions.
3 changes: 2 additions & 1 deletion web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"polished": "^4.2.2",
"react": "^18.2.0",
"react-avatar-editor": "^13.0.0",
"react-contenteditable": "^3.3.7",
"react-dom": "^18.2.0",
"react-hook-form": "^7.46.1",
"remark-parse": "^10.0.2",
Expand Down Expand Up @@ -64,4 +65,4 @@
"minimumChangeThreshold": 0,
"showDetails": true
}
}
}
4 changes: 2 additions & 2 deletions web/panda.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ import { admonition } from "src/theme/components/Admonition/admonition.recipe";
import { button } from "src/theme/components/Button/button.recipe";
import { checkbox } from "src/theme/components/Checkbox/checkbox.recipe";
import { heading } from "src/theme/components/Heading/heading.recipe";
import { headingInput } from "src/theme/components/HeadingInput/recipe";
import { input } from "src/theme/components/Input/input.recipe";
import { link } from "src/theme/components/Link/link.recipe";
import { menu } from "src/theme/components/Menu/menu.recipe";
import { popover } from "src/theme/components/Popover/popover.recipe";
import { skeleton } from "src/theme/components/Skeleton/skeleton.recipe";
import { tabs } from "src/theme/components/Tabs/tabs.recipe";
import { titleInput } from "src/theme/components/TitleInput/titleInput.recipe";

// TODO: Dark mode = 40%
const L = "80%";
Expand Down Expand Up @@ -125,7 +125,7 @@ export default defineConfig({
recipes: {
admonition: admonition,
input: input,
titleInput: titleInput,
headingInput: headingInput,
heading: heading,
button: button,
link: link,
Expand Down
30 changes: 20 additions & 10 deletions web/src/app/(dashboard)/directory/[...slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { last } from "lodash";
import { notFound } from "next/navigation";
import { notFound, redirect } from "next/navigation";

import {
ClusterGetOKResponse,
ItemGetOKResponse,
} from "src/api/openapi/schemas";
import { server } from "src/api/server";
import { ClusterScreen } from "src/screens/directory/datagraph/ClusterScreen/ClusterScreen";
import { getTargetSlug } from "src/components/directory/datagraph/utils";
import { ClusterCreateScreen } from "src/screens/directory/datagraph/ClusterCreateScreen/ClusterCreateScreen";
import { ClusterViewerScreen } from "src/screens/directory/datagraph/ClusterViewerScreen/ClusterViewerScreen";
import { ItemScreen } from "src/screens/directory/datagraph/ItemScreen/ItemScreen";
import {
Params,
Expand All @@ -20,26 +21,35 @@ type Props = {
export default async function Page(props: Props) {
const { slug } = ParamsSchema.parse(props.params);

const top = last(slug) ?? "";
const [targetSlug, fallback, isNew] = getTargetSlug(slug);

// TODO: here we're firing two requests to the server, one for a cluster and
// one for the item at the same slug. We should probably have a single request
// that returns either or a 404. We're also not handling other errors either.
const [cluster, item] = await Promise.all([
server<ClusterGetOKResponse>({ url: `/v1/clusters/${top}` }).catch(() => {
// ignore any errors
}),
server<ItemGetOKResponse>({ url: `/v1/items/${top}` }).catch(() => {
server<ClusterGetOKResponse>({ url: `/v1/clusters/${targetSlug}` }).catch(
() => {
// ignore any errors
},
),
server<ItemGetOKResponse>({ url: `/v1/items/${targetSlug}` }).catch(() => {
// ignore any errors
}),
]);

if (cluster) {
return <ClusterScreen slug={top} cluster={cluster} />;
if (isNew) {
return <ClusterCreateScreen />;
}

return <ClusterViewerScreen slug={targetSlug} cluster={cluster} />;
}

if (item) {
return <ItemScreen slug={top} item={item} />;
if (isNew) {
redirect(`/directory/${fallback}`);
}
return <ItemScreen slug={targetSlug} item={item} />;
}

notFound();
Expand Down
5 changes: 5 additions & 0 deletions web/src/app/(dashboard)/directory/new/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { ClusterCreateScreen } from "src/screens/directory/datagraph/ClusterCreateScreen/ClusterCreateScreen";

export default async function Page() {
return <ClusterCreateScreen />;
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { PropsWithChildren, useCallback } from "react";
import { Editable, Slate } from "slate-react";

import { FileDrop } from "../FileDrop/FileDrop";

import { Box } from "@/styled-system/jsx";

import { FileDrop } from "./components/FileDrop/FileDrop";
import { Element } from "./render/Element";
import { Leaf } from "./render/Leaf";
import { Props, useContentComposer } from "./useContentComposer";
Expand All @@ -13,7 +14,8 @@ export function ContentComposer({
children,
...props
}: PropsWithChildren<Props>) {
const { editor, initialValue, onChange } = useContentComposer(props);
const { editor, initialValue, onChange, handleAssetUpload } =
useContentComposer(props);

const renderLeaf = useCallback((props: any) => <Leaf {...props} />, []);
const renderElement = useCallback((props: any) => <Element {...props} />, []);
Expand All @@ -29,7 +31,7 @@ export function ContentComposer({
<Slate editor={editor} initialValue={initialValue} onChange={onChange}>
{children}

<FileDrop onComplete={props.onAssetUpload}>
<FileDrop onComplete={handleAssetUpload}>
<Editable
renderLeaf={renderLeaf}
renderElement={renderElement}
Expand Down
12 changes: 11 additions & 1 deletion web/src/components/content/ContentComposer/useContentComposer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,15 @@ export function useContentComposer(props: Props) {
}
}

return { editor, initialValue, onChange };
function handleAssetUpload(asset: Asset) {
Transforms.insertNodes(editor, {
type: "image",
caption: asset.url,
link: asset.url,
children: [{ text: "" }],
});
props.onAssetUpload?.(asset);
}

return { editor, initialValue, onChange, handleAssetUpload };
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ export type Props = {

export function useFileDrop(props: Props) {
const [dragging, setDragging] = useState(false);
const editor = useSlate();

function onDragStart() {
setDragging(true);
Expand All @@ -36,18 +35,11 @@ export function useFileDrop(props: Props) {
}

async function process(f: File) {
const { url } = await upload(f);

if (!isSupportedImage(f.type)) {
throw new Error("Unsupported image format");
}

Transforms.insertNodes(editor, {
type: "image",
caption: url,
link: url,
children: [{ text: "" }],
});
await upload(f);
}

async function handleEvent(e: DragEvent<HTMLDivElement>) {
Expand Down
63 changes: 54 additions & 9 deletions web/src/components/directory/datagraph/Breadcrumbs.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,80 @@
import { ChevronRightIcon } from "@heroicons/react/24/outline";
import { ChevronRightIcon, PlusCircleIcon } from "@heroicons/react/24/outline";
import { pull } from "lodash";
import { FormEventHandler, ForwardedRef, Fragment, forwardRef } from "react";

import {
DirectoryPath,
joinDirectoryPath,
} from "src/screens/directory/datagraph/useDirectoryPath";
import { Input } from "src/theme/components/Input";
import { Link } from "src/theme/components/Link";

import { HStack } from "@/styled-system/jsx";

type Props = {
directoryPath: DirectoryPath;
create: "hide" | "show" | "edit";
value?: string;
defaultValue?: string;
onChange?: FormEventHandler<HTMLInputElement>;
};

export function Breadcrumbs(props: Props) {
export const _Breadcrumbs = (
{ directoryPath, create, value, defaultValue, onChange, ...rest }: Props,
ref: ForwardedRef<HTMLInputElement>,
) => {
const isEditing = create == "edit" && onChange !== undefined;
const editingPath = isEditing ? directoryPath.slice(0, -1) : directoryPath;
const paths = pull(editingPath, "new");
const jointNew = joinDirectoryPath(directoryPath, "new");

return (
<HStack color="fg.subtle">
<Link href="/directory" size="xs">
<HStack w="full" color="fg.subtle">
<Link minW="min" href="/directory" size="xs">
Directory
</Link>
{props.directoryPath.map((p) => (
<>
{paths.map((p) => (
<Fragment key={p}>
<ChevronRightIcon width="1rem" />
<Link
key={p}
href={`/directory/${joinDirectoryPath(props.directoryPath, p)}`}
href={`/directory/${joinDirectoryPath(paths, p)}`}
size="xs"
>
{p}
</Link>
</>
</Fragment>
))}
{create == "show" && (
<>
<ChevronRightIcon width="1rem" />
<Link
flexShrink="0"
kind="primary"
href={`/directory/${jointNew}`}
size="xs"
>
<PlusCircleIcon /> Create
</Link>
</>
)}
{isEditing && (
<>
<ChevronRightIcon width="1rem" />
<Input
ref={ref}
w="full"
size="xs"
placeholder="URL slug"
defaultValue={defaultValue}
value={value}
onChange={onChange}
{...rest}
/>
</>
)}
</HStack>
);
}
};

export const Breadcrumbs = forwardRef(_Breadcrumbs);
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Cluster } from "src/api/openapi/schemas";
import { Empty } from "src/components/feed/common/PostRef/Empty";
import { Empty } from "src/components/site/Empty";
import {
DirectoryPath,
joinDirectoryPath,
Expand Down
21 changes: 21 additions & 0 deletions web/src/components/directory/datagraph/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { last, nth, takeWhile } from "lodash";

export function getTargetSlug(slug: string[]): [string, string, boolean] {
const top = last(slug);

const isNew = top === "new";

const target = isNew
? // If the tail item is "new" then walk back until we find an actual slug.
last(takeWhile(slug, (s) => s !== "new"))
: // Otherwise, it's whatever the last item is.
top;

// The fallback is for when the target is actually an item. Items cannot have
// children, so we can assume the parent is the left-most slug of the target,
// which is the third index from the end of the slug array. This is an edge
// case though as the only way to hit this would be to manually add "/new".
const fallback = slug.length > 2 ? nth(slug, -3) : "";

return [target ?? "", fallback ?? "", isNew];
}
2 changes: 1 addition & 1 deletion web/src/components/directory/links/LinkCard.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ArrowTopRightOnSquareIcon } from "@heroicons/react/24/solid";

import { Link as LinkSchema } from "src/api/openapi/schemas";
import { Empty } from "src/components/feed/common/PostRef/Empty";
import { Empty } from "src/components/site/Empty";
import { Link } from "src/theme/components/Link";

import {
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/directory/links/LinkCardList.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { LinkListResult } from "src/api/openapi/schemas";
import { LinkCard } from "src/components/directory/links/LinkCard";
import { Empty } from "src/components/feed/common/PostRef/Empty";
import { Empty } from "src/components/site/Empty";

import { styled } from "@/styled-system/jsx";

Expand Down
2 changes: 1 addition & 1 deletion web/src/components/directory/members/MemberList.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { PublicProfileList } from "src/api/openapi/schemas";
import { Empty } from "src/components/feed/common/PostRef/Empty";
import { Empty } from "src/components/site/Empty";

import { styled } from "@/styled-system/jsx";

Expand Down
3 changes: 2 additions & 1 deletion web/src/components/feed/common/PostRef/PostRefList.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { PostProps, ThreadReference } from "src/api/openapi/schemas";

import { Empty } from "../../../site/Empty";

import { styled } from "@/styled-system/jsx";

import { Empty } from "./Empty";
import { PostRef } from "./PostRef";

type Either = PostProps | ThreadReference;
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/feed/link/LinkPost.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { Link as LinkSchema, ThreadReference } from "src/api/openapi/schemas";
import { Anchor } from "src/components/site/Anchor";
import { Heading1 } from "src/theme/components/Heading/Index";

import { Empty } from "../../site/Empty";
import { FeedItemByline } from "../common/FeedItemByline/FeedItemByline";
import { Empty } from "../common/PostRef/Empty";

import { Box, Flex, VStack, styled } from "@/styled-system/jsx";
import { Card } from "@/styled-system/patterns";
Expand Down
7 changes: 6 additions & 1 deletion web/src/components/site/Action/Edit.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { PencilIcon } from "@heroicons/react/24/outline";
import { PropsWithChildren } from "react";

import { Button, ButtonProps } from "src/theme/components/Button";

export function EditAction(props: ButtonProps) {
export function EditAction({
children,
...props
}: PropsWithChildren<ButtonProps>) {
return (
<Button kind="ghost" size="xs" {...props}>
<PencilIcon width="0.5em" height="0.5em" />
{children}
</Button>
);
}
10 changes: 7 additions & 3 deletions web/src/components/site/Action/Save.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { CloudArrowUpIcon } from "@heroicons/react/24/outline";
import { PropsWithChildren } from "react";

import { Button, ButtonProps } from "src/theme/components/Button";

export function SaveAction(props: ButtonProps) {
export function SaveAction({
children,
...props
}: PropsWithChildren<ButtonProps>) {
return (
<Button kind="ghost" size="sm" {...props}>
<CloudArrowUpIcon width="1.4em" />
<Button kind="ghost" size="xs" {...props}>
<CloudArrowUpIcon width="1.4em" /> {children}
</Button>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,9 @@ import { HStack, styled } from "@/styled-system/jsx";

export function Empty({ children }: PropsWithChildren) {
return (
<HStack alignItems="center">
<CubeTransparentIcon className={css({ width: "6", color: "bg.muted" })} />
<styled.p fontStyle="italic" color="fg.subtle">
{children}
</styled.p>
<HStack alignItems="center" color="bg.muted">
<CubeTransparentIcon className={css({ width: "6" })} />
<styled.p fontStyle="italic">{children}</styled.p>
</HStack>
);
}
Loading

0 comments on commit 46feee9

Please sign in to comment.