mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-08-07 01:13:08 +02:00
#65 - Dashboard implementation
This commit is contained in:
Generated
+13
-3
@@ -48,6 +48,16 @@
|
||||
"lit-element": "^2.5.1"
|
||||
}
|
||||
},
|
||||
"@headlessui/react": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.4.0.tgz",
|
||||
"integrity": "sha512-C+FmBVF6YGvqcEI5fa2dfVbEaXr2RGR6Kw1E5HXIISIZEfsrH/yuCgsjWw5nlRF9vbCxmQ/EKs64GAdKeb8gCw=="
|
||||
},
|
||||
"@heroicons/react": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@heroicons/react/-/react-1.0.4.tgz",
|
||||
"integrity": "sha512-3kOrTmo8+Z8o6AL0rzN82MOf8J5CuxhRLFhpI8mrn+3OqekA6d5eb1GYO3EYYo1Vn6mYQSMNTzCWbEwUInb0cQ=="
|
||||
},
|
||||
"@iarna/toml": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.3.tgz",
|
||||
@@ -1487,9 +1497,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"date-fns": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.0.1.tgz",
|
||||
"integrity": "sha512-C14oTzTZy8DH1Eq8N78owrCWvf3+cnJw88BTK/N3DYWVxDJuJzPaNdplzYxDYuuXXGvqBcO4Vy5SOrwAooXSWw==",
|
||||
"version": "2.23.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.23.0.tgz",
|
||||
"integrity": "sha512-5ycpauovVyAk0kXNZz6ZoB9AYMZB4DObse7P3BPWmyEjXNORTI8EJ6X0uaSAq4sCHzM1uajzrkr6HnsLQpxGXA==",
|
||||
"dev": true
|
||||
},
|
||||
"debug": {
|
||||
|
||||
+18
-1
@@ -59,6 +59,7 @@
|
||||
"onCommand:frontMatter.init",
|
||||
"onCommand:frontMatter.collapseSections",
|
||||
"onCommand:frontMatter.preview",
|
||||
"onCommand:frontMatter.dashboard",
|
||||
"onView:frontMatter.explorer"
|
||||
],
|
||||
"main": "./dist/extension",
|
||||
@@ -101,6 +102,11 @@
|
||||
"default": [],
|
||||
"markdownDescription": "This array of folders defines where the extension can easily create new content by running the create article command."
|
||||
},
|
||||
"frontMatter.content.publicFolder": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"markdownDescription": "Specify the folder name where all your assets are located. For instance in Hugo this is the `static` folder."
|
||||
},
|
||||
"frontMatter.custom.scripts": {
|
||||
"type": "array",
|
||||
"default": [],
|
||||
@@ -299,6 +305,11 @@
|
||||
"command": "frontMatter.preview",
|
||||
"title": "Preview article",
|
||||
"category": "Front matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.dashboard",
|
||||
"title": "Open pages dashboard",
|
||||
"category": "Front matter"
|
||||
}
|
||||
],
|
||||
"menus": {
|
||||
@@ -335,6 +346,10 @@
|
||||
{
|
||||
"command": "frontMatter.collapseSections",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.dashboard",
|
||||
"when": "frontMatterCanOpenDashboard"
|
||||
}
|
||||
],
|
||||
"view/title": [
|
||||
@@ -375,7 +390,7 @@
|
||||
"@vscode/codicons": "0.0.20",
|
||||
"autoprefixer": "^10.3.2",
|
||||
"css-loader": "5.2.7",
|
||||
"date-fns": "2.0.1",
|
||||
"date-fns": "2.23.0",
|
||||
"downshift": "6.0.6",
|
||||
"glob": "7.1.6",
|
||||
"gray-matter": "4.0.2",
|
||||
@@ -396,6 +411,8 @@
|
||||
"webpack-cli": "3.3.12"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "1.4.0",
|
||||
"@heroicons/react": "1.0.4",
|
||||
"lodash.uniqby": "4.7.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
import { SETTINGS_CONTENT_STATIC_FOLDERS, SETTING_DATE_FIELD, SETTING_PREVIEW_HOST, SETTING_PREVIEW_PATHNAME, SETTING_SEO_DESCRIPTION_FIELD } from './../constants/settings';
|
||||
import { ArticleHelper } from './../helpers/ArticleHelper';
|
||||
import { join } from "path";
|
||||
import { commands, env, Uri, ViewColumn, Webview, WebviewOptions, WebviewPanel, WebviewPanelOptions, window, workspace } from "vscode";
|
||||
import { SettingsHelper } from '../helpers';
|
||||
import { PreviewSettings } from '../models';
|
||||
import { format } from 'date-fns';
|
||||
import { CONTEXT } from '../constants/context';
|
||||
import { Folders } from './Folders';
|
||||
import { getNonce } from '../helpers/getNonce';
|
||||
import { DashboardCommand } from '../pagesView/DashboardCommand';
|
||||
import { DashboardMessage } from '../pagesView/DashboardMessage';
|
||||
import { Page } from '../pagesView/models/Page';
|
||||
import { openFileInEditor } from '../helpers/openFileInEditor';
|
||||
|
||||
|
||||
export class Dashboard {
|
||||
private static webview: WebviewPanel | null = null;
|
||||
|
||||
/**
|
||||
* Init the dashboard
|
||||
*/
|
||||
public static async init() {
|
||||
const folders = Folders.get();
|
||||
await commands.executeCommand('setContext', CONTEXT.canOpenDashboard, folders && folders.length > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the markdown preview in the editor
|
||||
*/
|
||||
public static async open(extensionPath: string) {
|
||||
|
||||
// Create the preview webview
|
||||
Dashboard.webview = window.createWebviewPanel(
|
||||
'frontMatterDashboard',
|
||||
'FrontMatter Dashboard',
|
||||
ViewColumn.One,
|
||||
{
|
||||
enableScripts: true
|
||||
}
|
||||
);
|
||||
|
||||
Dashboard.webview.iconPath = {
|
||||
dark: Uri.file(join(extensionPath, 'assets/frontmatter-dark.svg')),
|
||||
light: Uri.file(join(extensionPath, 'assets/frontmatter.svg'))
|
||||
};
|
||||
|
||||
Dashboard.webview.webview.html = Dashboard.getWebviewContent(Dashboard.webview.webview, Uri.parse(extensionPath));
|
||||
|
||||
Dashboard.webview.onDidChangeViewState(() => {
|
||||
if (this.webview?.visible) {
|
||||
console.log(`Dashboard opened`);
|
||||
}
|
||||
});
|
||||
|
||||
Dashboard.webview.webview.onDidReceiveMessage(async (msg) => {
|
||||
switch(msg.command) {
|
||||
case DashboardMessage.getData:
|
||||
Dashboard.getPages();
|
||||
break;
|
||||
case DashboardMessage.openFile:
|
||||
openFileInEditor(msg.data);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private static async getPages() {
|
||||
const config = SettingsHelper.getConfig();
|
||||
const wsFolders = workspace.workspaceFolders;
|
||||
const crntWsFolder = wsFolders && wsFolders.length > 0 ? wsFolders[0] : null;
|
||||
|
||||
const descriptionField = config.get(SETTING_SEO_DESCRIPTION_FIELD) as string || "description";
|
||||
const dateField = config.get(SETTING_DATE_FIELD) as string || "date";
|
||||
const staticFolder = config.get<string>(SETTINGS_CONTENT_STATIC_FOLDERS);
|
||||
|
||||
const folderInfo = await Folders.getInfo();
|
||||
const pages: Page[] = [];
|
||||
|
||||
if (folderInfo) {
|
||||
for (const folder of folderInfo) {
|
||||
for (const file of folder.lastModified) {
|
||||
const article = ArticleHelper.getFrontMatterByPath(file.filePath);
|
||||
|
||||
if (article?.data.title) {
|
||||
const page: Page = {
|
||||
fmGroup: folder.title,
|
||||
fmModified: file.mtime,
|
||||
fmFilePath: file.filePath,
|
||||
fmFileName: file.fileName,
|
||||
title: article?.data.title,
|
||||
slug: article?.data.slug,
|
||||
date: article?.data[dateField] || "",
|
||||
draft: article?.data.draft,
|
||||
description: article?.data[descriptionField] || "",
|
||||
};
|
||||
|
||||
if (article?.data.preview && crntWsFolder) {
|
||||
const previewPath = join(crntWsFolder.uri.fsPath, staticFolder || "", article?.data.preview);
|
||||
const previewUri = Uri.file(previewPath);
|
||||
const preview = Dashboard.webview?.webview.asWebviewUri(previewUri);
|
||||
page.preview = preview?.toString() || "";
|
||||
}
|
||||
|
||||
pages.push(page);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Dashboard.postWebviewMessage({
|
||||
command: DashboardCommand.data,
|
||||
data: pages
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Post data to the dashboard
|
||||
* @param msg
|
||||
*/
|
||||
private static postWebviewMessage(msg: { command: DashboardCommand, data?: any }) {
|
||||
Dashboard.webview?.webview.postMessage(msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the webview HTML contents
|
||||
* @param webView
|
||||
*/
|
||||
private static getWebviewContent(webView: Webview, extensionPath: Uri): string {
|
||||
const scriptUri = webView.asWebviewUri(Uri.joinPath(extensionPath, 'dist', 'pages.js'));
|
||||
|
||||
const nonce = getNonce();
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" style="width:100%;height:100%;margin:0;padding:0;">
|
||||
<head>
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src https://images.unsplash.com/ ${`vscode-file://vscode-app`} ${webView.cspSource} https://api.visitorbadge.io 'self' 'unsafe-inline'; script-src 'nonce-${nonce}'; style-src ${webView.cspSource} 'self' 'unsafe-inline'; font-src ${webView.cspSource}">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<title>Front Matter</title>
|
||||
</head>
|
||||
<body style="width:100%;height:100%;margin:0;padding:0;background:rgba(250, 250, 250, 1);">
|
||||
<div id="app" style="width:100%;height:100%;margin:0;padding:0;"></div>
|
||||
|
||||
<img style="display:none" src="https://api.visitorbadge.io/api/combined?user=estruyf&repo=frontmatter-usage&countColor=%23263759" alt="Daily usage" />
|
||||
|
||||
<script nonce="${nonce}" src="${scriptUri}"></script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -122,7 +122,7 @@ export class Folders {
|
||||
/**
|
||||
* Get the registered folders information
|
||||
*/
|
||||
public static async getInfo(): Promise<FolderInfo[] | null> {
|
||||
public static async getInfo(limit?: number): Promise<FolderInfo[] | null> {
|
||||
const folders = Folders.get();
|
||||
if (folders && folders.length > 0) {
|
||||
let folderInfo: FolderInfo[] = [];
|
||||
@@ -149,7 +149,11 @@ export class Folders {
|
||||
}
|
||||
}
|
||||
|
||||
fileStats = fileStats.sort((a, b) => b.mtime - a.mtime).slice(0, 10);
|
||||
fileStats = fileStats.sort((a, b) => b.mtime - a.mtime);
|
||||
|
||||
if (limit) {
|
||||
fileStats = fileStats.slice(0, limit);
|
||||
}
|
||||
|
||||
folderInfo.push({
|
||||
title: folder.title,
|
||||
@@ -172,7 +176,7 @@ export class Folders {
|
||||
* Get the folder settings
|
||||
* @returns
|
||||
*/
|
||||
private static get() {
|
||||
public static get() {
|
||||
const config = SettingsHelper.getConfig();
|
||||
const folders: ContentFolder[] = config.get(SETTINGS_CONTENT_FOLDERS) as ContentFolder[];
|
||||
return folders;
|
||||
|
||||
@@ -23,4 +23,5 @@ export const COMMAND_NAME = {
|
||||
createTemplate: getCommandName("createTemplate"),
|
||||
collapseSections: getCommandName("collapseSections"),
|
||||
preview: getCommandName("preview"),
|
||||
dashboard: getCommandName("dashboard"),
|
||||
};
|
||||
@@ -1,7 +1,6 @@
|
||||
|
||||
|
||||
export const CONTEXT = {
|
||||
canInit: "frontMatterCanInit",
|
||||
canOpenPreview: "frontMatterCanOpenPreview",
|
||||
canOpenDashboard: "frontMatterCanOpenDashboard",
|
||||
registeredFolders: 'frontMatter.registeredFolders'
|
||||
};
|
||||
@@ -34,4 +34,5 @@ export const SETTING_CUSTOM_SCRIPTS = "custom.scripts";
|
||||
|
||||
export const SETTING_AUTO_UPDATE_DATE = "content.autoUpdateDate";
|
||||
export const SETTINGS_CONTENT_FOLDERS = "content.folders";
|
||||
export const SETTINGS_CONTENT_STATIC_FOLDERS = "content.publicFolder";
|
||||
export const SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT = "content.fmHighlight";
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Dashboard } from './commands/Dashboard';
|
||||
import * as vscode from 'vscode';
|
||||
import { Article, Settings, StatusListener } from './commands';
|
||||
import { Folders } from './commands/Folders';
|
||||
@@ -107,6 +108,7 @@ export async function activate({ subscriptions, extensionUri, extensionPath }: v
|
||||
vscode.workspace.onDidChangeConfiguration(() => {
|
||||
Template.init();
|
||||
Preview.init();
|
||||
Dashboard.init();
|
||||
Folders.updateVsCodeCtx();
|
||||
|
||||
const exView = ExplorerView.getInstance();
|
||||
@@ -144,6 +146,10 @@ export async function activate({ subscriptions, extensionUri, extensionPath }: v
|
||||
Preview.init();
|
||||
subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.preview, () => Preview.open(extensionPath) ));
|
||||
|
||||
// Pages dashboard
|
||||
Dashboard.init();
|
||||
subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.dashboard, () => Dashboard.open(extensionPath) ));
|
||||
|
||||
// Subscribe all commands
|
||||
subscriptions.push(
|
||||
insertTags,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { CommandToCode } from "../CommandToCode";
|
||||
import { DashboardMessage } from './../pagesView/DashboardMessage';
|
||||
import { CommandToCode } from "../viewpanel/CommandToCode";
|
||||
|
||||
|
||||
interface ClientVsCode<T> {
|
||||
getState: () => T;
|
||||
@@ -16,7 +18,7 @@ export class MessageHelper {
|
||||
return MessageHelper.vscode;
|
||||
}
|
||||
|
||||
public static sendMessage = (command: CommandToCode, data?: any) => {
|
||||
public static sendMessage = (command: CommandToCode | DashboardMessage, data?: any) => {
|
||||
if (data) {
|
||||
MessageHelper.vscode.postMessage({ command, data });
|
||||
} else {
|
||||
@@ -0,0 +1,10 @@
|
||||
|
||||
|
||||
export const getNonce = () => {
|
||||
let text = '';
|
||||
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
for (let i = 0; i < 32; i++) {
|
||||
text += possible.charAt(Math.floor(Math.random() * possible.length));
|
||||
}
|
||||
return text;
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Uri, workspace, window } from "vscode";
|
||||
import { Notifications } from "./Notifications";
|
||||
|
||||
export const openFileInEditor = async (filePath: string) => {
|
||||
if (filePath) {
|
||||
try {
|
||||
const doc = await workspace.openTextDocument(Uri.file(filePath));
|
||||
await window.showTextDocument(doc, 1, false);
|
||||
} catch (e) {
|
||||
Notifications.error(`Couldn't open the file.`);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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); }
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { FolderInfo, PanelSettings } from '../../models';
|
||||
import { CommandToCode } from '../CommandToCode';
|
||||
import { MessageHelper } from '../helper/MessageHelper';
|
||||
import { MessageHelper } from '../../helpers/MessageHelper';
|
||||
import { Collapsible } from './Collapsible';
|
||||
import { GlobalSettings } from './GlobalSettings';
|
||||
import { OtherActions } from './OtherActions';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { CommandToCode } from '../CommandToCode';
|
||||
import { MessageHelper } from '../helper/MessageHelper';
|
||||
import { MessageHelper } from '../../helpers/MessageHelper';
|
||||
import { ActionButton } from './ActionButton';
|
||||
|
||||
export interface ICustomScriptProps {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { CommandToCode } from '../CommandToCode';
|
||||
import { MessageHelper } from '../helper/MessageHelper';
|
||||
import { MessageHelper } from '../../helpers/MessageHelper';
|
||||
import { ActionButton } from './ActionButton';
|
||||
|
||||
export interface IDateActionProps {}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { FileInfo } from '../../models';
|
||||
import { CommandToCode } from '../CommandToCode';
|
||||
import { MessageHelper } from '../helper/MessageHelper';
|
||||
import { MessageHelper } from '../../helpers/MessageHelper';
|
||||
import { FileIcon } from './Icons/FileIcon';
|
||||
import { MarkdownIcon } from './Icons/MarkdownIcon';
|
||||
import { VsLabel } from './VscodeComponents';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { PanelSettings } from '../../models';
|
||||
import { CommandToCode } from '../CommandToCode';
|
||||
import { MessageHelper } from '../helper/MessageHelper';
|
||||
import { MessageHelper } from '../../helpers/MessageHelper';
|
||||
import { useDebounce } from '../hooks/useDebounce';
|
||||
import { Collapsible } from './Collapsible';
|
||||
import { VsCheckbox, VsLabel } from './VscodeComponents';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { PanelSettings } from '../../models';
|
||||
import { CommandToCode } from '../CommandToCode';
|
||||
import { MessageHelper } from '../helper/MessageHelper';
|
||||
import { MessageHelper } from '../../helpers/MessageHelper';
|
||||
import { TagType } from '../TagType';
|
||||
import { Collapsible } from './Collapsible';
|
||||
import { Toggle } from './Fields/Toggle';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { PanelSettings } from '../../models';
|
||||
import { CommandToCode } from '../CommandToCode';
|
||||
import { MessageHelper } from '../helper/MessageHelper';
|
||||
import { MessageHelper } from '../../helpers/MessageHelper';
|
||||
import { Collapsible } from './Collapsible';
|
||||
import { BugIcon } from './Icons/BugIcon';
|
||||
import { CenterIcon } from './Icons/CenterIcon';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { MessageHelper } from '../../helpers/MessageHelper';
|
||||
import { CommandToCode } from '../CommandToCode';
|
||||
import { MessageHelper } from '../helper/MessageHelper';
|
||||
import { ActionButton } from './ActionButton';
|
||||
|
||||
export interface IPreviewProps {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
|
||||
|
||||
import * as React from 'react';
|
||||
import { MessageHelper } from '../../helpers/MessageHelper';
|
||||
import { CommandToCode } from '../CommandToCode';
|
||||
import { MessageHelper } from '../helper/MessageHelper';
|
||||
import { ActionButton } from './ActionButton';
|
||||
|
||||
export interface IPublishActionProps {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as React from 'react';
|
||||
import { MessageHelper } from '../../helpers/MessageHelper';
|
||||
import { SlugHelper } from '../../helpers/SlugHelper';
|
||||
import { Slug } from '../../models/PanelSettings';
|
||||
import { CommandToCode } from '../CommandToCode';
|
||||
import { MessageHelper } from '../helper/MessageHelper';
|
||||
import { ActionButton } from './ActionButton';
|
||||
|
||||
export interface ISlugActionProps {
|
||||
|
||||
@@ -3,10 +3,10 @@ import { Tags } from './Tags';
|
||||
import { usePrevious } from '../hooks/usePrevious';
|
||||
import { CommandToCode } from '../CommandToCode';
|
||||
import { TagType } from '../TagType';
|
||||
import { MessageHelper } from '../helper/MessageHelper';
|
||||
import Downshift from 'downshift';
|
||||
import { AddIcon } from './Icons/AddIcon';
|
||||
import { VsLabel } from './VscodeComponents';
|
||||
import { MessageHelper } from '../../helpers/MessageHelper';
|
||||
|
||||
export interface ITagPickerProps {
|
||||
type: string;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { MessageHelper } from '../../helpers/MessageHelper';
|
||||
import { FolderInfo, PanelSettings } from '../../models/PanelSettings';
|
||||
import { Command } from '../Command';
|
||||
import { CommandToCode } from '../CommandToCode';
|
||||
import { MessageHelper } from '../helper/MessageHelper';
|
||||
import { TagType } from '../TagType';
|
||||
|
||||
const vscode = MessageHelper.getVsCodeAPI();
|
||||
|
||||
@@ -2,8 +2,6 @@ import * as React from "react";
|
||||
import { render } from "react-dom";
|
||||
import { ViewPanel } from "./ViewPanel";
|
||||
|
||||
import './styles.css';
|
||||
|
||||
// require('@vscode/codicons/dist/codicon.css');
|
||||
import '@bendera/vscode-webview-elements/dist/vscode-table';
|
||||
import '@bendera/vscode-webview-elements/dist/vscode-table-header';
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
@import "tailwindcss/base";
|
||||
@import "tailwindcss/components";
|
||||
@import "tailwindcss/utilities";
|
||||
@@ -18,7 +18,10 @@ import { Notifications } from '../helpers/Notifications';
|
||||
import { COMMAND_NAME } from '../constants/Extension';
|
||||
import { Folders } from '../commands/Folders';
|
||||
import { Preview } from '../commands/Preview';
|
||||
import { getNonce } from '../helpers/getNonce';
|
||||
import { openFileInEditor } from '../helpers/openFileInEditor';
|
||||
|
||||
const FILE_LIMIT = 10;
|
||||
|
||||
export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
public static readonly viewType = "frontMatter.explorer";
|
||||
@@ -170,7 +173,7 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
this.updatePreviewUrl(msg.data || "");
|
||||
break;
|
||||
case CommandToCode.openInEditor:
|
||||
this.openFileInEditor(msg.data);
|
||||
openFileInEditor(msg.data);
|
||||
break;
|
||||
case CommandToCode.updateMetadata:
|
||||
this.updateMetadata(msg.data);
|
||||
@@ -253,20 +256,6 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
ArticleHelper.update(editor, article);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the file via its path
|
||||
*/
|
||||
private async openFileInEditor(filePath: string) {
|
||||
if (filePath) {
|
||||
try {
|
||||
const doc = await workspace.openTextDocument(Uri.file(filePath));
|
||||
await window.showTextDocument(doc, 1, false);
|
||||
} catch (e) {
|
||||
Notifications.error(`Couldn't open the file.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a custom script
|
||||
* @param msg
|
||||
@@ -348,7 +337,7 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
public async getFoldersAndFiles() {
|
||||
this.postWebviewMessage({
|
||||
command: Command.folderInfo,
|
||||
data: await Folders.getInfo() || null
|
||||
data: await Folders.getInfo(FILE_LIMIT) || null
|
||||
});
|
||||
}
|
||||
|
||||
@@ -530,18 +519,6 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
this.panel!.webview.postMessage(msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unique nonce
|
||||
*/
|
||||
private getNonce() {
|
||||
let text = '';
|
||||
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
for (let i = 0; i < 32; i++) {
|
||||
text += possible.charAt(Math.floor(Math.random() * possible.length));
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the webview HTML contents
|
||||
* @param webView
|
||||
@@ -552,7 +529,7 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
const stylesUri = webView.asWebviewUri(Uri.joinPath(this.extPath, 'assets/media', 'styles.css'));
|
||||
const scriptUri = webView.asWebviewUri(Uri.joinPath(this.extPath, 'dist', 'viewpanel.js'));
|
||||
|
||||
const nonce = this.getNonce();
|
||||
const nonce = getNonce();
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
|
||||
+104
-1
@@ -1,9 +1,112 @@
|
||||
const colors = require('tailwindcss/colors');
|
||||
|
||||
module.exports = {
|
||||
mode: 'jit',
|
||||
purge: ['./src/**/*.{js,jsx,ts,tsx}'],
|
||||
darkMode: false, // or 'media' or 'class'
|
||||
theme: {
|
||||
extend: {},
|
||||
extend: {
|
||||
colors: {
|
||||
white: colors.white,
|
||||
gray: colors.trueGray,
|
||||
"red": {
|
||||
"50": "#ff7c7b",
|
||||
"100": "#ff7271",
|
||||
"200": "#ff6867",
|
||||
"300": "#ff5e5d",
|
||||
"400": "#ff5453",
|
||||
"500": "#fe4a49",
|
||||
"600": "#f4403f",
|
||||
"700": "#ea3635",
|
||||
"800": "#e02c2b",
|
||||
"900": "#d62221"
|
||||
},
|
||||
"blue": {
|
||||
"50": "#90dfff",
|
||||
"100": "#86d5ff",
|
||||
"200": "#7ccbff",
|
||||
"300": "#72c1ff",
|
||||
"400": "#68b7fc",
|
||||
"500": "#5eadf2",
|
||||
"600": "#54a3e8",
|
||||
"700": "#4a99de",
|
||||
"800": "#408fd4",
|
||||
"900": "#3685ca"
|
||||
},
|
||||
"teal": {
|
||||
"50": "#47f4fd",
|
||||
"100": "#3deaf3",
|
||||
"200": "#33e0e9",
|
||||
"300": "#29d6df",
|
||||
"400": "#1fccd5",
|
||||
"500": "#15c2cb",
|
||||
"600": "#0bb8c1",
|
||||
"700": "#01aeb7",
|
||||
"800": "#00a4ad",
|
||||
"900": "#009aa3"
|
||||
},
|
||||
"aqua": {
|
||||
"50": "#76ffff",
|
||||
"100": "#6cfffa",
|
||||
"200": "#62fff0",
|
||||
"300": "#58ffe6",
|
||||
"400": "#4effdc",
|
||||
"500": "#44ffd2",
|
||||
"600": "#3af5c8",
|
||||
"700": "#30ebbe",
|
||||
"800": "#26e1b4",
|
||||
"900": "#1cd7aa"
|
||||
},
|
||||
"yellow": {
|
||||
"50": "#ffff90",
|
||||
"100": "#ffff86",
|
||||
"200": "#ffff7c",
|
||||
"300": "#fff872",
|
||||
"400": "#ffee68",
|
||||
"500": "#ffe45e",
|
||||
"600": "#f5da54",
|
||||
"700": "#ebd04a",
|
||||
"800": "#e1c640",
|
||||
"900": "#d7bc36"
|
||||
},
|
||||
"whisper": {
|
||||
"50": "#ffffff",
|
||||
"100": "#ffffff",
|
||||
"200": "#ffffff",
|
||||
"300": "#ffffff",
|
||||
"400": "#fdf9ff",
|
||||
"500": "#f3eff5",
|
||||
"600": "#e9e5eb",
|
||||
"700": "#dfdbe1",
|
||||
"800": "#d5d1d7",
|
||||
"900": "#cbc7cd"
|
||||
},
|
||||
"vulcan": {
|
||||
"50": "#404551",
|
||||
"100": "#363b47",
|
||||
"200": "#2c313d",
|
||||
"300": "#222733",
|
||||
"400": "#181d29",
|
||||
"500": "#0e131f",
|
||||
"600": "#040915",
|
||||
"700": "#00000b",
|
||||
"800": "#000001",
|
||||
"900": "#000000"
|
||||
},
|
||||
"rose": {
|
||||
"50": "#ff73da",
|
||||
"100": "#ff69d0",
|
||||
"200": "#ff5fc6",
|
||||
"300": "#ff55bc",
|
||||
"400": "#fb4bb2",
|
||||
"500": "#f141a8",
|
||||
"600": "#e7379e",
|
||||
"700": "#dd2d94",
|
||||
"800": "#d3238a",
|
||||
"900": "#c91980"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
extend: {},
|
||||
|
||||
@@ -48,6 +48,34 @@ module.exports = [
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js', '.tsx', '.jsx']
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(ts|tsx)$/,
|
||||
exclude: /node_modules/,
|
||||
use: [{
|
||||
loader: 'ts-loader'
|
||||
}]
|
||||
}
|
||||
]
|
||||
},
|
||||
performance: {
|
||||
maxEntrypointSize: 400000,
|
||||
maxAssetSize: 400000
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'pagesView',
|
||||
target: 'web',
|
||||
entry: './src/pagesView/index.tsx',
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
filename: 'pages.js'
|
||||
},
|
||||
devtool: 'source-map',
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js', '.tsx', '.jsx']
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user