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

Publish blog posts 464 #473

Open
wants to merge 10 commits into
base: staging
Choose a base branch
from
20 changes: 19 additions & 1 deletion app/content-panel/blogs/create/page.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,33 @@
'use client';

/* eslint-disable jsx-a11y/label-has-associated-control */

import React from 'react';
import { useSession } from 'next-auth/react';
import styles from './createArticlePage.module.scss';
import PublishArticleForm from '../../../../components/module-components/blog/PublishArticleForm';

type UserRole = 'content_manager' | 'admin';

const PostArticle = () => {
const { data: session, status } = useSession();

if (status === 'loading') {
return <div>Loading....</div>;
}
aleksandarmicev marked this conversation as resolved.
Show resolved Hide resolved

if (!session || !session.user.role) {
return <div>Access denied</div>;
}
aleksandarmicev marked this conversation as resolved.
Show resolved Hide resolved

const userRole = session.user.role as UserRole;

return (
<div className={styles.container}>
<h2>Објави статија</h2>

<div className={styles.controlsContainer}>
<PublishArticleForm />
<PublishArticleForm userRole={userRole} />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main purpose of the create blog page should be to create/add new blog. On create we should call this endpoint: https://api.learnhub.mk/docs/#content-POSTcontent-blog-posts
We can change the status of already created blogs. That is why you need to send the id of the blog.

</div>
</div>
);
Expand Down
24 changes: 23 additions & 1 deletion components/module-components/blog/PublishArticleForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ import TiptapEditor from '../../editor/TiptapEditor';
import TagManager from './TagManager';
import Button from '../../reusable-components/button/Button';

const PublishArticleForm = () => {
interface PublishArticleFormProps {
userRole: 'content_manager' | 'admin';
aleksandarmicev marked this conversation as resolved.
Show resolved Hide resolved
}

const PublishArticleForm: React.FC<PublishArticleFormProps> = ({ userRole }) => {
const addNewPostMutation = useAddNewPost();
const [selectedTags, setSelectedTags] = useState<TagObject[]>([]);

Expand Down Expand Up @@ -40,6 +44,7 @@ const PublishArticleForm = () => {
excerpt: '',
content: '',
tags: [],
status: 'draft',
}}
onSubmit={handleAddPost}
>
Expand Down Expand Up @@ -104,6 +109,23 @@ const PublishArticleForm = () => {
{touched.tags && errors.tags && <div className={styles.error}>{errors.tags}</div>}
</div>

<div className={styles.fields}>
<label htmlFor="status" className={styles.inputLabel}>
Статус
</label>
<Field as="select" name="status" classname={styles.input}>
<option value="draft">Draft</option>
{userRole === 'content_manager' && <option value="in_review">In Review</option>}
{userRole === 'admin' && (
<>
<option value="in_review">In Review</option>
<option value="published">Published</option>
aleksandarmicev marked this conversation as resolved.
Show resolved Hide resolved
</>
)}
</Field>
{touched.status && errors.status && <div className={styles.error}>{errors.status}</div>}
</div>

{addNewPostMutation.isPending ? (
<Button
disabled
Expand Down
127 changes: 111 additions & 16 deletions components/module-components/create-blogs/BlogListView.tsx
aleksandarmicev marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import React, { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { useSession } from 'next-auth/react';
import ReusableTable from '../../reusable-components/reusable-table/ReusableTable';
import BlogManagementControls from './BlogManagementControls';
import ActionDropdown from '../../reusable-components/reusable-table/ActionDropdown';
Expand Down Expand Up @@ -29,42 +30,49 @@ interface BlogPost {
title: string;
tags: Tag[];
author: string;
status: string;
}

const BlogListView = () => {
const [searchTerm, setSearchTerm] = useState('');
const { editorStateChange } = useEditor();
const [data, setData] = useState<BlogPost[]>([]);
const url = `${process.env.NEXT_PUBLIC_API_BASE_URL}/blog-posts`;
const apiUrl = `${process.env.NEXT_PUBLIC_API_BASE_URL}/blog-posts`;
const router = useRouter();
const { data: session } = useSession();

useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(url);
const response = await fetch(apiUrl);
if (!response.ok) {
throw new Error(`${response.status} ${response.statusText}`);
}
const result = await response.json();

const transformedData: BlogPost[] = result.data.map((item: BlogPostAPI) => ({
id: item.slug,
title: item.title,
tags: item.tags,
author: `${item.author.first_name} ${item.author.last_name}`,
status: 'draft',
}));

setData(transformedData);
} catch (error) {
throw new Error(`Error fetching data: ${error}`);
// eslint-disable-next-line no-console
console.error(`Error fetching data: ${error}`);
}
};

fetchData();
}, [url]);
}, [apiUrl]);

const headers: (keyof BlogPost)[] = ['title', 'author'];
const headers: (keyof BlogPost)[] = ['title', 'author', 'status'];
const displayNames = {
title: 'Title',
author: 'Author',
status: 'Status',
};

const handleView = (id: string) => {
Expand All @@ -77,19 +85,106 @@ const BlogListView = () => {
router.push(`/content-panel/blogs/${id}`);
};

const handleDelete = () => {
// delete logic here
const handleChangeStatus = async (id: string, newStatus: string) => {
if (!session || !session.user) {
aleksandarmicev marked this conversation as resolved.
Show resolved Hide resolved
// eslint-disable-next-line no-console
console.error('Session is not available');
return;
}

try {
const changeStatusUrl = new URL(
`${process.env.NEXT_PUBLIC_API_BASE_URL}/blog-posts/${id}/statuses`
aleksandarmicev marked this conversation as resolved.
Show resolved Hide resolved
);

const requestHeaders = {
Authorization: `Bearer ${session.accessToken}`,
Accept: 'application/json',
'Content-Type': 'application/json',
};

const body: { publish_date?: string; status: string } = {
status: newStatus,
};

if (newStatus === 'published') {
// eslint-disable-next-line prefer-destructuring
body.publish_date = new Date().toISOString().split('T')[0];
}

fetch(changeStatusUrl.toString(), {
method: 'PATCH',
headers: requestHeaders,
body: JSON.stringify(body),
})
.then((response) => {
if (!response.ok) {
throw new Error(`${response.status} ${response.statusText}`);
}
return response.json();
})
.then(() => {
setData((prevData) => {
const [updatedItem] = prevData.filter((item) => item.id === id);
updatedItem.status = newStatus;
return prevData.map((item) => (item.id === id ? updatedItem : item));
});
})
aleksandarmicev marked this conversation as resolved.
Show resolved Hide resolved
.catch((err) => {
if (err instanceof Error) {
// eslint-disable-next-line no-console
console.error(`Error updating status: ${err.message}`);
if (err.message.includes('404')) {
// eslint-disable-next-line no-console
console.error('The specified blog post ID was not found.');
}
} else {
// eslint-disable-next-line no-console
console.error('An unexpected error occurred:', err);
}
});
} catch (err) {
if (err instanceof Error) {
// eslint-disable-next-line no-console
console.error(`Error updating status: ${err.message}`);
} else {
// eslint-disable-next-line no-console
console.error('An unexpected error occurred:', err);
}
}
};

const renderActionsDropdown = (item: BlogPost) => (
<ActionDropdown
dropdownItems={[
{ id: 'view', label: 'View', onClick: () => handleView(item.id) },
{ id: 'edit', label: 'Edit', onClick: () => handleEdit(item.id) },
{ id: 'delete', label: 'Delete', onClick: () => handleDelete() },
]}
/>
);
const renderActionsDropdown = (item: BlogPost) => {
if (!session || !session.user.role) {
return null;
}

const userRole = session.user.role as 'admin' | 'content_manager';

const dropdownItems = [
{ id: 'view', label: 'View', onClick: () => handleView(item.id) },
{ id: 'edit', label: 'Edit', onClick: () => handleEdit(item.id) },
userRole === 'admin'
? {
id: 'publish',
label: 'Publish',
onClick: () => handleChangeStatus(item.id, 'published'),
}
: undefined,
userRole === 'content_manager' && item.status === 'draft'
? {
id: 'in-review',
label: 'Move to In Review',
onClick: () => handleChangeStatus(item.id, 'in_review'),
}
: undefined,
].filter(
(dropdownItem): dropdownItem is { id: string; label: string; onClick: () => void } =>
dropdownItem !== undefined
);

return <ActionDropdown dropdownItems={dropdownItems} />;
};

return (
<div className={style.mainContainer}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const ActionDropdown = ({ dropdownItems }: ActionDropdownProps) => {
{isOpen && (
<ul className={style.dropdownMenu}>
{dropdownItems.map((item) => (
<li key={item.id} className={style.dropdownItem}>
<li key={item.id} className={`${style.dropdownItem} ${style[item.id]}`}>
<button type="button" onClick={item.onClick}>
{item.label}
</button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@ $table-height: 400px;
padding: 15px;
white-space: nowrap;
text-align: left;
overflow: hidden;
overflow: visible;
}

td {
height: 60px;
padding: 15px;
overflow: hidden;
white-space: nowrap;
overflow: visible;
white-space: wrap;
}

th:first-child,
Expand Down