Skip to content

Commit

Permalink
Merge pull request #462 from oddbit/issue/459-editing-article-meta-data
Browse files Browse the repository at this point in the history
feat: editing article meta data
  • Loading branch information
DennisAlund authored Oct 2, 2024
2 parents 4d6a3d6 + d7d3e04 commit 30b8b08
Show file tree
Hide file tree
Showing 8 changed files with 417 additions and 185 deletions.
183 changes: 150 additions & 33 deletions apps/cms/src/app/(protected)/content/article/[documentId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
"use client";
import {UserNotification} from "@tanam/domain-frontend";
import {Button, Input, Loader, Notification, PageHeader, TiptapEditor} from "@tanam/ui-components";
import {
Badge,
Button,
Input,
Loader,
Modal,
MultipleText,
Notification,
PageHeader,
TextArea,
TiptapEditor,
} from "@tanam/ui-components";
import {useParams, useRouter} from "next/navigation";
import {Suspense, useEffect, useState} from "react";
import {useCrudTanamDocument, useTanamDocument} from "../../../../../hooks/useTanamDocuments";
Expand All @@ -12,8 +23,11 @@ export default function DocumentDetailsPage() {
const {update, error: writeError} = useCrudTanamDocument();

const [title, setTitle] = useState<string>("");
const [description, setDescription] = useState<string>("");
const [tags, setTags] = useState<string[]>([]);
const [showModalMetadata, setShowMetadataModal] = useState<boolean>(false);
const [readonlyMode] = useState<boolean>(false);
const [updateTitleShown, setUpdateTitleShown] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(false);
const [notification, setNotification] = useState<UserNotification | null>(null);

useEffect(() => {
Expand All @@ -26,27 +40,33 @@ export default function DocumentDetailsPage() {
setNotification(documentError || writeError);
}, [documentError, writeError]);

useEffect(() => {
if (updateTitleShown) return;

onDocumentTitleChange(title);
}, [updateTitleShown]);

useEffect(() => {
if (document) {
setTitle(document.data.title as string);
setDescription(document.data.blurb as string);
setTags(document.data.tags as string[]);
}

return () => setTitle("");
}, [document]);
return () => {
pruneState();
};
}, [document, showModalMetadata]);

function pruneState() {
setTitle("");
setDescription("");
setTags((document?.data.tags as string[]) ?? []);
}

async function onDocumentTitleChange(title: string) {
console.log("[onDocumentTitleChange]", title);
async function fetchDocumentUpdate(title: string, blurb: string, tags: string[]) {
console.log("[fetchDocumentUpdate]", title);
if (!document) {
return;
}

document.data.title = title;
document.data.blurb = blurb;
document.data.tags = tags;
await update(document);
}

Expand All @@ -60,6 +80,61 @@ export default function DocumentDetailsPage() {
await update(document);
}

function onCloseMetadataModal() {
setShowMetadataModal(false);
}

async function onSaveMetadataModal() {
setLoading(true);

setNotification(null);

try {
await fetchDocumentUpdate(title, description, tags);

setNotification(new UserNotification("success", "Update Metadata", "Success to update metadata"));
} catch (error) {
console.error(error);
setNotification(new UserNotification("error", "Update Metadata", "Failed to update metadata"));
} finally {
setLoading(false);
onCloseMetadataModal();
}
}

function onOpenMetadata() {
setShowMetadataModal(true);
}

/**
* Modal actions for saving or canceling metadata changes.
* @constant
* @type {JSX.Element}
*/
const modalActionMetadata = (
<div className="flex flex-col sm:flex-row justify-end gap-3">
{/* Start button to close the metadata modal */}
<Button
title="Close"
loading={loading}
disabled={readonlyMode || loading}
onClick={onCloseMetadataModal}
style="outline-rounded"
/>
{/* End button to close the metadata modal */}

{/* Start button to save changes metadata */}
<Button
title="Save"
loading={loading}
disabled={readonlyMode || loading}
onClick={onSaveMetadataModal}
style="rounded"
/>
{/* End button to save changes metadata */}
</div>
);

return (
<>
{notification && (
Expand All @@ -69,27 +144,26 @@ export default function DocumentDetailsPage() {
<Suspense fallback={<Loader />}>
{document ? (
<>
<div className="relative w-full flex flex-row gap-3">
{!updateTitleShown && <PageHeader pageName={document.data.title as string} />}

{updateTitleShown && (
<Input
key="titleArticle"
type="text"
placeholder="Title"
disabled={readonlyMode}
value={title || ""}
onChange={(e) => setTitle(e.target.value)}
/>
)}

<Button
title={updateTitleShown ? "Save Changes" : "Edit Title"}
onClick={() => setUpdateTitleShown(!updateTitleShown)}
style="rounded"
>
<span className="i-ic-outline-edit mr-2" />
</Button>
<div className="relative w-full">
<div className="relative w-full flex flex-row mb-4">
<Button title="Edit Metadata" loading={loading} onClick={onOpenMetadata} style="rounded">
<span className="i-ic-outline-edit mr-2" />
</Button>
</div>

<div className="relative w-full flex flex-row mb-4">
<PageHeader pageName={document.data.title as string} />
</div>

<div className="relative w-full flex flex-row mb-4">
<p>{document.data.blurb as string}</p>
</div>

<div className="relative w-full flex flex-wrap gap-2">
{tags.length > 0 && tags.map((tag, index) => <Badge key={index} title={tag} />)}
</div>

<hr className="mt-4" />
</div>

{document?.data.content && (
Expand All @@ -100,6 +174,49 @@ export default function DocumentDetailsPage() {
onChange={onDocumentContentChange}
/>
)}

{/* Start modal metadata */}
<Modal
isOpen={showModalMetadata}
disableOverlayClose={true}
onClose={onCloseMetadataModal}
actions={modalActionMetadata}
title="Metadata"
>
<div className="relative w-full">
<div className="relative w-full flex flex-row mb-4">
<Input
key="titleArticle"
type="text"
placeholder="Title"
disabled={readonlyMode || loading}
value={title || ""}
onChange={(e) => setTitle(e.target.value)}
/>
</div>

<div className="relative w-full flex flex-row mb-4">
<TextArea
key="descriptionArticle"
placeholder="Description"
rows={3}
disabled={readonlyMode || loading}
value={description || ""}
onChange={(e) => setDescription(e.target.value)}
/>
</div>

<div className="relative w-full">
<MultipleText
placeholder="Add tags"
disabled={readonlyMode || loading}
value={tags}
onChange={(value) => setTags(value)}
/>
</div>
</div>
</Modal>
{/* End modal metadata */}
</>
) : (
<Loader />
Expand Down
4 changes: 2 additions & 2 deletions apps/cms/src/app/(protected)/content/article/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,10 @@ export default function DocumentTypeDocumentsPage() {
<>
<Suspense fallback={<Loader />}>
{documentType ? (
<div className="flex items-center gap-4">
<div className="relative w-full flex gap-2 mb-4">
<PageHeader pageName={documentType.titlePlural.translated} />

<div className="mb-6">
<div className="relative">
<span className="relative">
<Button
title={`Create ${documentType.titleSingular.translated}`}
Expand Down
27 changes: 27 additions & 0 deletions libs/ui-components/src/Badge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
interface BadgeProps {
title: string;
className?: string;
disabled?: boolean;
onRemove?: () => void;
}

export function Badge(props: BadgeProps) {
const {title, className, disabled, onRemove} = props;

let badgeClassName = "border px-3 py-1 text-xs font-semibold rounded-full";
badgeClassName = className ? badgeClassName.concat(` ${className}`) : badgeClassName;

return (
<div className={badgeClassName}>
<div className="relative w-full inline-flex items-center justify-between">
<span>{title}</span>

{onRemove && (
<button disabled={disabled} onClick={onRemove} aria-label="Remove badge" className="ml-2">
<span className="relative top-[2px] i-ic-close" />
</button>
)}
</div>
</div>
);
}
61 changes: 61 additions & 0 deletions libs/ui-components/src/Form/MultipleText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import {useState} from "react";
import {Badge} from "../Badge";
import {Button} from "./../Button";
import {Input} from "./Input";

interface MultipleTextProps {
title?: string;
placeholder?: string;
loading?: boolean;
disabled?: boolean;
value?: string[];
onChange?: (value: string[]) => void;
}

export function MultipleText(props: MultipleTextProps) {
const {title, placeholder = "Add something here", disabled, loading, value = [], onChange} = props;

const [entry, setEntry] = useState("");
const [entries, setEntries] = useState(value);

function handleAddItem() {
if (entry.trim()) {
const updatedArray = [...entries, entry.trim()];

setEntries(updatedArray);
onChange && onChange(updatedArray);
setEntry("");
}
}

function handleRemoveItem(index: number) {
const updatedArray = entries.filter((_, i) => i !== index);

setEntries(updatedArray);
onChange && onChange(updatedArray);
}

return (
<div className="relative w-full">
{title && <h3 className="text-lg font-semibold mb-2">{title}</h3>}

<div className="flex gap-2 mb-4">
<Input
key="multipleTextInput"
type="text"
placeholder={placeholder}
disabled={disabled}
value={entry}
onChange={(e) => setEntry(e.target.value)}
/>

<Button loading={loading} disabled={disabled} title="Add" onClick={handleAddItem} style="rounded" />
</div>

<div className="relative w-full flex flex-wrap gap-2">
{entries.length > 0 &&
entries.map((value, index) => <Badge key={index} title={value} onRemove={() => handleRemoveItem(index)} />)}
</div>
</div>
);
}
1 change: 1 addition & 0 deletions libs/ui-components/src/Form/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export {Dropdown} from "./Dropdown";
export {FileUpload} from "./FileUpload";
export {FormGroup} from "./FormGroup";
export {Input} from "./Input";
export {MultipleText} from "./MultipleText";
export {RadioButton} from "./RadioButton";
export {Select} from "./Select";
export {Switcher} from "./Switcher";
Expand Down
2 changes: 1 addition & 1 deletion libs/ui-components/src/common/PageHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ interface PageHeaderProps {

export function PageHeader({pageName, pageActions = []}: PageHeaderProps) {
return (
<div className="mb-6 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="relative flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<h2 className="text-title-md2 font-semibold text-black dark:text-white">{pageName}</h2>
{pageActions.length > 0 && <div>{pageActions.map((action) => action)}</div>}
</div>
Expand Down
1 change: 1 addition & 0 deletions libs/ui-components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export * from "./Table";
export * from "./Tiptap/TiptapEditor";

// Other Components
export * from "./Badge";
export * from "./Button";
export * from "./CropImage";
export * from "./FilePicker";
Expand Down
Loading

0 comments on commit 30b8b08

Please sign in to comment.