mirror of
https://github.com/roleypoly/roleypoly.git
synced 2025-04-25 03:49:11 +00:00
feat(editor): add basis of role editor pane
This commit is contained in:
parent
c8adad6c81
commit
5fcac53be2
34 changed files with 524 additions and 8 deletions
|
@ -26,6 +26,7 @@
|
||||||
"@improbable-eng/grpc-web": "^0.13.0",
|
"@improbable-eng/grpc-web": "^0.13.0",
|
||||||
"browser-headers": "^0.4.1",
|
"browser-headers": "^0.4.1",
|
||||||
"chroma-js": "^2.1.0",
|
"chroma-js": "^2.1.0",
|
||||||
|
"fuse.js": "^6.4.2",
|
||||||
"google-protobuf": "^3.13.0",
|
"google-protobuf": "^3.13.0",
|
||||||
"next": "^9.5.5",
|
"next": "^9.5.5",
|
||||||
"react": "^17.0.1",
|
"react": "^17.0.1",
|
||||||
|
@ -90,4 +91,4 @@
|
||||||
"tsconfig-paths-webpack-plugin": "^3.3.0",
|
"tsconfig-paths-webpack-plugin": "^3.3.0",
|
||||||
"typescript": "^4.0.3"
|
"typescript": "^4.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
1
src/common/utils/featureFlags/go/featureflags.go
Normal file
1
src/common/utils/featureFlags/go/featureflags.go
Normal file
|
@ -0,0 +1 @@
|
||||||
|
package featureflags
|
7
src/common/utils/featureFlags/react/BUILD.bazel
Normal file
7
src/common/utils/featureFlags/react/BUILD.bazel
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
load("//hack/bazel/js:react.bzl", "react_library")
|
||||||
|
|
||||||
|
package(default_visibility = ["//visibility:public"])
|
||||||
|
|
||||||
|
react_library(
|
||||||
|
name = "react",
|
||||||
|
)
|
19
src/common/utils/featureFlags/react/FeatureFlags.tsx
Normal file
19
src/common/utils/featureFlags/react/FeatureFlags.tsx
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
export enum FeatureFlag {
|
||||||
|
AllowListsBlockLists = 'AllowListsBlockLists',
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FeatureFlagProvider {
|
||||||
|
activeFlags: FeatureFlag[] = [];
|
||||||
|
|
||||||
|
constructor(flags: FeatureFlag[] = []) {
|
||||||
|
this.activeFlags = flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
has(flag: FeatureFlag) {
|
||||||
|
return this.activeFlags.includes(flag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FeatureFlagsContext = React.createContext(new FeatureFlagProvider());
|
1
src/common/utils/featureFlags/react/index.ts
Normal file
1
src/common/utils/featureFlags/react/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './FeatureFlags';
|
12
src/common/utils/featureFlags/react/storyDecorator.tsx
Normal file
12
src/common/utils/featureFlags/react/storyDecorator.tsx
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { FeatureFlag, FeatureFlagProvider, FeatureFlagsContext } from './FeatureFlags';
|
||||||
|
|
||||||
|
export const FeatureFlagDecorator = (flags: FeatureFlag[]) => (
|
||||||
|
storyFn: () => React.ReactNode
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
<FeatureFlagsContext.Provider value={new FeatureFlagProvider(flags)}>
|
||||||
|
{storyFn()}
|
||||||
|
</FeatureFlagsContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
|
@ -4,6 +4,4 @@ package(default_visibility = ["//visibility:public"])
|
||||||
|
|
||||||
react_library(
|
react_library(
|
||||||
name = "withContext",
|
name = "withContext",
|
||||||
deps = [
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
|
|
10
src/design-system/atoms/feature-gate/BUILD.bazel
Normal file
10
src/design-system/atoms/feature-gate/BUILD.bazel
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
load("//hack/bazel/js:react.bzl", "react_library")
|
||||||
|
|
||||||
|
package(default_visibility = ["//visibility:public"])
|
||||||
|
|
||||||
|
react_library(
|
||||||
|
name = "feature-gate",
|
||||||
|
deps = [
|
||||||
|
"//src/common/utils/featureFlags/react",
|
||||||
|
],
|
||||||
|
)
|
16
src/design-system/atoms/feature-gate/FeatureGate.stories.tsx
Normal file
16
src/design-system/atoms/feature-gate/FeatureGate.stories.tsx
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { FeatureFlagDecorator } from 'roleypoly/src/common/utils/featureFlags/react/storyDecorator';
|
||||||
|
import { FeatureGate } from './FeatureGate';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Atoms/Feature Gate',
|
||||||
|
decorators: [FeatureFlagDecorator(['AllowListBlockList'])],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ActiveGate = () => (
|
||||||
|
<FeatureGate featureFlag="AllowListBlockList">{() => <div>hello!</div>}</FeatureGate>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const InactiveGate = () => (
|
||||||
|
<FeatureGate featureFlag="aaa">{() => <div>hello!</div>}</FeatureGate>
|
||||||
|
);
|
20
src/design-system/atoms/feature-gate/FeatureGate.tsx
Normal file
20
src/design-system/atoms/feature-gate/FeatureGate.tsx
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import {
|
||||||
|
FeatureFlag,
|
||||||
|
FeatureFlagsContext,
|
||||||
|
} from 'roleypoly/src/common/utils/featureFlags/react';
|
||||||
|
|
||||||
|
export type FeatureGateProps = {
|
||||||
|
featureFlag: FeatureFlag;
|
||||||
|
children: () => React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FeatureGate = (props: FeatureGateProps) => {
|
||||||
|
const featureContext = React.useContext(FeatureFlagsContext);
|
||||||
|
|
||||||
|
if (featureContext.has(props.featureFlag)) {
|
||||||
|
return props.children();
|
||||||
|
} else {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
};
|
1
src/design-system/atoms/feature-gate/index.ts
Normal file
1
src/design-system/atoms/feature-gate/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './FeatureGate';
|
11
src/design-system/atoms/horizontal-switch/BUILD.bazel
Normal file
11
src/design-system/atoms/horizontal-switch/BUILD.bazel
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
load("//hack/bazel/js:react.bzl", "react_library")
|
||||||
|
|
||||||
|
package(default_visibility = ["//visibility:public"])
|
||||||
|
|
||||||
|
react_library(
|
||||||
|
name = "horizontal-switch",
|
||||||
|
deps = [
|
||||||
|
"//src/design-system/atoms/colors",
|
||||||
|
"//src/design-system/atoms/timings",
|
||||||
|
],
|
||||||
|
)
|
|
@ -0,0 +1,33 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { HorizontalSwitch } from './HorizontalSwitch';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Atoms/Horizontal Switch',
|
||||||
|
component: HorizontalSwitch,
|
||||||
|
args: {
|
||||||
|
items: ['true', 'false'],
|
||||||
|
value: 'true',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const Story = (args) => {
|
||||||
|
const [value, setValue] = React.useState(args.value);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HorizontalSwitch
|
||||||
|
{...args}
|
||||||
|
value={value}
|
||||||
|
onChange={(a) => {
|
||||||
|
setValue(a);
|
||||||
|
args.onChange(a);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Switch = Story.bind({});
|
||||||
|
export const SwitchThree = Story.bind({});
|
||||||
|
SwitchThree.args = {
|
||||||
|
items: ['aaa', 'bbb', 'ccc'],
|
||||||
|
value: 'aaa',
|
||||||
|
};
|
|
@ -0,0 +1,25 @@
|
||||||
|
import styled, { css } from 'styled-components';
|
||||||
|
import * as _ from 'styled-components'; // eslint-disable-line no-duplicate-imports
|
||||||
|
import { palette } from 'roleypoly/src/design-system/atoms/colors';
|
||||||
|
import { transitions } from 'roleypoly/src/design-system/atoms/timings';
|
||||||
|
|
||||||
|
export const Item = styled.div<{ selected: boolean }>`
|
||||||
|
padding: 10px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: background-color ease-in-out ${transitions.actionable}s;
|
||||||
|
|
||||||
|
${(props) =>
|
||||||
|
props.selected &&
|
||||||
|
css`
|
||||||
|
background-color: ${palette.taupe300};
|
||||||
|
`}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const Wrapper = styled.div`
|
||||||
|
display: inline-flex;
|
||||||
|
user-select: none;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid ${palette.taupe200};
|
||||||
|
border-radius: calc(1em + 20px);
|
||||||
|
overflow: hidden;
|
||||||
|
`;
|
|
@ -0,0 +1,28 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { Item, Wrapper } from './HorizontalSwitch.styled';
|
||||||
|
|
||||||
|
export type SwitchProps = {
|
||||||
|
items: string[];
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const HorizontalSwitch = (props: SwitchProps) => {
|
||||||
|
const handleClick = (item: typeof props.value) => () => {
|
||||||
|
props.onChange?.(item);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Wrapper>
|
||||||
|
{props.items.map((item, idx) => (
|
||||||
|
<Item
|
||||||
|
key={idx}
|
||||||
|
selected={item === props.value}
|
||||||
|
onClick={handleClick(item)}
|
||||||
|
>
|
||||||
|
{item}
|
||||||
|
</Item>
|
||||||
|
))}
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
};
|
1
src/design-system/atoms/horizontal-switch/index.tsx
Normal file
1
src/design-system/atoms/horizontal-switch/index.tsx
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './HorizontalSwitch';
|
|
@ -10,7 +10,7 @@ import { globalOnKeyUp } from 'roleypoly/src/design-system/atoms/key-events';
|
||||||
import { IoMdClose } from 'react-icons/io';
|
import { IoMdClose } from 'react-icons/io';
|
||||||
|
|
||||||
type PopoverProps = {
|
type PopoverProps = {
|
||||||
children: React.ReactNode;
|
children: () => React.ReactNode;
|
||||||
position: 'top left' | 'top right' | 'bottom left' | 'bottom right';
|
position: 'top left' | 'top right' | 'bottom left' | 'bottom right';
|
||||||
active: boolean;
|
active: boolean;
|
||||||
canDefocus?: boolean;
|
canDefocus?: boolean;
|
||||||
|
@ -29,7 +29,7 @@ export const Popover = (props: PopoverProps) => {
|
||||||
</PopoverHeadCloser>
|
</PopoverHeadCloser>
|
||||||
<div>{props.headContent}</div>
|
<div>{props.headContent}</div>
|
||||||
</PopoverHead>
|
</PopoverHead>
|
||||||
<PopoverContent>{props.children}</PopoverContent>
|
<PopoverContent>{props.children()}</PopoverContent>
|
||||||
</PopoverBase>
|
</PopoverBase>
|
||||||
{props.canDefocus && (
|
{props.canDefocus && (
|
||||||
<DefocusHandler
|
<DefocusHandler
|
||||||
|
|
|
@ -7,6 +7,7 @@ export type StyledProps = {
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
defaultColor: boolean;
|
defaultColor: boolean;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
|
type?: 'delete';
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Outer = styled.div<StyledProps>`
|
export const Outer = styled.div<StyledProps>`
|
||||||
|
@ -29,6 +30,9 @@ export const Outer = styled.div<StyledProps>`
|
||||||
&:hover {
|
&:hover {
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
|
||||||
|
${Circle} svg {
|
||||||
|
fill-opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
|
@ -69,6 +73,13 @@ export const Circle = styled.div<StyledProps>`
|
||||||
? 'var(--role-color)'
|
? 'var(--role-color)'
|
||||||
: 'var(--role-contrast)'};
|
: 'var(--role-contrast)'};
|
||||||
}
|
}
|
||||||
|
${(props) =>
|
||||||
|
props.type === 'delete' &&
|
||||||
|
css`
|
||||||
|
svg {
|
||||||
|
fill-opacity: 0;
|
||||||
|
}
|
||||||
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Text = styled.div`
|
export const Text = styled.div`
|
||||||
|
|
|
@ -11,6 +11,7 @@ type Props = {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
onClick?: (newState: boolean) => void;
|
onClick?: (newState: boolean) => void;
|
||||||
tooltipId?: string;
|
tooltipId?: string;
|
||||||
|
type?: 'delete';
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Role = (props: Props) => {
|
export const Role = (props: Props) => {
|
||||||
|
@ -36,6 +37,7 @@ export const Role = (props: Props) => {
|
||||||
selected: props.selected,
|
selected: props.selected,
|
||||||
defaultColor: props.role.color === 0,
|
defaultColor: props.role.color === 0,
|
||||||
disabled: !!props.disabled,
|
disabled: !!props.disabled,
|
||||||
|
type: props.type,
|
||||||
};
|
};
|
||||||
|
|
||||||
const extra = !props.disabled
|
const extra = !props.disabled
|
||||||
|
@ -53,7 +55,7 @@ export const Role = (props: Props) => {
|
||||||
{...extra}
|
{...extra}
|
||||||
>
|
>
|
||||||
<styled.Circle {...styledProps}>
|
<styled.Circle {...styledProps}>
|
||||||
{!props.disabled ? <FaCheck /> : <FaTimes />}
|
{!props.disabled && props.type !== 'delete' ? <FaCheck /> : <FaTimes />}
|
||||||
</styled.Circle>
|
</styled.Circle>
|
||||||
<styled.Text>{props.role.name}</styled.Text>
|
<styled.Text>{props.role.name}</styled.Text>
|
||||||
</styled.Outer>
|
</styled.Outer>
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { EditorCategory } from './EditorCategory';
|
||||||
|
import {
|
||||||
|
mockCategory,
|
||||||
|
roleCategory2,
|
||||||
|
roleCategory,
|
||||||
|
guildRoles,
|
||||||
|
} from 'roleypoly/src/design-system/shared-types/storyData';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Molecules/Editor/Category',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CategoryEditor = () => {
|
||||||
|
const [categoryData, setCategoryData] = React.useState(mockCategory);
|
||||||
|
return (
|
||||||
|
<EditorCategory
|
||||||
|
category={categoryData}
|
||||||
|
onChange={(category) => setCategoryData(category)}
|
||||||
|
uncategorizedRoles={roleCategory}
|
||||||
|
guildRoles={[...roleCategory, ...roleCategory2]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,7 @@
|
||||||
|
import * as _ from 'styled-components'; // eslint-disable-line no-duplicate-imports
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
export const RoleContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
margin: 10px;
|
||||||
|
`;
|
150
src/design-system/molecules/editor-category/EditorCategory.tsx
Normal file
150
src/design-system/molecules/editor-category/EditorCategory.tsx
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { HorizontalSwitch } from 'roleypoly/src/design-system/atoms/horizontal-switch';
|
||||||
|
import { Space } from 'roleypoly/src/design-system/atoms/space';
|
||||||
|
import {
|
||||||
|
TextInput,
|
||||||
|
TextInputWithIcon,
|
||||||
|
} from 'roleypoly/src/design-system/atoms/text-input';
|
||||||
|
import { Text } from 'roleypoly/src/design-system/atoms/typography';
|
||||||
|
import { Popover } from 'roleypoly/src/design-system/atoms/popover';
|
||||||
|
import { FaderOpacity } from 'roleypoly/src/design-system/atoms/fader';
|
||||||
|
import { RoleSearch } from 'roleypoly/src/design-system/molecules/role-search';
|
||||||
|
import {
|
||||||
|
Category,
|
||||||
|
CategoryType,
|
||||||
|
Role as RoleType,
|
||||||
|
} from 'roleypoly/src/design-system/shared-types';
|
||||||
|
import { Role } from 'roleypoly/src/design-system/atoms/role';
|
||||||
|
import { GoSearch } from 'react-icons/go';
|
||||||
|
import { RoleContainer } from './EditorCategory.styled';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
category: Category;
|
||||||
|
uncategorizedRoles: RoleType[];
|
||||||
|
guildRoles: RoleType[];
|
||||||
|
onChange: (category: Category) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const typeEnumToSwitch = (typeData: CategoryType) => {
|
||||||
|
if (typeData === CategoryType.SINGLE) {
|
||||||
|
return 'Single';
|
||||||
|
} else {
|
||||||
|
return 'Multiple';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const switchToTypeEnum = (typeData: 'Single' | 'Multiple') => {
|
||||||
|
if (typeData === 'Single') {
|
||||||
|
return CategoryType.SINGLE;
|
||||||
|
} else {
|
||||||
|
return CategoryType.MULTI;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EditorCategory = (props: Props) => {
|
||||||
|
const [roleSearchPopoverActive, setRoleSearchPopoverActive] = React.useState(false);
|
||||||
|
const [roleSearchTerm, updateSearchTerm] = React.useState('');
|
||||||
|
|
||||||
|
const onUpdate = (
|
||||||
|
key: keyof typeof props.category,
|
||||||
|
pred?: (newValue: any) => any
|
||||||
|
) => (newValue: any) => {
|
||||||
|
props.onChange({
|
||||||
|
...props.category,
|
||||||
|
[key]: pred ? pred(newValue) : newValue,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRoleSelect = (role: RoleType) => {
|
||||||
|
setRoleSearchPopoverActive(false);
|
||||||
|
updateSearchTerm('');
|
||||||
|
props.onChange({
|
||||||
|
...props.category,
|
||||||
|
rolesList: [...props.category.rolesList, role.id],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRoleDeselect = (role: RoleType) => () => {
|
||||||
|
props.onChange({
|
||||||
|
...props.category,
|
||||||
|
rolesList: props.category.rolesList.filter((x) => x !== role.id),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Text>Category Name</Text>
|
||||||
|
<TextInput
|
||||||
|
placeholder="Pronouns, Political, Colors..."
|
||||||
|
value={props.category.name}
|
||||||
|
onChange={onUpdate('name', (x) => x.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Space />
|
||||||
|
|
||||||
|
<Text>Selection Type</Text>
|
||||||
|
<div>
|
||||||
|
<HorizontalSwitch
|
||||||
|
items={['Multiple', 'Single']}
|
||||||
|
value={typeEnumToSwitch(props.category.type)}
|
||||||
|
onChange={onUpdate('type', switchToTypeEnum)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Space />
|
||||||
|
|
||||||
|
<Text>Visiblity</Text>
|
||||||
|
<div>
|
||||||
|
<HorizontalSwitch
|
||||||
|
items={['Visible', 'Hidden']}
|
||||||
|
value={props.category.hidden ? 'Hidden' : 'Visible'}
|
||||||
|
onChange={onUpdate('hidden', (a) => a === 'Hidden')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Space />
|
||||||
|
<Text>Roles</Text>
|
||||||
|
<Popover
|
||||||
|
active={roleSearchPopoverActive}
|
||||||
|
onExit={() => setRoleSearchPopoverActive(false)}
|
||||||
|
>
|
||||||
|
{() => (
|
||||||
|
<RoleSearch
|
||||||
|
placeholder={'Type or drag a role...'}
|
||||||
|
roles={props.uncategorizedRoles}
|
||||||
|
onSelect={handleRoleSelect}
|
||||||
|
searchTerm={roleSearchTerm}
|
||||||
|
onSearchUpdate={(newTerm) => updateSearchTerm(newTerm)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Popover>
|
||||||
|
<FaderOpacity isVisible={!roleSearchPopoverActive}>
|
||||||
|
<TextInputWithIcon
|
||||||
|
icon={<GoSearch />}
|
||||||
|
placeholder={'Type or drag a role...'}
|
||||||
|
onFocus={() => setRoleSearchPopoverActive(true)}
|
||||||
|
value={roleSearchTerm}
|
||||||
|
onChange={(x) => updateSearchTerm(x.target.value)}
|
||||||
|
/>
|
||||||
|
<RoleContainer>
|
||||||
|
{props.category.rolesList.map((id) => {
|
||||||
|
const role = props.guildRoles.find((x) => x.id === id);
|
||||||
|
if (!role) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Role
|
||||||
|
role={role}
|
||||||
|
selected={false}
|
||||||
|
key={id}
|
||||||
|
type="delete"
|
||||||
|
onClick={handleRoleDeselect(role)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</RoleContainer>
|
||||||
|
</FaderOpacity>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
1
src/design-system/molecules/editor-category/index.ts
Normal file
1
src/design-system/molecules/editor-category/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './EditorCategory';
|
10
src/design-system/molecules/help-page-base/BUILD.bazel
Normal file
10
src/design-system/molecules/help-page-base/BUILD.bazel
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
load("//hack/bazel/js:react.bzl", "react_library")
|
||||||
|
|
||||||
|
package(default_visibility = ["//visibility:public"])
|
||||||
|
|
||||||
|
react_library(
|
||||||
|
name = "help-page-base",
|
||||||
|
deps = [
|
||||||
|
"//src/design-system/atoms/colors",
|
||||||
|
],
|
||||||
|
)
|
|
@ -0,0 +1,14 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { RoleSearch } from './RoleSearch';
|
||||||
|
import { roleCategory } from 'roleypoly/src/design-system/shared-types/storyData';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Molecules/Role Search',
|
||||||
|
component: RoleSearch,
|
||||||
|
args: {
|
||||||
|
roles: roleCategory,
|
||||||
|
searchTerm: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Search = (args) => <RoleSearch {...args} />;
|
57
src/design-system/molecules/role-search/RoleSearch.tsx
Normal file
57
src/design-system/molecules/role-search/RoleSearch.tsx
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import Fuse from 'fuse.js';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { GoSearch } from 'react-icons/go';
|
||||||
|
import { Role } from 'roleypoly/src/design-system/atoms/role';
|
||||||
|
import { Space } from 'roleypoly/src/design-system/atoms/space';
|
||||||
|
import { TextInputWithIcon } from 'roleypoly/src/design-system/atoms/text-input';
|
||||||
|
import { Role as RoleType } from 'roleypoly/src/design-system/shared-types';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
roles: RoleType[];
|
||||||
|
placeholder?: string;
|
||||||
|
onSelect: (role: RoleType) => void;
|
||||||
|
onSearchUpdate: (newTerm: string) => void;
|
||||||
|
searchTerm: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RoleSearch = (props: Props) => {
|
||||||
|
const fuse = new Fuse(props.roles, { includeScore: true, keys: ['name'] });
|
||||||
|
const results =
|
||||||
|
props.searchTerm !== ''
|
||||||
|
? fuse.search(props.searchTerm)
|
||||||
|
: props.roles.map((role) => ({
|
||||||
|
item: role,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const handleClick = (role: RoleType) => () => {
|
||||||
|
props.onSelect(role);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<TextInputWithIcon
|
||||||
|
icon={<GoSearch />}
|
||||||
|
placeholder={props.placeholder || 'Search or drag a role...'}
|
||||||
|
value={props.searchTerm}
|
||||||
|
onChange={(x) => props.onSearchUpdate(x.target.value)}
|
||||||
|
/>
|
||||||
|
<Space />
|
||||||
|
{results.map((resultRole, idx) => (
|
||||||
|
<RoleInliner key={idx}>
|
||||||
|
<Role
|
||||||
|
selected={false}
|
||||||
|
role={resultRole.item}
|
||||||
|
onClick={handleClick(resultRole.item)}
|
||||||
|
key={`${idx}role`}
|
||||||
|
/>
|
||||||
|
</RoleInliner>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const RoleInliner = styled.div`
|
||||||
|
display: flex;
|
||||||
|
margin: 5px 0;
|
||||||
|
`;
|
1
src/design-system/molecules/role-search/index.ts
Normal file
1
src/design-system/molecules/role-search/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './RoleSearch';
|
10
src/design-system/organisms/editor/EditorShell.stories.tsx
Normal file
10
src/design-system/organisms/editor/EditorShell.stories.tsx
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { EditorShell } from './EditorShell';
|
||||||
|
import { guildEnum } from 'roleypoly/src/design-system/shared-types/storyData';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Organisms/Editor',
|
||||||
|
component: EditorShell,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Shell = () => <EditorShell guild={guildEnum.guildsList[0]} />;
|
8
src/design-system/organisms/editor/EditorShell.styled.ts
Normal file
8
src/design-system/organisms/editor/EditorShell.styled.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { palette } from 'roleypoly/src/design-system/atoms/colors';
|
||||||
|
|
||||||
|
export const CategoryContainer = styled.div`
|
||||||
|
background-color: ${palette.taupe100};
|
||||||
|
padding: 10px;
|
||||||
|
margin: 15px 0;
|
||||||
|
`;
|
31
src/design-system/organisms/editor/EditorShell.tsx
Normal file
31
src/design-system/organisms/editor/EditorShell.tsx
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { TabView, Tab } from 'roleypoly/src/design-system/atoms/tab-view';
|
||||||
|
import { PresentableGuild } from 'roleypoly/src/design-system/shared-types';
|
||||||
|
import { EditorCategory } from '../../molecules/editor-category';
|
||||||
|
import { CategoryContainer } from './EditorShell.styled';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
guild: PresentableGuild;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EditorShell = (props: Props) => (
|
||||||
|
<TabView selected={0}>
|
||||||
|
<Tab title="Roles">{() => <RolesTab {...props} />}</Tab>
|
||||||
|
<Tab title="Server Details">{() => <div>hi2!</div>}</Tab>
|
||||||
|
</TabView>
|
||||||
|
);
|
||||||
|
|
||||||
|
const RolesTab = (props: Props) => (
|
||||||
|
<div>
|
||||||
|
{props.guild.data.categoriesList.map((category, idx) => (
|
||||||
|
<CategoryContainer key={idx}>
|
||||||
|
<EditorCategory
|
||||||
|
category={category}
|
||||||
|
uncategorizedRoles={[]}
|
||||||
|
guildRoles={props.guild.roles.rolesList}
|
||||||
|
onChange={(x) => console.log(x)}
|
||||||
|
/>
|
||||||
|
</CategoryContainer>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
1
src/design-system/organisms/editor/index.ts
Normal file
1
src/design-system/organisms/editor/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './EditorShell';
|
|
@ -3,7 +3,7 @@ load("//hack/bazel/js:react.bzl", "react_library")
|
||||||
package(default_visibility = ["//visibility:public"])
|
package(default_visibility = ["//visibility:public"])
|
||||||
|
|
||||||
react_library(
|
react_library(
|
||||||
name = "role-picker",
|
name = "preauth",
|
||||||
deps = [
|
deps = [
|
||||||
"react-icons",
|
"react-icons",
|
||||||
"//src/design-system/atoms/button",
|
"//src/design-system/atoms/button",
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"paths": {
|
"paths": {
|
||||||
"roleypoly/*": ["./*", "./bazel-bin/*"]
|
"roleypoly/*": ["./*", "./dist/bin/*"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7082,6 +7082,11 @@ fuse.js@^3.6.1:
|
||||||
resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-3.6.1.tgz#7de85fdd6e1b3377c23ce010892656385fd9b10c"
|
resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-3.6.1.tgz#7de85fdd6e1b3377c23ce010892656385fd9b10c"
|
||||||
integrity sha512-hT9yh/tiinkmirKrlv4KWOjztdoZo1mx9Qh4KvWqC7isoXwdUY3PNWUxceF4/qO9R6riA2C29jdTOeQOIROjgw==
|
integrity sha512-hT9yh/tiinkmirKrlv4KWOjztdoZo1mx9Qh4KvWqC7isoXwdUY3PNWUxceF4/qO9R6riA2C29jdTOeQOIROjgw==
|
||||||
|
|
||||||
|
fuse.js@^6.4.2:
|
||||||
|
version "6.4.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-6.4.2.tgz#bd1400164de6562c077baeaa54ed642b4c2fca62"
|
||||||
|
integrity sha512-jyy+zOtV96ylMqOGpVjAWQfvEkfTtuJRjsOC6pjReeju8SoDZ2vgFF6eur0a8fxsVwwcpdt3ieaQQeLdkqXH1Q==
|
||||||
|
|
||||||
gauge@~2.7.3:
|
gauge@~2.7.3:
|
||||||
version "2.7.4"
|
version "2.7.4"
|
||||||
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
|
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
|
||||||
|
|
Loading…
Add table
Reference in a new issue