Skip to content

Commit

Permalink
feat: new backend dashboard [wip][ref Codeinwp/neve-pro-addon#2914]
Browse files Browse the repository at this point in the history
  • Loading branch information
abaicus committed Jan 15, 2025
1 parent 8258a95 commit 3687c39
Show file tree
Hide file tree
Showing 47 changed files with 2,237 additions and 589 deletions.
36 changes: 17 additions & 19 deletions assets/apps/dashboard/src/Components/App.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
import Container from '../Layout/Container';
import { fetchOptions } from '../utils/rest';
import Sidebar from './Content/Sidebar/Sidebar';
import Header from './Header';
import Notifications from './Notifications';
import TabsContent from './TabsContent';
import Sidebar from './Sidebar';
import Loading from './Loading';
import SkeletonLoader from './SkeletonLoader';
import Snackbar from './Snackbar';
import Container from '../Layout/Container';
import { fetchOptions } from '../utils/rest';

import { useDispatch, useSelect } from '@wordpress/data';
import { useState, useEffect } from '@wordpress/element';
import Deal from './Deal';
import { useEffect, useState } from '@wordpress/element';
import { tabs } from '../utils/common';
import { TransitionWrapper } from './Common/TransitionWrapper';
import { NEVE_STORE } from '../utils/constants';

const App = () => {
const [loading, setLoading] = useState(true);

const { setSettings, setTab } = useDispatch('neve-dashboard');
const { setSettings, setTab } = useDispatch(NEVE_STORE);

const { toast, currentTab } = useSelect((select) => {
const { getToast, getTab } = select('neve-dashboard');
const { currentTab } = useSelect((select) => {
const { getTab } = select(NEVE_STORE);
return {
toast: getToast(),
currentTab: getTab(),
};
});
Expand All @@ -32,7 +32,7 @@ const App = () => {
}, []);

if (loading) {
return <Loading />;
return <SkeletonLoader />;
}
return (
<div className="antialiased grow flex flex-col gap-6 h-full">
Expand All @@ -42,18 +42,16 @@ const App = () => {
{'starter-sites' !== currentTab && <Notifications />}

<Container className="flex flex-col lg:flex-row gap-6 h-full grow">
<div className="grow">
<TabsContent currentTab={currentTab} setTab={setTab} />
</div>
<div className="grow">{tabs[currentTab].render(setTab)}</div>

{'starter-sites' !== currentTab && (
<div className="shrink-0 lg:w-[435px]">
{!['starter-sites', 'settings'].includes(currentTab) && (
<TransitionWrapper className="shrink-0 lg:w-[435px]">
<Sidebar />
</div>
</TransitionWrapper>
)}
</Container>

{toast && <Snackbar />}
<Snackbar />
</div>
);
};
Expand Down
8 changes: 5 additions & 3 deletions assets/apps/dashboard/src/Components/Common/Button.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const Button = (props) => {
const {
href,
onClick,
className,
className = '',
isSubmit,
isPrimary,
isSecondary,
Expand All @@ -17,20 +17,22 @@ const Button = (props) => {
} = props;

const classNames = cn([
'flex items-center px-3 py-2 transition-colors duration-150 rounded text-sm border gap-2',
'flex items-center px-3 py-2 transition-colors duration-150 text-sm border gap-2',
{
rounded: !className.includes('rounded'),
'border-transparent bg-blue-600 text-white hover:bg-blue-700 hover:text-white':
isPrimary,
'border-blue-600 text-blue-600 hover:bg-blue-600 hover:text-white':
isSecondary,
'border-transparent text-gray-600 hover:text-gray-900': isLink,
'cursor-not-allowed opacity-50 pointer-events-none': disabled,
'cursor-not-allowed opacity-50': disabled,
},
className,
]);

const passedProps = {
className: classNames,
disabled,
onClick,
};

Expand Down
122 changes: 122 additions & 0 deletions assets/apps/dashboard/src/Components/Common/Multiselect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { useRef, useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import cn from 'classnames';
import { Check, ChevronDown } from 'lucide-react';
import { useEffect } from 'react';
const MultiSelect = ({ value, label, disabled, choices = {}, onChange }) => {
const [isOpen, setIsOpen] = useState(false);

const dropdownRef = useRef(null);

const closeDropdown = (e) => {
if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
setIsOpen(false);
}
};

useEffect(() => {
if (isOpen) {
document.addEventListener('click', closeDropdown);
} else {
document.removeEventListener('click', closeDropdown);
}

return () => {
document.removeEventListener('click', closeDropdown);
};
}, [isOpen]);

const handleChange = (optionValue) => {
const nextValues = value.includes(optionValue)
? value.filter((v) => v !== optionValue)
: [...value, optionValue];
onChange(nextValues);
};

return (
<div className="grid gap-1">
{label && (
<span className="text-sm text-gray-600 font-medium">
{label}
</span>
)}
<div className="relative">
<button
onClick={() => !disabled && setIsOpen(!isOpen)}
className={cn(
'relative w-full py-1.5 px-2 text-sm rounded border flex items-center gap-3 min-w-[200px]',
'border-gray-300 hover:border-gray-500',
{
'bg-gray-100': disabled,
}
)}
disabled={disabled}
>
<div className="flex flex-wrap gap-1 max-w-[160px] overflow-hidden">
{Object.entries(value).length === 0 ? (
<span className="text-gray-500">
{__('Select options', 'neve')}...
</span>
) : (
value.map((choice, index) => {
if (!choices[choice]) {
return null;
}

return (
<span
key={index}
className="inline-flex items-center gap-1 bg-blue-100 text-blue-800 rounded px-2 py-0.5 text-xs"
>
{choices[choice]}
</span>
);
})
)}
</div>

<ChevronDown
size={18}
className={`ml-auto transition-transform ${
isOpen ? 'transform rotate-180' : ''
}`}
aria-hidden="true"
/>
</button>

{isOpen && (
<div
ref={dropdownRef}
className="absolute z-10 w-full mt-1 bg-white border rounded shadow-lg"
>
<div className="max-h-60 overflow-y-auto p-1">
{Object.entries(choices).map(
([optionValue, optionLabel]) => (
<button
key={optionValue}
className="flex w-full items-center gap-2 text-sm rounded py-1.5 px-3 hover:bg-blue-100 hover:text-blue-700 cursor-pointer"
onClick={() =>
handleChange(optionValue)
}
>
<div className="w-4 h-4 border bg-white rounded flex items-center justify-center">
{value.includes(optionValue) && (
<Check
size={12}
className="text-blue-600"
/>
)}
</div>
{optionLabel}
</button>
)
)}
</div>
</div>
)}
</div>
</div>
);
};

export default MultiSelect;
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import Tooltip from './Tooltip';
import TransitionInOut from './TransitionInOut';
import { useEffect } from 'react';

const Notification = ({ data, slug }) => {
const Notification = ({ data }) => {
const [hidden, setHidden] = useState(false);
const { text, cta, type, update, url, targetBlank } = data;
const { canInstallPlugins } = neveDash;
Expand Down
2 changes: 1 addition & 1 deletion assets/apps/dashboard/src/Components/Common/Pill.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export default ({ children, type = 'primary', className }) => {
const typeClasses = {
primary: 'bg-blue-100 text-blue-700',
secondary: 'bg-gray-100 text-gray-700',
success: 'bg-green-100 text-green-700',
success: 'bg-lime-100 text-lime-700',
error: 'bg-red-100 text-red-700',
warning: 'bg-yellow-100 text-yellow-700',
};
Expand Down
92 changes: 92 additions & 0 deletions assets/apps/dashboard/src/Components/Common/Select.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import cn from 'classnames';
import { LoaderCircle, LucideChevronDown } from 'lucide-react';

import {
Field,
Label,
Listbox,
ListboxButton,
ListboxOption,
ListboxOptions,
} from '@headlessui/react';
import { __ } from '@wordpress/i18n';

export default ({
label,
value,
onChange,
disabled = false,
loading,
choices,
}) => {
return (
<Field className="grid gap-1">
{label && (
<Label className="text-sm text-gray-600 font-medium">
{label}
</Label>
)}
<div className="flex items-center gap-3">
{loading && (
<LoaderCircle size={18} className="animate-spin shrink-0" />
)}
<Listbox
value={value}
onChange={onChange}
disabled={loading || disabled}
>
{({ open }) => (
<>
<ListboxButton
className={cn(
'relative w-full rounded py-1.5 px-2 text-left',
'rounded border border-gray-300 hover:border-gray-500',
'flex items-center gap-3 min-w-[200px]',
{
'bg-gray-100': disabled || loading,
}
)}
>
{choices[value] ||
__('Select an option', 'neve')}

<LucideChevronDown
size={18}
className={cn(
'ml-auto transition-transform',
{
'transform rotate-180': open,
}
)}
aria-hidden="true"
/>
</ListboxButton>

<ListboxOptions
anchor="bottom"
transition
className={cn(
'text-sm font-normal shadow-lg',
'rounded border bg-white p-1 my-1 min-w-[200px]',
'transition duration-100 ease-in data-[leave]:data-[closed]:opacity-0 antialiased'
)}
>
{Object.entries(choices).map(
([optionValue, optionLabel]) => (
<ListboxOption
key={optionValue}
value={optionValue}
className="flex w-full items-center gap-2 text-sm rounded py-1.5 px-3 hover:bg-blue-100 hover:text-blue-700 data-[focus]:bg-blue-100 data-[focus]:text-blue-700 cursor-pointer"
>
{optionLabel}
</ListboxOption>
)
)}
</ListboxOptions>
</>
)}
</Listbox>
</div>
</Field>
);
};
52 changes: 52 additions & 0 deletions assets/apps/dashboard/src/Components/Common/TextInput.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Description, Field, Input, Label } from '@headlessui/react';
import { Fragment } from '@wordpress/element';
import cn from 'classnames';

const TextInput = ({
value,
label,
disabled,
onChange,
name,
className = '',
type = 'text',
description,
}) => {
let TagName = 'input';

if (type === 'textarea') {
TagName = 'textarea';
}

return (
<Field className="grid gap-1">
{label && (
<Label className="text-sm text-gray-600 font-medium">
{label}
</Label>
)}
{description && (
<Description className="text-xs text-gray-600">
{description}
</Description>
)}
<Input type={type} as={Fragment} name={name}>
<TagName
type={type}
value={value}
onChange={onChange}
disabled={disabled}
className={cn(
'block w-full px-3 !py-1 text-sm border rounded disabled:bg-gray-100 !border-gray-300 focus:!border-gray-500 focus:!shadow-none',
{
'cursor-not-allowed': disabled,
},
className
)}
/>
</Input>
</Field>
);
};

export default TextInput;
Loading

0 comments on commit 3687c39

Please sign in to comment.