mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-08-12 03:43:01 +02:00
#65 - Dashboard implementation
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
export enum DashboardCommand {
|
||||
loading = "loading",
|
||||
data = "data"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum DashboardMessage {
|
||||
getData = 'getData',
|
||||
openFile = 'openFile',
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
export enum SortOption {
|
||||
LastModified = 1,
|
||||
FileNameAsc,
|
||||
FileNameDesc
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export enum Tab {
|
||||
All = 'all',
|
||||
Published = 'published',
|
||||
Draft = 'draft',
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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); }
|
||||
}
|
||||
Reference in New Issue
Block a user