Merge pull request #1857 from SkynetLabs/dashboard-v2-files-page

Dashboard v2 files page
This commit is contained in:
Karol Wypchło 2022-03-16 12:18:57 +01:00 committed by GitHub
commit 69e52f6d72
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 558 additions and 99 deletions

View File

@ -0,0 +1,50 @@
import { useCallback, useRef, useState } from "react";
import copy from "copy-text-to-clipboard";
import styled from "styled-components";
import { useClickAway } from "react-use";
import { CopyIcon } from "./Icons";
const Button = styled.button.attrs({
className: "relative inline-flex items-center hover:text-primary",
})``;
const TooltipContainer = styled.div.attrs(({ $visible }) => ({
className: `absolute left-full top-1/2 z-10
bg-white rounded border border-primary/30 shadow-md
pointer-events-none transition-opacity duration-150 ease-in-out
${$visible ? "opacity-100" : "opacity-0"}`,
}))`
transform: translateY(-50%);
`;
const TooltipContent = styled.div.attrs({
className: "bg-primary-light/10 text-palette-600 py-2 px-4 ",
})``;
export const CopyButton = ({ value }) => {
const containerRef = useRef();
const [copied, setCopied] = useState(false);
const [timer, setTimer] = useState(null);
const handleCopy = useCallback(() => {
clearTimeout(timer);
copy(value);
setCopied(true);
setTimer(setTimeout(() => setCopied(false), 150000));
}, [value, timer]);
useClickAway(containerRef, () => setCopied(false));
return (
<div ref={containerRef} className="inline-flex relative overflow-visible pr-2">
<Button onClick={handleCopy} className={copied ? "text-primary" : ""}>
<CopyIcon size={16} />
</Button>
<TooltipContainer $visible={copied}>
<TooltipContent>Copied to clipboard</TooltipContent>
</TooltipContainer>
</div>
);
};

View File

@ -0,0 +1,74 @@
import * as React from "react";
import useSWR from "swr";
import { useMedia } from "react-use";
import theme from "../../lib/theme";
import { ContainerLoadingIndicator } from "../LoadingIndicator";
import { Select, SelectOption } from "../Select";
import { Switch } from "../Switch";
import { TextInputIcon } from "../TextInputIcon/TextInputIcon";
import { SearchIcon } from "../Icons";
import FileTable from "./FileTable";
import useFormattedFilesData from "./useFormattedFilesData";
const FileList = ({ type }) => {
const isMediumScreenOrLarger = useMedia(`(min-width: ${theme.screens.md})`);
const { data, error } = useSWR(`user/${type}?pageSize=10`);
const items = useFormattedFilesData(data?.items || []);
const setFilter = (name, value) => console.log("filter", name, "set to", value);
if (!items.length) {
return (
<div className="flex w-full h-full justify-center items-center text-palette-400">
{/* TODO: proper error message */}
{!data && !error && <ContainerLoadingIndicator />}
{!data && error && <p>An error occurred while loading this data.</p>}
{data && <p>No {type} found.</p>}
</div>
);
}
return (
<div className="flex flex-col gap-4 pt-4">
<div className="flex flex-col gap-4 lg:flex-row justify-between items-center">
<TextInputIcon
className="w-full lg:w-[280px] xl:w-[420px]"
placeholder="Search"
icon={<SearchIcon size={20} />}
onChange={console.log.bind(console)}
/>
<div className="flex flex-row items-center uppercase font-light text-sm gap-4">
<Switch onChange={(value) => setFilter("showSmallFiles", value)} className="mr-8">
<span className="underline decoration-dashed underline-offset-2 decoration-2 decoration-gray-300">
Show small files
</span>
</Switch>
<div>
<span className="pr-2">File type:</span>
<Select onChange={(value) => setFilter("type", value)}>
<SelectOption value="all" label="All" />
<SelectOption value="mp4" label=".mp4" />
<SelectOption value="pdf" label=".pdf" />
</Select>
</div>
<div>
<span className="pr-2">Sort:</span>
<Select onChange={(value) => setFilter("type", value)}>
<SelectOption value="size-desc" label="Biggest size" />
<SelectOption value="size-asc" label="Smallest size" />
<SelectOption value="date-desc" label="Latest" />
<SelectOption value="date-asc" label="Oldest" />
</Select>
</div>
</div>
</div>
{/* TODO: mobile view (it's not tabular) */}
{isMediumScreenOrLarger ? <FileTable items={items} /> : "Mobile view"}
</div>
);
};
export default FileList;

View File

@ -0,0 +1,111 @@
import { CogIcon, ShareIcon } from "../Icons";
import { PopoverMenu } from "../PopoverMenu/PopoverMenu";
import { Table, TableBody, TableCell, TableHead, TableHeadCell, TableRow } from "../Table";
import { CopyButton } from "../CopyButton";
const buildShareMenu = (item) => {
return [
{
label: "Facebook",
callback: () => {
console.info("share to Facebook", item);
},
},
{
label: "Twitter",
callback: () => {
console.info("share to Twitter", item);
},
},
{
label: "Discord",
callback: () => {
console.info("share to Discord", item);
},
},
];
};
const buildOptionsMenu = (item) => {
return [
{
label: "Preview",
callback: () => {
console.info("preview", item);
},
},
{
label: "Download",
callback: () => {
console.info("download", item);
},
},
{
label: "Unpin",
callback: () => {
console.info("unpin", item);
},
},
{
label: "Report",
callback: () => {
console.info("report", item);
},
},
];
};
export default function FileTable({ items }) {
return (
<Table style={{ tableLayout: "fixed" }}>
<TableHead>
<TableRow noHoverEffect>
<TableHeadCell className="w-[240px] xl:w-[360px]">Name</TableHeadCell>
<TableHeadCell className="w-[80px]">Type</TableHeadCell>
<TableHeadCell className="w-[80px]" align="right">
Size
</TableHeadCell>
<TableHeadCell className="w-[180px]">Uploaded</TableHeadCell>
<TableHeadCell className="hidden lg:table-cell">Skylink</TableHeadCell>
<TableHeadCell className="w-[100px]">Activity</TableHeadCell>
</TableRow>
</TableHead>
<TableBody>
{items.map((item) => {
const { id, name, type, size, date, skylink } = item;
return (
<TableRow key={id}>
<TableCell className="w-[240px] xl:w-[360px]">{name}</TableCell>
<TableCell className="w-[80px]">{type}</TableCell>
<TableCell className="w-[80px]" align="right">
{size}
</TableCell>
<TableCell className="w-[180px]">{date}</TableCell>
<TableCell className="hidden lg:table-cell pr-6 !overflow-visible">
<div className="flex items-center">
<CopyButton value={skylink} className="mr-2" />
<span className="w-full inline-block truncate">{skylink}</span>
</div>
</TableCell>
<TableCell className="w-[100px] !overflow-visible">
<div className="flex text-palette-600 gap-4">
<PopoverMenu options={buildShareMenu(item)} openClassName="text-primary">
<button>
<ShareIcon size={22} />
</button>
</PopoverMenu>
<PopoverMenu options={buildOptionsMenu(item)} openClassName="text-primary">
<button>
<CogIcon />
</button>
</PopoverMenu>
</div>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
);
}

View File

@ -0,0 +1 @@
export * from "./FileList";

View File

@ -0,0 +1,26 @@
import { useMemo } from "react";
import prettyBytes from "pretty-bytes";
import dayjs from "dayjs";
const parseFileName = (fileName) => {
const lastDotIndex = Math.max(0, fileName.lastIndexOf(".")) || Infinity;
return [fileName.substr(0, lastDotIndex), fileName.substr(lastDotIndex)];
};
const formatItem = ({ size, name: rawFileName, uploadedOn, downloadedOn, ...rest }) => {
const [name, type] = parseFileName(rawFileName);
const date = dayjs(uploadedOn || downloadedOn).format("MM/DD/YYYY; HH:MM");
return {
...rest,
date,
size: prettyBytes(size),
type,
name,
};
};
const useFormattedFilesData = (items) => useMemo(() => items.map(formatItem), [items]);
export default useFormattedFilesData;

View File

@ -0,0 +1,10 @@
import { withIconProps } from "../withIconProps";
export const CopyIcon = withIconProps(({ size, ...props }) => (
<svg width={size} height={size} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
fill="currentColor"
d="M26.35,11.29A5.65,5.65,0,0,1,32,16.94v9.41A5.65,5.65,0,0,1,26.35,32H16.94a5.65,5.65,0,0,1-5.65-5.65V16.94a5.65,5.65,0,0,1,5.65-5.65Zm0,3.77H16.94a1.88,1.88,0,0,0-1.88,1.88v9.41a1.89,1.89,0,0,0,1.88,1.89h9.41a1.89,1.89,0,0,0,1.89-1.89V16.94A1.89,1.89,0,0,0,26.35,15.06ZM16.22,0A4.49,4.49,0,0,1,20.7,4.18V5.79A1.89,1.89,0,0,1,17,6V4.49a.73.73,0,0,0-.58-.71l-.14,0H4.49a.74.74,0,0,0-.71.58l0,.15V16.22a.73.73,0,0,0,.58.71H5.79A1.88,1.88,0,0,1,6,20.69l-.22,0H4.49A4.49,4.49,0,0,1,0,16.53v-12A4.48,4.48,0,0,1,4.18,0h12Z"
/>
</svg>
));

View File

@ -0,0 +1,10 @@
import { withIconProps } from "../withIconProps";
export const SearchIcon = withIconProps(({ size, ...props }) => (
<svg width={size} height={size} viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
fill="currentColor"
d="M9,0a9,9,0,0,1,7,14.62l3.68,3.67a1,1,0,0,1-1.32,1.5l-.1-.08L14.62,16A9,9,0,1,1,9,0ZM9,2a7,7,0,1,0,4.87,12l.07-.09.09-.07A7,7,0,0,0,9,2Z"
/>
</svg>
));

View File

@ -0,0 +1,18 @@
import { withIconProps } from "../withIconProps";
export const ShareIcon = withIconProps(({ size, ...props }) => (
<svg
width={size}
height={size}
viewBox="0 0 29.09 32"
xmlns="http://www.w3.org/2000/svg"
shapeRendering="geometricPrecision"
{...props}
>
<path
fill="currentColor"
d="M24.73,0a5.82,5.82,0,1,1-4.14,9.91l-7.72,4.51a5.85,5.85,0,0,1,0,3.16l7.73,4.5a5.81,5.81,0,1,1-1.47,2.51l-7.72-4.5a5.82,5.82,0,1,1,0-8.22l0,0L19.13,7.4a5.82,5.82,0,0,1,4-7.18A5.69,5.69,0,0,1,24.73,0Zm0,23.27a2.93,2.93,0,0,0-2.43,1.3,1,1,0,0,1-.07.15l-.09.14-.05.09A2.91,2.91,0,1,0,26,23.54,2.86,2.86,0,0,0,24.73,23.27ZM7.27,13.09a2.91,2.91,0,1,0,2.51,4.37l0-.05A2.93,2.93,0,0,0,10.18,16a2.89,2.89,0,0,0-.4-1.46v0A2.9,2.9,0,0,0,7.27,13.09ZM24.73,2.91a2.92,2.92,0,0,0-2.55,4.32l0,0v0a2.91,2.91,0,1,0,2.5-4.4Z"
transform="translate(-1.46 0)"
/>
</svg>
));

View File

@ -9,3 +9,6 @@ export * from "./icons/CircledErrorIcon";
export * from "./icons/CircledProgressIcon"; export * from "./icons/CircledProgressIcon";
export * from "./icons/CircledArrowUpIcon"; export * from "./icons/CircledArrowUpIcon";
export * from "./icons/PlusIcon"; export * from "./icons/PlusIcon";
export * from "./icons/SearchIcon";
export * from "./icons/CopyIcon";
export * from "./icons/ShareIcon";

View File

@ -4,7 +4,7 @@ const propTypes = {
/** /**
* Size of the icon's bounding box. * Size of the icon's bounding box.
*/ */
size: PropTypes.number, size: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
}; };
const defaultProps = { const defaultProps = {

View File

@ -0,0 +1,90 @@
import { Children, cloneElement, useRef, useState } from "react";
import PropTypes from "prop-types";
import { useClickAway } from "react-use";
import styled, { css, keyframes } from "styled-components";
const dropDown = keyframes`
0% {
transform: scaleY(0);
}
80% {
transform: scaleY(1.1);
}
100% {
transform: scaleY(1);
}
`;
const Container = styled.div.attrs({ className: "relative inline-flex" })``;
const Flyout = styled.ul.attrs({
className: `absolute right-0 z-10 py-2
border rounded bg-white
overflow-hidden pointer-events-none
shadow-md shadow-palette-200/50
pointer-events-auto h-auto overflow-visible border-primary`,
})`
top: calc(100% + 2px);
animation: ${css`
${dropDown} 0.1s ease-in-out
`};
`;
const Option = styled.li.attrs({
className: `font-sans text-xs uppercase
relative pl-3 pr-5 py-1
text-palette-600 cursor-pointer
hover:text-primary hover:font-normal
active:text-primary active:font-normal
before:content-[initial] before:absolute before:left-0 before:h-3 before:w-0.5 before:bg-primary before:top-1.5
hover:before:content-['']`,
})``;
export const PopoverMenu = ({ options, children, openClassName, ...props }) => {
const containerRef = useRef();
const [open, setOpen] = useState(false);
useClickAway(containerRef, () => setOpen(false));
const handleChoice = (callback) => () => {
setOpen(false);
callback();
};
return (
<Container ref={containerRef} {...props}>
{Children.only(
cloneElement(children, {
onClick: () => setOpen((open) => !open),
className: `${children.props.className ?? ""} ${open ? openClassName : ""}`,
})
)}
{open && (
<Flyout>
{options.map(({ label, callback }) => (
<Option key={label} onClick={handleChoice(callback)}>
{label}
</Option>
))}
</Flyout>
)}
</Container>
);
};
PopoverMenu.propTypes = {
/**
* Accepts a single child node that will become a menu toggle.
*/
children: PropTypes.element.isRequired,
/**
* Positions in the menu
*/
options: PropTypes.arrayOf(
PropTypes.shape({
label: PropTypes.string.isRequired,
callback: PropTypes.func.isRequired,
})
).isRequired,
};

View File

@ -21,9 +21,11 @@ const dropDown = keyframes`
const Container = styled.div.attrs({ className: "relative inline-flex" })``; const Container = styled.div.attrs({ className: "relative inline-flex" })``;
const Trigger = styled.button.attrs(({ placeholder }) => ({ const Trigger = styled.button.attrs(({ $placeholder }) => ({
className: `flex items-center cursor-pointer ${placeholder ? "text-palette-300" : ""}`, className: `flex items-center cursor-pointer font-bold ${$placeholder ? "text-palette-300" : ""}`,
}))``; }))`
text-transform: inherit;
`;
const TriggerIcon = styled(ChevronDownIcon).attrs({ const TriggerIcon = styled(ChevronDownIcon).attrs({
className: "transition-transform text-primary", className: "transition-transform text-primary",
@ -32,13 +34,14 @@ const TriggerIcon = styled(ChevronDownIcon).attrs({
`; `;
const Flyout = styled.ul.attrs(({ open }) => ({ const Flyout = styled.ul.attrs(({ open }) => ({
className: `absolute top-[20px] right-0 className: `absolute right-0 z-10
p-0 h-0 border rounded bg-white p-0 border rounded bg-white
overflow-hidden pointer-events-none overflow-hidden pointer-events-none
shadow-md shadow-palette-200/50 shadow-md shadow-palette-200/50
${open ? "pointer-events-auto h-auto overflow-visible border-primary" : ""} ${open ? "pointer-events-auto h-auto overflow-visible border-primary" : ""}
${open ? "visible" : "invisible"}`, ${open ? "visible" : "invisible"}`,
}))` }))`
top: calc(100% + 2px);
animation: ${({ open }) => animation: ${({ open }) =>
open open
? css` ? css`
@ -47,7 +50,7 @@ const Flyout = styled.ul.attrs(({ open }) => ({
: "none"}; : "none"};
`; `;
export const Select = ({ defaultValue, children, onChange, placeholder }) => { export const Select = ({ defaultValue, children, onChange, placeholder, ...props }) => {
const selectRef = useRef(); const selectRef = useRef();
const options = useMemo(() => Children.toArray(children).filter(({ type }) => type === SelectOption), [children]); const options = useMemo(() => Children.toArray(children).filter(({ type }) => type === SelectOption), [children]);
const [state, dispatch] = useSelectReducer({ defaultValue, placeholder, options }); const [state, dispatch] = useSelectReducer({ defaultValue, placeholder, options });
@ -65,8 +68,8 @@ export const Select = ({ defaultValue, children, onChange, placeholder }) => {
const activeLabel = activeOption?.props?.label ?? null; const activeLabel = activeOption?.props?.label ?? null;
return ( return (
<Container ref={selectRef}> <Container ref={selectRef} {...props}>
<Trigger placeholder={!activeLabel && placeholder} onClick={toggle}> <Trigger $placeholder={!activeLabel && placeholder} onClick={toggle} className={state.open ? "text-primary" : ""}>
{activeLabel ?? placeholder} <TriggerIcon open={state.open} /> {activeLabel ?? placeholder} <TriggerIcon open={state.open} />
</Trigger> </Trigger>
<Flyout role="listbox" open={state.open}> <Flyout role="listbox" open={state.open}>

View File

@ -1,40 +0,0 @@
.react-switch-checkbox {
height: 0;
width: 0;
visibility: hidden;
}
.react-switch-label {
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
width: 44px;
height: 22px;
background: white;
border-radius: 11px;
@apply border-palette-200;
border-width: 1px;
position: relative;
transition: background-color 0.2s;
}
.react-switch-label .react-switch-button {
content: "";
position: absolute;
top: 2px;
left: 2px;
width: 16px;
height: 16px;
border-radius: 8px;
transition: 0.2s;
}
.react-switch-checkbox:checked + .react-switch-label .react-switch-button {
left: calc(100% - 2px);
transform: translateX(-100%);
}
.react-switch-label:active .react-switch-button {
width: 20px;
}

View File

@ -1,37 +1,86 @@
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import "./Switch.css"; import { useEffect, useMemo, useState } from "react";
import styled from "styled-components";
import { nanoid } from "nanoid";
const Container = styled.div.attrs({
className: "inline-flex items-center gap-1 cursor-pointer select-none",
})``;
const Checkbox = styled.input.attrs({
type: "checkbox",
className: `h-0 w-0 hidden`,
})``;
const Label = styled.label.attrs({
className: "cursor-pointer inline-flex items-center gap-2",
})`
&:active .toggle-pin {
width: 20px;
}
`;
const Toggle = styled.span.attrs({
className: `flex flex-row items-center justify-between
w-[44px] h-[22px] bg-white rounded-full
border border-palette-200 relative cursor-pointer`,
})`
&:active .toggle-pin {
width: 20px;
}
`;
const TogglePin = styled.span.attrs(({ $checked }) => ({
className: `toggle-pin
absolute top-[2px] w-4 h-4 rounded-full
transition-[width_left] active:w-5
${$checked ? "checked bg-primary" : "bg-palette-200"}`,
}))`
left: 2px;
&.checked {
left: calc(100% - 2px);
transform: translateX(-100%);
}
`;
export const Switch = ({ children, defaultChecked, onChange, ...props }) => {
const id = useMemo(nanoid, [onChange]);
const [checked, setChecked] = useState(defaultChecked);
useEffect(() => {
onChange(checked);
}, [checked, onChange]);
/**
* Primary UI component for user interaction
*/
export const Switch = ({ isOn, handleToggle }) => {
return ( return (
<> <Container {...props}>
<input <Checkbox checked={checked} onChange={(ev) => setChecked(ev.target.checked)} id={id} />
checked={isOn} <Label htmlFor={id}>
onChange={handleToggle} <Toggle>
className="react-switch-checkbox" <TogglePin $checked={checked} />
id={`react-switch-new`} </Toggle>
type="checkbox" {children}
/> </Label>
<label className={"react-switch-label"} htmlFor={`react-switch-new`}> </Container>
<span className={`react-switch-button ${isOn ? "bg-primary" : "bg-palette-200"}`} />
</label>
</>
); );
}; };
Switch.propTypes = { Switch.propTypes = {
/** /**
* Switch's current value * Should the checkbox be checked by default?
*/ */
isOn: PropTypes.bool, defaultChecked: PropTypes.bool,
/**
* Element to be rendered as the switch label
*/
children: PropTypes.element,
/** /**
* Function to execute on change * Function to execute on change
*/ */
handleToggle: PropTypes.func, onChange: PropTypes.func.isRequired,
}; };
Switch.defaultProps = { Switch.defaultProps = {
isOn: false, defaultChecked: false,
}; };

View File

@ -13,10 +13,10 @@ const Template = (args) => <Switch {...args} />;
export const SwitchTrue = Template.bind({}); export const SwitchTrue = Template.bind({});
// More on args: https://storybook.js.org/docs/react/writing-stories/args // More on args: https://storybook.js.org/docs/react/writing-stories/args
SwitchTrue.args = { SwitchTrue.args = {
isOn: true, defaultChecked: true,
}; };
export const SwitchFalse = Template.bind({}); export const SwitchFalse = Template.bind({});
// More on args: https://storybook.js.org/docs/react/writing-stories/args // More on args: https://storybook.js.org/docs/react/writing-stories/args
SwitchFalse.args = { SwitchFalse.args = {
isOn: false, defaultChecked: false,
}; };

View File

@ -1,7 +1,7 @@
import styled from "styled-components"; import styled from "styled-components";
const Container = styled.div.attrs({ const Container = styled.div.attrs({
className: "p-1 max-w-full overflow-x-auto", className: "p-1 max-w-full",
})``; })``;
const StyledTable = styled.table.attrs({ const StyledTable = styled.table.attrs({

View File

@ -4,7 +4,8 @@ import styled from "styled-components";
* Accepts all HMTL attributes a `<th>` element does. * Accepts all HMTL attributes a `<th>` element does.
*/ */
export const TableHeadCell = styled.th.attrs({ export const TableHeadCell = styled.th.attrs({
className: `px-6 py-2.5 truncate h-tableRow className: `first:pl-6 last:pr-6 px-2 py-4
truncate h-tableRow
text-palette-600 font-sans font-light text-xs text-palette-600 font-sans font-light text-xs
first:rounded-l-sm last:rounded-r-sm`, first:rounded-l-sm last:rounded-r-sm`,
})` })`

View File

@ -34,7 +34,11 @@ const Body = styled.div.attrs({ className: "grow min-h-0" })``;
export const Tabs = ({ defaultTab, children, variant }) => { export const Tabs = ({ defaultTab, children, variant }) => {
const getTabId = usePrefixedTabIds(); const getTabId = usePrefixedTabIds();
const { tabs, panels, tabsRefs } = useTabsChildren(children, getTabId); const { tabs, panels, tabsRefs } = useTabsChildren(children, getTabId);
const defaultTabId = useMemo(() => getTabId(defaultTab || tabs[0].props.id), [getTabId, defaultTab, tabs]); const defaultTabId = useMemo(() => {
const requestedTabIsPresent = tabs.find(({ props }) => props.id === defaultTab);
return getTabId(requestedTabIsPresent ? defaultTab : tabs[0].props.id);
}, [getTabId, defaultTab, tabs]);
const [activeTabId, setActiveTabId] = useState(defaultTabId); const [activeTabId, setActiveTabId] = useState(defaultTabId);
const [activeTabRef, setActiveTabRef] = useState(tabsRefs[activeTabId]); const [activeTabRef, setActiveTabRef] = useState(tabsRefs[activeTabId]);
const isActive = (id) => id === activeTabId; const isActive = (id) => id === activeTabId;

View File

@ -1,20 +1,45 @@
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import cn from "classnames";
import { useEffect, useRef, useState } from "react";
import { PlusIcon } from "../Icons";
export const TextInputIcon = ({ className, icon, placeholder, onChange }) => {
const inputRef = useRef();
const [focused, setFocused] = useState(false);
const [value, setValue] = useState("");
useEffect(() => {
onChange(value);
}, [value, onChange]);
/**
* Primary UI component for user interaction
*/
export const TextInputIcon = ({ icon, position, placeholder }) => {
return ( return (
<div className={"flex flex-row items-center px-textInputIcon h-textInput rounded-full bg-palette-100"}> <div
{position === "left" ? <div className={"w-buttonIconLg h-buttonIconLg"}>{icon}</div> : null} className={cn(
<input "grid-flow-col inline-grid grid-cols-[2rem_1fr_1.5rem] items-center rounded-full bg-palette-100 px-4 py-2",
placeholder={placeholder} className,
className={ {
"w-full focus:outline-none mx-textInputHorizontal rounded-full bg-transparent " + "outline outline-1 outline-primary": focused,
"placeholder-palette-400 text-content tracking-inputPlaceholder text-textInput"
} }
)}
>
<div>{icon}</div>
<input
ref={inputRef}
value={value}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
onChange={(event) => setValue(event.target.value)}
placeholder={placeholder}
className="focus:outline-none bg-transparent placeholder:text-palette-400"
/> />
{position === "right" ? <div className={"w-buttonIconLg h-buttonIconLg"}>{icon}</div> : null} {value && (
<PlusIcon
size={14}
role="button"
className="justify-self-end text-palette-400 rotate-45"
onClick={() => setValue("")}
/>
)}
</div> </div>
); );
}; };
@ -23,13 +48,13 @@ TextInputIcon.propTypes = {
/** /**
* Icon to place in text input * Icon to place in text input
*/ */
icon: PropTypes.element, icon: PropTypes.element.isRequired,
/**
* Side to place icon
*/
position: PropTypes.oneOf(["left", "right"]),
/** /**
* Input placeholder * Input placeholder
*/ */
placeholder: PropTypes.string, placeholder: PropTypes.string,
/**
* Function to be called whenever value changes
*/
onChange: PropTypes.func.isRequired,
}; };

View File

@ -1,5 +1,6 @@
import { TextInputIcon } from "./TextInputIcon"; import { TextInputIcon } from "./TextInputIcon";
import { CogIcon } from "../Icons"; import { SearchIcon } from "../Icons";
import { Panel } from "../Panel";
// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export // More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
export default { export default {
@ -9,19 +10,21 @@ export default {
}; };
// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args // More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
const Template = (args) => <TextInputIcon {...args} />; const Template = (args) => (
<Panel>
<TextInputIcon {...args} />
</Panel>
);
export const IconLeft = Template.bind({}); export const IconLeft = Template.bind({});
// More on args: https://storybook.js.org/docs/react/writing-stories/args // More on args: https://storybook.js.org/docs/react/writing-stories/args
IconLeft.args = { IconLeft.args = {
icon: <CogIcon />, icon: <SearchIcon size={20} />,
position: "left",
placeholder: "Search", placeholder: "Search",
}; };
export const IconRight = Template.bind({}); export const IconRight = Template.bind({});
IconRight.args = { IconRight.args = {
icon: <CogIcon />, icon: <SearchIcon size={20} />,
position: "right",
placeholder: "Search", placeholder: "Search",
}; };

View File

@ -2,8 +2,28 @@ import * as React from "react";
import DashboardLayout from "../layouts/DashboardLayout"; import DashboardLayout from "../layouts/DashboardLayout";
import { Panel } from "../components/Panel";
import { Tab, TabPanel, Tabs } from "../components/Tabs";
import FileList from "../components/FileList/FileList";
import { useSearchParam } from "react-use";
const FilesPage = () => { const FilesPage = () => {
return <>FILES</>; const defaultTab = useSearchParam("tab");
return (
<Panel title="Files">
<Tabs defaultTab={defaultTab || "uploads"}>
<Tab id="uploads" title="Uploads" />
<Tab id="downloads" title="Downloads" />
<TabPanel tabId="uploads" className="pt-4">
<FileList type="uploads" />
</TabPanel>
<TabPanel tabId="downloads" className="pt-4">
<FileList type="downloads" />
</TabPanel>
</Tabs>
</Panel>
);
}; };
FilesPage.Layout = DashboardLayout; FilesPage.Layout = DashboardLayout;

View File

@ -28,6 +28,7 @@ module.exports = {
borderColor: (theme) => ({ ...theme("colors"), ...colors }), borderColor: (theme) => ({ ...theme("colors"), ...colors }),
textColor: (theme) => ({ ...theme("colors"), ...colors }), textColor: (theme) => ({ ...theme("colors"), ...colors }),
placeholderColor: (theme) => ({ ...theme("colors"), ...colors }), placeholderColor: (theme) => ({ ...theme("colors"), ...colors }),
outlineColor: (theme) => ({ ...theme("colors"), ...colors }),
extend: { extend: {
fontFamily: { fontFamily: {
sans: ["Sora", ...defaultTheme.fontFamily.sans], sans: ["Sora", ...defaultTheme.fontFamily.sans],