Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: editing article meta data #462

Merged
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
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
Loading