Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export const meta = {
preview: `/document-preview/`,
management: `/documents/`,
},
markdownPostCreator: `/markdown-post-creator/`,
notFound: `/404/`,
privacyPolicy: `/privacy-policy/`,
auth: {
Expand Down
43 changes: 43 additions & 0 deletions src/acts/create-markdown-post.act.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { getAPI, parseError, setCache } from "api-4markdown";
import type { Atoms } from "api-4markdown-contracts";
import type { AsyncResult } from "development-kit/utility-types";
import { docManagementStoreActions } from "store/doc-management/doc-management.store";
import { docStoreActions } from "store/doc/doc.store";
import { docsStoreActions, docsStoreSelectors } from "store/docs/docs.store";
import { useMarkdownPostCreatorState } from "store/markdown-post-creator";
import { resetAction } from "store/markdown-post-creator/actions";

const createMarkdownPostAct = async (): AsyncResult<{
id: Atoms["DocumentId"];
}> => {
const { title, content } = useMarkdownPostCreatorState.get();

try {
docManagementStoreActions.busy();

const createdDocument = await getAPI().call(`createDocument`)({
name: title,
code: content,
});

docManagementStoreActions.ok();
docStoreActions.setActive(createdDocument);

const docsState = docsStoreSelectors.state();

if (docsState.is === `ok`) {
docsStoreActions.addDoc(createdDocument);
setCache(`getYourDocuments`, docsStoreSelectors.ok().docs);
}

resetAction();

return { is: `ok`, data: { id: createdDocument.id } };
} catch (error: unknown) {
docManagementStoreActions.fail(error);

return { is: `fail`, error: parseError(error) };
}
};

export { createMarkdownPostAct };
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import React from "react";
import { navigate } from "gatsby";
import { Button } from "design-system/button";
import { Field } from "design-system/field";
import { Input } from "design-system/input";
import { useForm } from "development-kit/use-form";
import type { ValidatorFn, ValidatorsSetup } from "development-kit/form";
import { useDocManagementStore } from "store/doc-management/doc-management.store";
import {
changeContentAction,
changeTitleAction,
} from "store/markdown-post-creator/actions";
import { useMarkdownPostCreatorState } from "store/markdown-post-creator";
import { createMarkdownPostAct } from "acts/create-markdown-post.act";
import { meta } from "../../../../meta";

type FormValues = {
title: string;
};

const titleMinLength = 3;
const titleMaxLength = 100;

const titleValidator: ValidatorFn<string, string> = (value) => {
const trimmed = value.trim();

if (trimmed.length < titleMinLength) {
return `Title must be at least ${titleMinLength} characters`;
}

if (trimmed.length > titleMaxLength) {
return `Title must be fewer than ${titleMaxLength} characters`;
}

return null;
};

const validators: ValidatorsSetup<FormValues> = {
title: [titleValidator],
};

const CreatePostFormContainer = () => {
const docManagementStore = useDocManagementStore();
const { title, content } = useMarkdownPostCreatorState();
const [{ invalid, untouched, result }, { inject }] = useForm<FormValues>(
{ title },
validators,
);

const disabled = docManagementStore.is === `busy`;

const handleTitleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
inject(`title`).onChange(e);
changeTitleAction(e.target.value);
};

const handleContentChange: React.ChangeEventHandler<HTMLTextAreaElement> = (
e,
) => {
changeContentAction(e.target.value);
};

const handleSubmit = async (): Promise<void> => {
const result = await createMarkdownPostAct();

if (result.is === `ok`) {
void navigate(`${meta.routes.home}?id=${result.data.id}`);
}
};

return (
<div className="flex flex-col gap-4 h-full">
<Field
label={<Field.Label label="Title" value={title} required />}
hint={
result.title ? (
<Field.Error>{result.title}</Field.Error>
) : (
<Field.Hint>
{titleMinLength}–{titleMaxLength} characters
</Field.Hint>
)
}
>
<Input
autoFocus
placeholder="Your post title..."
value={title}
onChange={handleTitleChange}
name="title"
disabled={disabled}
/>
</Field>
<Field className="flex-1 flex flex-col" label="Content (Markdown)">
<textarea
className="flex-1 resize-none w-full px-3 py-2 text-black dark:text-white placeholder:text-gray-600 dark:placeholder:text-gray-300 text-sm rounded-md bg-gray-300 dark:bg-slate-800 border-[2.5px] border-transparent focus:border-black focus:dark:border-white outline-none"
placeholder="Write your post in markdown..."
value={content}
onChange={handleContentChange}
disabled={disabled}
spellCheck={false}
/>
</Field>
{docManagementStore.is === `fail` && (
<p className="text-sm text-red-700 dark:text-red-300" role="alert">
{docManagementStore.error.message}
</p>
)}
<Button
i={2}
s={2}
auto
className="self-end"
disabled={
invalid || untouched || content.trim().length === 0 || disabled
}
onClick={handleSubmit}
title="Create markdown post"
>
{disabled ? `Creating...` : `Create Post`}
</Button>
</div>
);
};

export { CreatePostFormContainer };
50 changes: 50 additions & 0 deletions src/features/markdown-post-creator/markdown-post-creator.view.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React from "react";
import { Link } from "gatsby";
import { Markdown } from "components/markdown";
import { Loader } from "design-system/loader";
import { Communicate } from "design-system/communicate";
import { useAuthStore } from "store/auth/auth.store";
import { useMarkdownPostCreatorState } from "store/markdown-post-creator";
import { CreatePostFormContainer } from "./containers/create-post-form.container";
import { meta } from "../../../meta";

const MarkdownPostCreatorView = () => {
const auth = useAuthStore();
const { content } = useMarkdownPostCreatorState();

return (
<main className="flex h-[calc(100svh-72px)]">
{auth.is === `idle` && <Loader className="m-auto" size="xl" />}
{auth.is === `unauthorized` && (
<Communicate className="m-auto">
<Communicate.Message>
Sign in to create markdown posts
</Communicate.Message>
<Communicate.Footer>
<Communicate.Action title="Go to login page" onClick={() => void 0}>
<Link to={meta.routes.auth.login}>Sign In</Link>
</Communicate.Action>
</Communicate.Footer>
</Communicate>
)}
{auth.is === `authorized` && (
<>
<section className="flex flex-col w-full md:w-1/2 border-r border-zinc-300 dark:border-zinc-800 p-4 overflow-y-auto">
<CreatePostFormContainer />
</section>
<section className="hidden md:block w-1/2 overflow-y-auto">
{content.trim().length === 0 ? (
<p className="p-4 text-sm text-gray-500 dark:text-gray-400 italic">
Preview will appear here as you type...
</p>
) : (
<Markdown className="p-4">{content}</Markdown>
)}
</section>
</>
)}
</main>
);
};

export { MarkdownPostCreatorView };
47 changes: 47 additions & 0 deletions src/pages/markdown-post-creator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React from "react";
import type { HeadFC } from "gatsby";
import LogoThumbnail from "images/logo-thumbnail.png";
import Meta from "components/meta";
import { meta } from "../../meta";
import { MarkdownPostCreatorView } from "features/markdown-post-creator/markdown-post-creator.view";
import { AppNavigation } from "components/app-navigation";
import { CreationLinkContainer } from "containers/creation-link.container";
import { EducationZoneLinkContainer } from "containers/education-zone-link.container";
import { EducationRankLinkContainer } from "containers/education-rank-link.container";
import { AppFooterContainer } from "containers/app-footer.container";
import UserPopover from "components/user-popover";
import MoreNav from "components/more-nav";

const MarkdownPostCreatorPage = () => {
return (
<>
<AppNavigation>
<CreationLinkContainer />
<EducationRankLinkContainer />
<EducationZoneLinkContainer />
<div className="ml-auto flex items-center gap-2">
<UserPopover />
<MoreNav />
</div>
</AppNavigation>
<MarkdownPostCreatorView />
<AppFooterContainer />
</>
);
};

export default MarkdownPostCreatorPage;

export const Head: HeadFC = () => {
return (
<Meta
appName={meta.appName}
title={`Markdown Post Creator | ${meta.appName}`}
description="Create and publish markdown posts with a live preview editor"
url={meta.siteUrl + meta.routes.markdownPostCreator}
lang={meta.lang}
image={meta.siteUrl + LogoThumbnail}
robots="noindex, nofollow"
/>
);
};
20 changes: 20 additions & 0 deletions src/store/markdown-post-creator/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { useMarkdownPostCreatorState } from ".";
import type { MarkdownPostCreatorState } from "./models";

const { set, getInitial } = useMarkdownPostCreatorState;

const changeTitleAction = (title: MarkdownPostCreatorState["title"]): void => {
set({ title });
};

const changeContentAction = (
content: MarkdownPostCreatorState["content"],
): void => {
set({ content });
};

const resetAction = (): void => {
set(getInitial());
};

export { changeTitleAction, changeContentAction, resetAction };
9 changes: 9 additions & 0 deletions src/store/markdown-post-creator/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { state } from "development-kit/state";
import type { MarkdownPostCreatorState } from "./models";

const useMarkdownPostCreatorState = state<MarkdownPostCreatorState>({
title: ``,
content: ``,
});

export { useMarkdownPostCreatorState };
6 changes: 6 additions & 0 deletions src/store/markdown-post-creator/models.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
type MarkdownPostCreatorState = {
title: string;
content: string;
};

export type { MarkdownPostCreatorState };
Loading