#65 - Dashboard implementation

This commit is contained in:
Elio Struyf
2021-08-24 21:01:58 +02:00
parent ce8a343ffd
commit 722c0d6888
44 changed files with 745 additions and 58 deletions
+4
View File
@@ -0,0 +1,4 @@
export enum DashboardCommand {
loading = "loading",
data = "data"
}
+4
View File
@@ -0,0 +1,4 @@
export enum DashboardMessage {
getData = 'getData',
openFile = 'openFile',
}
+54
View File
@@ -0,0 +1,54 @@
import * as React from 'react';
import { Spinner } from './Spinner';
import useMessages from '../hooks/useMessages';
import { Overview } from './Overview';
import { Header } from './Header';
import { Tab } from '../constants/Tab';
import { SortOption } from '../constants/SortOption';
export interface IDashboardProps {}
export const Dashboard: React.FunctionComponent<IDashboardProps> = (props: React.PropsWithChildren<IDashboardProps>) => {
const { loading, pages } = useMessages();
const [ tab, setTab ] = React.useState(Tab.All);
const [ sorting, setSorting ] = React.useState(SortOption.LastModified);
let pagesToShow = pages;
if (tab === Tab.Published) {
pagesToShow = pages.filter(page => !page.draft);
} else if (tab === Tab.Draft) {
pagesToShow = pages.filter(page => !!page.draft);
} else {
pagesToShow = pages;
}
let pagesSorted = pagesToShow;
if (sorting === SortOption.FileNameAsc) {
pagesSorted = pagesToShow.sort((a, b) => a.fmFileName.toLowerCase().localeCompare(b.fmFileName.toLowerCase()));
} else if (sorting === SortOption.FileNameDesc) {
pagesSorted = pagesToShow.sort((a, b) => b.fmFileName.toLowerCase().localeCompare(a.fmFileName.toLowerCase()));
} else {
pagesSorted = pagesToShow.sort((a, b) => b.fmModified - a.fmModified);
}
// Show draft/published
// Filter by draft
// Filter by folder (if multiple)
// TODO: Sort by last modified
return (
<main className="h-full w-full">
<div className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
<Header currentTab={tab}
currentSorting={sorting}
switchTab={(tabId: Tab) => setTab(tabId)}
switchSorting={(sortId: SortOption) => setSorting(sortId)}
/>
<Overview pages={pagesSorted} />
</div>
{ loading ? <Spinner /> : null }
</main>
);
};
+16
View File
@@ -0,0 +1,16 @@
import { format, parseJSON } from 'date-fns';
import * as React from 'react';
export interface IDateFieldProps {
value: Date | string;
}
export const DateField: React.FunctionComponent<IDateFieldProps> = ({value}: React.PropsWithChildren<IDateFieldProps>) => {
const parsedValue = typeof value === 'string' ? parseJSON(value) : value;
const dateString = format(parsedValue, 'yyyy-MM-dd');
return (
<span className={`text-vulcan-100 text-xs`}>{dateString}</span>
);
};
+97
View File
@@ -0,0 +1,97 @@
import { Menu, Transition } from '@headlessui/react';
import * as React from 'react';
import { Tab } from '../constants/Tab';
import { ChevronDownIcon } from '@heroicons/react/solid';
import { Fragment } from 'react';
import { SortOption } from '../constants/SortOption';
export interface IHeaderProps {
currentTab: Tab;
currentSorting: SortOption;
switchTab: (tabId: Tab) => void;
switchSorting: (sortId: SortOption) => void;
}
function classNames(...classes: any[]) {
return classes.filter(Boolean).join(' ')
}
export const tabs = [
{ name: 'All articles', id: Tab.All},
{ name: 'Published', id: Tab.Published },
{ name: 'In draft', id: Tab.Draft }
];
export const sortOptions = [
{ name: "Last modified", id: SortOption.LastModified },
{ name: "By filename (asc)", id: SortOption.FileNameAsc },
{ name: "By filename (desc)", id: SortOption.FileNameDesc },
];
export const Header: React.FunctionComponent<IHeaderProps> = ({currentTab, currentSorting, switchSorting, switchTab}: React.PropsWithChildren<IHeaderProps>) => {
return (
<div className="px-4 flex items-center border-b border-gray-200 mb-8 sticky top-0 z-50 bg-gray-50 shadow-sm">
<nav className="flex-1 -mb-px flex space-x-6 xl:space-x-8" aria-label="Tabs">
{tabs.map((tab) => (
<button
key={tab.name}
className={classNames(
tab.id === currentTab
? 'border-teal-900 text-teal-900'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300',
'whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm'
)}
aria-current={tab.id === currentTab ? 'page' : undefined}
onClick={() => switchTab(tab.id)}
>
{tab.name}
</button>
))}
</nav>
<div className="flex items-center ml-6">
<Menu as="div" className="relative z-10 inline-block text-left">
<div>
<Menu.Button className="group inline-flex justify-center text-sm font-medium text-gray-500 hover:text-gray-700">
Sort
<ChevronDownIcon
className="flex-shrink-0 -mr-1 ml-1 h-5 w-5 text-gray-400 group-hover:text-gray-500"
aria-hidden="true"
/>
</Menu.Button>
</div>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="origin-top-right absolute right-0 z-10 mt-2 w-40 rounded-md shadow-2xl bg-white ring-1 ring-vulcan-400 ring-opacity-5 focus:outline-none text-sm">
<div className="py-1">
{sortOptions.map((option) => (
<Menu.Item key={option.id}>
<button
onClick={() => switchSorting(option.id)}
className={classNames(
option.id === currentSorting ? 'text-vulcan-500' : 'text-gray-500',
'block px-4 py-2 text-sm font-medium w-full text-left hover:text-gray-700'
)}
>
{option.name}
</button>
</Menu.Item>
))}
</div>
</Menu.Items>
</Transition>
</Menu>
</div>
</div>
);
};
+44
View File
@@ -0,0 +1,44 @@
import * as React from 'react';
import { MessageHelper } from '../../helpers/MessageHelper';
import { DashboardMessage } from '../DashboardMessage';
import { Page } from '../models/Page';
import { DateField } from './DateField';
import { Status } from './Status';
export interface IItemProps extends Page {}
export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, title, draft, description, preview }: React.PropsWithChildren<IItemProps>) => {
const openFile = () => {
MessageHelper.sendMessage(DashboardMessage.openFile, fmFilePath);
};
return (
<li className="relative">
<button className={`group cursor-pointer flex flex-wrap items-start content-start h-full w-full rounded-lg bg-gray-50 text-vulcan-500 text-left overflow-hidden shadow-md hover:shadow-xl`}
onClick={openFile}>
<div className="relative h-36 w-full overflow-hidden">
{
preview ? (
<img src={`${preview}`} alt={title} className="absolute inset-0 h-full w-full object-cover" loading="lazy" />
) : (
<img src={`https://images.unsplash.com/photo-1598620617148-c9e8ddee6711?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1950&q=80`} alt={title} className="absolute inset-0 h-full w-full object-cover group-hover:opacity-95" loading="lazy" />
)
}
</div>
<div className="p-4">
<div className={`flex justify-between items-center`}>
<Status draft={!!draft} />
<DateField value={date} />
</div>
<h2 className="mt-2 mb-2 font-bold">{title}</h2>
<p className="text-xs text-vulcan-200">{description}</p>
</div>
</button>
</li>
);
};
+11
View File
@@ -0,0 +1,11 @@
import * as React from 'react';
export interface IListProps {}
export const List: React.FunctionComponent<IListProps> = ({children}: React.PropsWithChildren<IListProps>) => {
return (
<ul role="list" className="grid grid-cols-2 gap-x-4 gap-y-8 sm:grid-cols-3 sm:gap-x-6 lg:grid-cols-4 xl:gap-x-8">
{children}
</ul>
);
};
+19
View File
@@ -0,0 +1,19 @@
import * as React from 'react';
import { Page } from '../models/Page';
import { Item } from './Item';
import { List } from './List';
export interface IOverviewProps {
pages: Page[];
}
export const Overview: React.FunctionComponent<IOverviewProps> = ({pages}: React.PropsWithChildren<IOverviewProps>) => {
return (
<List>
{pages.map(page => (
<Item key={page.slug} {...page} />
))}
</List>
);
};
+11
View File
@@ -0,0 +1,11 @@
import * as React from 'react';
export interface ISpinnerProps {}
export const Spinner: React.FunctionComponent<ISpinnerProps> = (props: React.PropsWithChildren<ISpinnerProps>) => {
return (
<div className={`fixed top-0 left-0 right-0 bottom-0 w-full h-full flex flex-wrap items-center justify-center bg-white bg-opacity-10 z-40`}>
<div className="loader ease-linear rounded-full border-8 border-t-8 border-gray-50 h-32 w-32" />
</div>
);
};
+11
View File
@@ -0,0 +1,11 @@
import * as React from 'react';
export interface IStatusProps {
draft: boolean;
}
export const Status: React.FunctionComponent<IStatusProps> = ({draft}: React.PropsWithChildren<IStatusProps>) => {
return (
<span className={`inline-block px-2 py-1 leading-none rounded-full font-semibold uppercase tracking-wide text-xs ${draft ? "bg-red-500 text-whisper-200" : "bg-teal-500 text-whisper-500"}`}>{draft ? "Draft" : "Published"}</span>
);
};
+5
View File
@@ -0,0 +1,5 @@
export enum SortOption {
LastModified = 1,
FileNameAsc,
FileNameDesc
}
+5
View File
@@ -0,0 +1,5 @@
export enum Tab {
All = 'all',
Published = 'published',
Draft = 'draft',
};
+36
View File
@@ -0,0 +1,36 @@
import { useState, useEffect } from 'react';
import { MessageHelper } from '../../helpers/MessageHelper';
import { DashboardCommand } from '../DashboardCommand';
import { DashboardMessage } from '../DashboardMessage';
import { Page } from '../models/Page';
const vscode = MessageHelper.getVsCodeAPI();
export default function useMessages(options?: any) {
const [loading, setLoading] = useState<boolean>(false);
const [pages, setPages] = useState<Page[]>([]);
window.addEventListener('message', event => {
const message = event.data;
switch (message.command) {
case DashboardCommand.loading:
setLoading(message.data);
break;
case DashboardCommand.data:
setPages(message.data);
setLoading(false);
break;
}
});
useEffect(() => {
setLoading(true);
vscode.postMessage({ command: DashboardMessage.getData });
}, ['']);
return {
loading,
pages
};
}
+14
View File
@@ -0,0 +1,14 @@
import * as React from "react";
import { render } from "react-dom";
import { Dashboard } from "./components/Dashboard";
import './styles.css';
declare const acquireVsCodeApi: <T = unknown>() => {
getState: () => T;
setState: (data: T) => void;
postMessage: (msg: unknown) => void;
};
const elm = document.querySelector("#app");
render(<Dashboard />, elm);
+17
View File
@@ -0,0 +1,17 @@
import { Uri } from "vscode";
export interface Page {
fmGroup: string;
fmFilePath: string;
fmFileName: string;
fmModified: number;
title: string;
slug: string;
date: string | Date;
draft: string;
description: string;
preview?: string;
[prop: string]: any;
}
+19
View File
@@ -0,0 +1,19 @@
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
.loader {
border-top-color: var(--vscode-activityBar-activeBorder);;
animation: spinner 1.5s linear infinite;
}
@-webkit-keyframes spinner {
0% { -webkit-transform: rotate(0deg); }
100% { -webkit-transform: rotate(360deg); }
}
@keyframes spinner {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}