feat: implemented new pages design with bare minimum functionality (#503)

* chore: add page types and page api service

* chore: add create, list, update and delete on pages

* chore: add create, delete and patch page blocks

* feat: add and remove pages to favorite

* fix: made neccessary changes

- used tailwind for hover events
- add error toast alert
- used partial for patch request

* fix: replace absolute positiong with a flex box

* fix: design list page view to match with ui

* feat: add top large textarea for page title and description

* refactor: add page label with types

* feat: add pages grid layout

* feat: add tabs and masonry layout

* fix: build errors

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
This commit is contained in:
Saheb Giri 2023-03-23 16:12:14 +05:30 committed by GitHub
parent c755907d99
commit 4a81b988b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 455 additions and 37 deletions

View File

@ -0,0 +1,172 @@
import React from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import { mutate } from "swr";
// services
import pagesService from "services/pages.service";
// ui
import { CustomMenu } from "components/ui";
// icons
import { PencilIcon, StarIcon, TrashIcon } from "@heroicons/react/24/outline";
// helpers
import { truncateText } from "helpers/string.helper";
// hooks
import useToast from "hooks/use-toast";
// types
import { IPage } from "types";
// fetch keys
import { PAGE_LIST } from "constants/fetch-keys";
import Label from "./page-label";
type TSingleStatProps = {
page: IPage;
handleEditPage: () => void;
handleDeletePage: () => void;
};
export const SinglePageGridItem: React.FC<TSingleStatProps> = (props) => {
const { page, handleEditPage, handleDeletePage } = props;
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { setToastAlert } = useToast();
const handleAddToFavorites = () => {
if (!workspaceSlug && !projectId && !page) return;
pagesService
.addPageToFavorites(workspaceSlug as string, projectId as string, {
page: page.id,
})
.then(() => {
mutate<IPage[]>(
PAGE_LIST(projectId as string),
(prevData) =>
(prevData ?? []).map((m) => ({
...m,
is_favorite: m.id === page.id ? true : m.is_favorite,
})),
false
);
setToastAlert({
type: "success",
title: "Success!",
message: "Successfully added the page to favorites.",
});
})
.catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "Couldn't add the page to favorites. Please try again.",
});
});
};
const handleRemoveFromFavorites = () => {
if (!workspaceSlug || !page) return;
pagesService
.removePageFromFavorites(workspaceSlug as string, projectId as string, page.id)
.then(() => {
mutate<IPage[]>(
PAGE_LIST(projectId as string),
(prevData) =>
(prevData ?? []).map((m) => ({
...m,
is_favorite: m.id === page.id ? false : m.is_favorite,
})),
false
);
setToastAlert({
type: "success",
title: "Success!",
message: "Successfully removed the page from favorites.",
});
})
.catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "Couldn't remove the page from favorites. Please try again.",
});
});
};
return (
<div>
<li>
<div className="relative rounded px-4 py-4 sm:px-6">
<div className="flex items-center justify-between">
<div className="flex gap-2">
<Link href={`/${workspaceSlug}/projects/${projectId}/pages/${page.id}`}>
<a className="after:absolute after:inset-0">
<p className="mr-2 truncate text-sm font-medium">{truncateText(page.name, 75)}</p>
</a>
</Link>
<Label variant="green">Meetings</Label>
<Label variant="red">Standup</Label>
<Label variant="blue">Plans</Label>
</div>
<div className="ml-2 flex flex-shrink-0">
<div className="flex items-center gap-2">
<p className="text-sm text-gray-400">
{new Date(page.updated_at).toLocaleTimeString()}
</p>
{page.is_favorite ? (
<button onClick={handleRemoveFromFavorites} className="z-10">
<StarIcon className="h-4 w-4 text-orange-400" fill="#f6ad55" />
</button>
) : (
<button onClick={handleAddToFavorites} type="button" className="z-10">
<StarIcon className="h-4 w-4 " color="#858E96" />
</button>
)}
<CustomMenu width="auto" verticalEllipsis>
<CustomMenu.MenuItem onClick={handleEditPage}>
<span className="flex items-center justify-start gap-2 text-gray-800">
<PencilIcon className="h-4 w-4" />
<span>Edit Page</span>
</span>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={handleDeletePage}>
<span className="flex items-center justify-start gap-2 text-gray-800">
<TrashIcon className="h-4 w-4" />
<span>Delete Page</span>
</span>
</CustomMenu.MenuItem>
</CustomMenu>
</div>
</div>
</div>
<div className="relative mt-6 space-y-2 text-sm leading-relaxed text-gray-600">
<p>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Reiciendis atque aliquam
saepe sapiente illo ratione delectus dolorem repellat, id autem, molestiae neque
quaerat ipsum perspiciatis pariatur? Unde consectetur quibusdam ut.
</p>
<p>
Quisquam quas expedita cupiditate ipsum cumque fugit at, optio quia ea? Id doloribus
assumenda ad magni laborum aut, aspernatur nemo similique, suscipit dolores porro
necessitatibus, inventore ab aliquid molestias. Aspernatur.
</p>
<p>
Beatae obcaecati minus temporibus sunt, quo nulla, tenetur nisi sit maiores aspernatur
numquam facilis asperiores eos rerum, ad dolorem quos laboriosam dicta eaque! Pariatur
magni eos, architecto itaque esse minus.
</p>
<p>
Dolorum saepe impedit officiis odit! Porro aliquid dolorum corporis impedit eaque
iusto, illo hic neque quia vero aperiam? Nemo aliquam, hic incidunt mollitia totam
asperiores sunt nam inventore voluptatibus eum?
</p>
<div className="absolute bottom-0 h-24 w-full bg-gradient-to-t from-white" />
</div>
</div>
</li>
</div>
);
};

View File

@ -0,0 +1,23 @@
import React from "react";
type TLabelProps = {
variant: "red" | "blue" | string;
children?: React.ReactNode;
};
const Label: React.FC<TLabelProps> = (props) => {
let color = "bg-green-100 text-green-800";
if (props.variant === "red") {
color = "bg-red-100 text-red-800";
} else if (props.variant === "blue") {
color = "bg-blue-100 text-blue-800";
}
return (
<p className={`inline-flex rounded-full px-2 text-xs font-semibold leading-5 ${color}`}>
{props.children}
</p>
);
};
export default Label;

View File

@ -0,0 +1,58 @@
import { useState } from "react";
// components
import { DeletePageModal } from "components/pages";
import { Loader } from "components/ui";
import { SinglePageGridItem } from "components/pages/page-grid-item";
// types
import { IPage } from "types";
export const PagesGrid: React.FC<any> = ({ pages, setCreateUpdatePageModal, setSelectedPage }) => {
const [pageDeleteModal, setPageDeleteModal] = useState(false);
const [selectedPageForDelete, setSelectedPageForDelete] = useState<any>();
const handleDeletePage = (page: IPage) => {
setSelectedPageForDelete({ ...page, actionType: "delete" });
setPageDeleteModal(true);
};
const handleEditPage = (page: IPage) => {
setSelectedPage({ ...page, actionType: "edit" });
setCreateUpdatePageModal(true);
};
return (
<>
<DeletePageModal
isOpen={
pageDeleteModal &&
!!selectedPageForDelete &&
selectedPageForDelete.actionType === "delete"
}
setIsOpen={setPageDeleteModal}
data={selectedPageForDelete}
/>
{pages ? (
pages.length > 0 ? (
<div className="rounded-[10px] border border-gray-200 bg-white">
<ul role="list" className="divide-y divide-gray-200">
{pages.map((page: any) => (
<SinglePageGridItem
page={page}
key={page.id}
handleDeletePage={() => handleDeletePage(page)}
handleEditPage={() => handleEditPage(page)}
/>
))}
</ul>
</div>
) : (
"No Pages found"
)
) : (
<Loader className="grid grid-cols-1 gap-9 md:grid-cols-2 lg:grid-cols-3">
<Loader.Item height="200px" />
</Loader>
)}
</>
);
};

View File

@ -42,18 +42,16 @@ export const PagesList: React.FC<TPagesListProps> = ({
/> />
{pages ? ( {pages ? (
pages.length > 0 ? ( pages.length > 0 ? (
<div className="border border-gray-200 bg-white sm:rounded-[10px] "> <ul role="list" className="divide-y divide-gray-200">
<ul role="list" className="divide-y divide-gray-200"> {pages.map((page) => (
{pages.map((page) => ( <SinglePageListItem
<SinglePageListItem page={page}
page={page} key={page.id}
key={page.id} handleDeletePage={() => handleDeletePage(page)}
handleDeletePage={() => handleDeletePage(page)} handleEditPage={() => handleEditPage(page)}
handleEditPage={() => handleEditPage(page)} />
/> ))}
))} </ul>
</ul>
</div>
) : ( ) : (
"No Pages found" "No Pages found"
) )

View File

@ -0,0 +1,82 @@
import React from "react";
// ui
import { CustomMenu } from "components/ui";
// icons
import { PencilIcon, StarIcon, SwatchIcon, TrashIcon } from "@heroicons/react/24/outline";
const MasonryItem: React.FC<any> = (props) => (
<div
className="mb-6 w-full rounded-lg border border-gray-200 bg-white p-3"
style={{
backgroundColor: props.color,
}}
>
<h2 className="text-lg font-medium">Personal Diary</h2>
<p className="mt-2 text-sm leading-relaxed">{props.children}</p>
<div className="mt-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<button onClick={() => {}} type="button" className="z-10">
<SwatchIcon className="h-4 w-4 " color="#858E96" />
</button>
{false ? (
<button onClick={() => {}} className="z-10">
<StarIcon className="h-4 w-4 text-orange-400" fill="#f6ad55" />
</button>
) : (
<button onClick={() => {}} type="button" className="z-10">
<StarIcon className="h-4 w-4 " color="#858E96" />
</button>
)}
<CustomMenu width="auto" verticalEllipsis>
<CustomMenu.MenuItem onClick={() => {}}>
<span className="flex items-center justify-start gap-2 text-gray-800">
<PencilIcon className="h-4 w-4" />
<span>Edit Page</span>
</span>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={() => {}}>
<span className="flex items-center justify-start gap-2 text-gray-800">
<TrashIcon className="h-4 w-4" />
<span>Delete Page</span>
</span>
</CustomMenu.MenuItem>
</CustomMenu>
</div>
<p className="text-sm text-gray-400">9:41 PM</p>
</div>
</div>
</div>
);
export default function PagesMasonry() {
return (
<div className="columns-4 gap-6">
<MasonryItem color="#FF9E9E" isVideo>
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Impedit porro mollitia iure,
reiciendis quo tempora rem debitis velit quas doloremque. Dicta velit voluptas, blanditiis
excepturi vitae eum corporis totam eius?
</MasonryItem>
<MasonryItem>
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Impedit porro mollitia iure,
reiciendis quo tempora rem debitis velit quas doloremque. Dicta velit voluptas, blanditiis
excepturi vitae eum corporis totam eius? Lorem, ipsum dolor sit amet consectetur adipisicing
elit. Impedit porro mollitia iure, reiciendis quo tempora rem debitis velit quas doloremque.
Dicta velit voluptas, blanditiis excepturi vitae eum corporis totam eius?
</MasonryItem>
<MasonryItem color="#FCBE1D">
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Impedit porro mollitia iure,
reiciendis quo tempora rem debitis velit quas doloremque. Dicta velit voluptas, blanditiis
excepturi vitae eum corporis totam eius?
</MasonryItem>
<MasonryItem>
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Impedit porro mollitia iure,
reiciendis quo tempora rem debitis velit quas doloremque. Dicta velit voluptas, blanditiis
excepturi vitae eum corporis totam eius? Lorem, ipsum dolor sit amet consectetur adipisicing
elit. Impedit porro mollitia iure, reiciendis quo tempora rem debitis velit quas doloremque.
Dicta velit voluptas, blanditiis excepturi vitae eum corporis totam eius?
</MasonryItem>
</div>
);
}

View File

@ -7,6 +7,7 @@ import { mutate } from "swr";
import pagesService from "services/pages.service"; import pagesService from "services/pages.service";
// ui // ui
import { CustomMenu } from "components/ui"; import { CustomMenu } from "components/ui";
import Label from "./page-label";
// icons // icons
import { PencilIcon, StarIcon, TrashIcon } from "@heroicons/react/24/outline"; import { PencilIcon, StarIcon, TrashIcon } from "@heroicons/react/24/outline";
// helpers // helpers
@ -24,20 +25,6 @@ type TSingleStatProps = {
handleDeletePage: () => void; handleDeletePage: () => void;
}; };
const Label: React.FC<any> = (props) => {
let color = "bg-green-100 text-green-800";
if (props.variant === "red") {
color = "bg-red-100 text-red-800";
} else if (props.variant === "blue") {
color = "bg-blue-100 text-blue-800";
}
return (
<p className={`inline-flex rounded-full px-2 text-xs font-semibold leading-5 ${color}`}>
{props.children}
</p>
);
};
export const SinglePageListItem: React.FC<TSingleStatProps> = (props) => { export const SinglePageListItem: React.FC<TSingleStatProps> = (props) => {
const { page, handleEditPage, handleDeletePage } = props; const { page, handleEditPage, handleDeletePage } = props;
@ -111,7 +98,7 @@ export const SinglePageListItem: React.FC<TSingleStatProps> = (props) => {
return ( return (
<div> <div>
<li> <li>
<div className="relative px-4 py-4 hover:bg-gray-50 sm:px-6"> <div className="relative rounded px-4 py-4 hover:bg-gray-200 sm:px-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex gap-2"> <div className="flex gap-2">
<Link href={`/${workspaceSlug}/projects/${projectId}/pages/${page.id}`}> <Link href={`/${workspaceSlug}/projects/${projectId}/pages/${page.id}`}>

View File

@ -24,10 +24,29 @@ import { HeaderButton } from "components/ui";
import { CreateUpdatePageModal } from "components/pages/create-update-page-modal"; import { CreateUpdatePageModal } from "components/pages/create-update-page-modal";
import { PagesList } from "components/pages/pages-list"; import { PagesList } from "components/pages/pages-list";
import { IPage } from "types"; import { IPage } from "types";
import PagesMasonry from "components/pages/pages-masonry";
import { Tab } from "@headlessui/react";
import { ListBulletIcon, RectangleGroupIcon, Squares2X2Icon } from "@heroicons/react/20/solid";
import { PagesGrid } from "components/pages/pages-grid";
const TabPill: React.FC<any> = (props) => (
<Tab
className={({ selected }) =>
`rounded-full border px-5 py-1.5 text-sm outline-none ${
selected
? "border-theme bg-theme text-white"
: "border-gray-300 bg-white hover:bg-hover-gray"
}`
}
>
{props.children}
</Tab>
);
const ProjectPages: NextPage = () => { const ProjectPages: NextPage = () => {
const [isCreateUpdatePageModalOpen, setIsCreateUpdatePageModalOpen] = useState(false); const [isCreateUpdatePageModalOpen, setIsCreateUpdatePageModalOpen] = useState(false);
const [selectedPage, setSelectedPage] = useState<IPage>(); const [selectedPage, setSelectedPage] = useState<IPage>();
const [viewType, setViewType] = useState("list");
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
@ -82,17 +101,96 @@ const ProjectPages: NextPage = () => {
handleClose={() => setIsCreateUpdatePageModalOpen(false)} handleClose={() => setIsCreateUpdatePageModalOpen(false)}
data={selectedPage} data={selectedPage}
/> />
<div className="space-y-2 pb-8"> <div className="space-y-4">
<h3 className="text-3xl font-semibold text-black">Pages</h3> <div className="overflow-hidden rounded-lg border border-gray-200 bg-white px-4 pt-3 pb-4 shadow-sm ">
<p className="text-sm text-gray-500"> <label htmlFor="name" className="sr-only">
Note down all the important and minor details in the way you want to. Title
</p> </label>
<input
type="text"
name="name"
id="name"
className="block w-full border-0 pt-2.5 text-lg font-medium placeholder-gray-500 outline-none focus:ring-0"
placeholder="Title"
/>
<label htmlFor="description" className="sr-only">
Description
</label>
<textarea
rows={2}
name="description"
id="description"
className="block w-full resize-none border-0 pb-8 placeholder-gray-500 outline-none focus:ring-0 sm:text-sm"
placeholder="Write something..."
defaultValue={""}
/>
</div>
{/* <div className="space-y-2 pb-8">
<h3 className="text-3xl font-semibold text-black">Pages</h3>
<p className="text-sm text-gray-500">
Note down all the important and minor details in the way you want to.
</p>
</div> */}
<div>
<Tab.Group>
<Tab.List as="div" className="flex items-center justify-between ">
<div className="flex gap-4 text-base font-medium">
<TabPill>Recent</TabPill>
<TabPill>All</TabPill>
<TabPill>Favorites</TabPill>
<TabPill>Created by me</TabPill>
<TabPill>Created by others</TabPill>
</div>
<div className="flex items-center gap-x-1">
<button
type="button"
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-200 ${
viewType === "list" ? "bg-gray-200" : ""
}`}
onClick={() => setViewType("list")}
>
<ListBulletIcon className="h-4 w-4" />
</button>
<button
type="button"
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-200 ${
viewType === "grid" ? "bg-gray-200" : ""
}`}
onClick={() => setViewType("grid")}
>
<Squares2X2Icon className="h-4 w-4" />
</button>
<button
type="button"
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-200 ${
viewType === "masonry" ? "bg-gray-200" : ""
}`}
onClick={() => setViewType("masonry")}
>
<RectangleGroupIcon className="h-4 w-4" />
</button>
</div>
</Tab.List>
</Tab.Group>
</div>
{viewType === "list" && (
<PagesList
setSelectedPage={setSelectedPage}
setCreateUpdatePageModal={setIsCreateUpdatePageModalOpen}
pages={pages}
/>
)}
{viewType === "grid" && (
<PagesGrid
setSelectedPage={setSelectedPage}
setCreateUpdatePageModal={setIsCreateUpdatePageModalOpen}
pages={pages}
/>
)}
{viewType === "masonry" && <PagesMasonry />}
</div> </div>
<PagesList
setSelectedPage={setSelectedPage}
setCreateUpdatePageModal={setIsCreateUpdatePageModalOpen}
pages={pages}
/>
</AppLayout> </AppLayout>
); );
}; };