mirror of
https://github.com/filecoin-project/slate.git
synced 2024-11-30 12:26:00 +03:00
Merge pull request #169 from filecoin-project/@martinalong/profiles
In-client profiles / slates + directory tab
This commit is contained in:
commit
efdc156448
@ -71,6 +71,14 @@ export const generate = ({ library = [], slates = [] }) => [
|
|||||||
pageTitle: "Slates",
|
pageTitle: "Slates",
|
||||||
children: constructSlatesTreeForNavigation(slates),
|
children: constructSlatesTreeForNavigation(slates),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "V1_NAVIGATION_SLATE",
|
||||||
|
decorator: "VIEW_SLATE",
|
||||||
|
name: "Slate",
|
||||||
|
pageTitle: "Slate",
|
||||||
|
children: null,
|
||||||
|
ignore: true,
|
||||||
|
},
|
||||||
constructFilesTreeForNavigation(library),
|
constructFilesTreeForNavigation(library),
|
||||||
/*
|
/*
|
||||||
{
|
{
|
||||||
@ -111,7 +119,7 @@ export const generate = ({ library = [], slates = [] }) => [
|
|||||||
children: null,
|
children: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "V1_NAVIGATION_PROFILE",
|
id: "V1_NAVIGATION_PROFILE_EDIT",
|
||||||
decorator: "EDIT_ACCOUNT",
|
decorator: "EDIT_ACCOUNT",
|
||||||
name: "Profile & Account Settings",
|
name: "Profile & Account Settings",
|
||||||
pageTitle: "Your Profile & Account Settings",
|
pageTitle: "Your Profile & Account Settings",
|
||||||
@ -126,4 +134,20 @@ export const generate = ({ library = [], slates = [] }) => [
|
|||||||
children: null,
|
children: null,
|
||||||
ignore: true,
|
ignore: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "V1_NAVIGATION_PROFILE",
|
||||||
|
decorator: "PROFILE",
|
||||||
|
name: "Profile",
|
||||||
|
pageTitle: "Profile",
|
||||||
|
children: null,
|
||||||
|
ignore: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "V1_NAVIGATION_FILE",
|
||||||
|
decorator: "FILE",
|
||||||
|
name: "File",
|
||||||
|
pageTitle: "File",
|
||||||
|
children: null,
|
||||||
|
ignore: true,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
@ -23,6 +23,7 @@ import SceneSignIn from "~/scenes/SceneSignIn";
|
|||||||
import SceneSlate from "~/scenes/SceneSlate";
|
import SceneSlate from "~/scenes/SceneSlate";
|
||||||
import SceneActivity from "~/scenes/SceneActivity";
|
import SceneActivity from "~/scenes/SceneActivity";
|
||||||
import SceneDirectory from "~/scenes/SceneDirectory";
|
import SceneDirectory from "~/scenes/SceneDirectory";
|
||||||
|
import SceneProfile from "~/scenes/SceneProfile";
|
||||||
|
|
||||||
// NOTE(jim):
|
// NOTE(jim):
|
||||||
// Sidebars each have a decorator and can be shown to with _handleAction
|
// Sidebars each have a decorator and can be shown to with _handleAction
|
||||||
@ -58,6 +59,9 @@ const SIDEBARS = {
|
|||||||
|
|
||||||
const SCENES = {
|
const SCENES = {
|
||||||
HOME: <SceneHome />,
|
HOME: <SceneHome />,
|
||||||
|
DIRECTORY: <SceneDirectory />,
|
||||||
|
PROFILE: <SceneProfile />,
|
||||||
|
VIEW_SLATE: <SceneSlate />,
|
||||||
WALLET: <SceneWallet />,
|
WALLET: <SceneWallet />,
|
||||||
FOLDER: <SceneFilesFolder />,
|
FOLDER: <SceneFilesFolder />,
|
||||||
FILE: <SceneFile />,
|
FILE: <SceneFile />,
|
||||||
@ -73,6 +77,8 @@ const SCENES = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default class ApplicationPage extends React.Component {
|
export default class ApplicationPage extends React.Component {
|
||||||
|
_body;
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
selected: {},
|
selected: {},
|
||||||
viewer: this.props.viewer,
|
viewer: this.props.viewer,
|
||||||
@ -232,7 +238,7 @@ export default class ApplicationPage extends React.Component {
|
|||||||
|
|
||||||
this.setState(updates);
|
this.setState(updates);
|
||||||
|
|
||||||
return { rehydrated: true };
|
return response;
|
||||||
};
|
};
|
||||||
|
|
||||||
_handleSubmit = async (data) => {
|
_handleSubmit = async (data) => {
|
||||||
@ -347,13 +353,19 @@ export default class ApplicationPage extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
_handleDismissSidebar = () => {
|
_handleDismissSidebar = () => {
|
||||||
this.setState({ sidebar: null, sidebarLoading: false, data: null });
|
this.setState({ sidebar: null, sidebarLoading: false, sidebarData: null });
|
||||||
};
|
};
|
||||||
|
|
||||||
_handleAction = (options) => {
|
_handleAction = (options) => {
|
||||||
console.log(options);
|
console.log(options);
|
||||||
if (options.type === "NAVIGATE") {
|
if (options.type === "NAVIGATE") {
|
||||||
return this._handleNavigateTo({ id: options.value }, options.data);
|
// NOTE(martina): The `scene` property is only necessary when you need to display a component different from the one corresponding to the tab it appears in
|
||||||
|
// + e.g. to display <SceneProfile/> while on the Home tab
|
||||||
|
// + `scene` should be the decorator of the component you want displayed
|
||||||
|
return this._handleNavigateTo(
|
||||||
|
{ id: options.value, scene: options.scene },
|
||||||
|
options.data
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.type === "NEW_WINDOW") {
|
if (options.type === "NEW_WINDOW") {
|
||||||
@ -371,7 +383,7 @@ export default class ApplicationPage extends React.Component {
|
|||||||
if (options.type === "SIDEBAR") {
|
if (options.type === "SIDEBAR") {
|
||||||
return this.setState({
|
return this.setState({
|
||||||
sidebar: SIDEBARS[options.value],
|
sidebar: SIDEBARS[options.value],
|
||||||
data: options.data,
|
sidebarData: options.data,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -379,8 +391,9 @@ export default class ApplicationPage extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
_handleNavigateTo = (next, data = null) => {
|
_handleNavigateTo = (next, data = null) => {
|
||||||
this.state.history[this.state.currentIndex].scrollTop = window.scrollY;
|
let body = document.getElementById("slate-client-body");
|
||||||
this.state.history[this.state.currentIndex].data = data;
|
this.state.history[this.state.currentIndex].scrollTop = body.scrollTop; //window.scrollY => body.scrollTop (where body is the body of the ApplicationLayout)
|
||||||
|
this.state.history[this.state.currentIndex].data = this.state.data; //BUG FIX: was originally = data. So it was setting it equal to the data for the next one rather than the current one
|
||||||
|
|
||||||
if (this.state.currentIndex !== this.state.history.length - 1) {
|
if (this.state.currentIndex !== this.state.history.length - 1) {
|
||||||
const adjustedArray = [...this.state.history];
|
const adjustedArray = [...this.state.history];
|
||||||
@ -393,7 +406,7 @@ export default class ApplicationPage extends React.Component {
|
|||||||
data,
|
data,
|
||||||
sidebar: null,
|
sidebar: null,
|
||||||
},
|
},
|
||||||
() => window.scrollTo(0, 0)
|
() => body.scrollTo(0, 0)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -404,12 +417,14 @@ export default class ApplicationPage extends React.Component {
|
|||||||
data,
|
data,
|
||||||
sidebar: null,
|
sidebar: null,
|
||||||
},
|
},
|
||||||
() => window.scrollTo(0, 0)
|
() => body.scrollTo(0, 0)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
_handleBack = () => {
|
_handleBack = () => {
|
||||||
this.state.history[this.state.currentIndex].scrollTop = window.scrollY;
|
let body = document.getElementById("slate-client-body");
|
||||||
|
this.state.history[this.state.currentIndex].scrollTop = body.scrollTop;
|
||||||
|
this.state.history[this.state.currentIndex].data = this.state.data; //BUG FIX: if you go back, it doesn't save the data for that page. so if you go forward to it again, it breaks. changed data => this.state.data
|
||||||
|
|
||||||
const next = this.state.history[this.state.currentIndex - 1];
|
const next = this.state.history[this.state.currentIndex - 1];
|
||||||
|
|
||||||
@ -421,13 +436,14 @@ export default class ApplicationPage extends React.Component {
|
|||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
console.log({ next });
|
console.log({ next });
|
||||||
window.scrollTo(0, next.scrollTop);
|
body.scrollTo(0, next.scrollTop);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
_handleForward = () => {
|
_handleForward = () => {
|
||||||
this.state.history[this.state.currentIndex].scrollTop = window.scrollY;
|
let body = document.getElementById("slate-client-body");
|
||||||
|
this.state.history[this.state.currentIndex].scrollTop = body.scrollTop;
|
||||||
|
|
||||||
const next = this.state.history[this.state.currentIndex + 1];
|
const next = this.state.history[this.state.currentIndex + 1];
|
||||||
|
|
||||||
@ -439,7 +455,7 @@ export default class ApplicationPage extends React.Component {
|
|||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
console.log({ next });
|
console.log({ next });
|
||||||
window.scrollTo(0, next.scrollTop);
|
body.scrollTo(0, next.scrollTop);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -467,6 +483,7 @@ export default class ApplicationPage extends React.Component {
|
|||||||
const navigation = NavigationData.generate(this.state.viewer);
|
const navigation = NavigationData.generate(this.state.viewer);
|
||||||
const next = this.state.history[this.state.currentIndex];
|
const next = this.state.history[this.state.currentIndex];
|
||||||
const current = NavigationData.getCurrentById(navigation, next.id);
|
const current = NavigationData.getCurrentById(navigation, next.id);
|
||||||
|
console.log(this.state.history);
|
||||||
|
|
||||||
const navigationElement = (
|
const navigationElement = (
|
||||||
<ApplicationNavigation
|
<ApplicationNavigation
|
||||||
@ -492,20 +509,24 @@ export default class ApplicationPage extends React.Component {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
const scene = React.cloneElement(SCENES[current.target.decorator], {
|
const scene = React.cloneElement(
|
||||||
current: current.target,
|
SCENES[next.scene || current.target.decorator],
|
||||||
data: this.state.data,
|
{
|
||||||
viewer: this.state.viewer,
|
current: current.target,
|
||||||
selected: this.state.selected,
|
data: this.state.data,
|
||||||
onNavigateTo: this._handleNavigateTo,
|
viewer: this.state.viewer,
|
||||||
onSelectedChange: this._handleSelectedChange,
|
selected: this.state.selected,
|
||||||
onViewerChange: this._handleViewerChange,
|
onNavigateTo: this._handleNavigateTo,
|
||||||
onDeleteYourself: this._handleDeleteYourself,
|
onSelectedChange: this._handleSelectedChange,
|
||||||
onAction: this._handleAction,
|
onViewerChange: this._handleViewerChange,
|
||||||
onBack: this._handleBack,
|
onDeleteYourself: this._handleDeleteYourself,
|
||||||
onForward: this._handleForward,
|
onAction: this._handleAction,
|
||||||
onRehydrate: this.rehydrate,
|
onBack: this._handleBack,
|
||||||
});
|
onForward: this._handleForward,
|
||||||
|
onRehydrate: this.rehydrate,
|
||||||
|
sceneId: current.target.id,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
let sidebarElement;
|
let sidebarElement;
|
||||||
if (this.state.sidebar) {
|
if (this.state.sidebar) {
|
||||||
|
@ -2,7 +2,11 @@ import * as React from "react";
|
|||||||
import * as Constants from "~/common/constants";
|
import * as Constants from "~/common/constants";
|
||||||
import * as OldSVG from "~/components/system/svg";
|
import * as OldSVG from "~/components/system/svg";
|
||||||
|
|
||||||
import { TooltipWrapper, dispatchCustomEvent, PopoverNavigation } from "~/components/system";
|
import {
|
||||||
|
TooltipWrapper,
|
||||||
|
dispatchCustomEvent,
|
||||||
|
PopoverNavigation,
|
||||||
|
} from "~/components/system";
|
||||||
import { css } from "@emotion/react";
|
import { css } from "@emotion/react";
|
||||||
|
|
||||||
import Dismissible from "~/components/core/Dismissible";
|
import Dismissible from "~/components/core/Dismissible";
|
||||||
@ -67,7 +71,8 @@ export default class ApplicationControlMenu extends React.Component {
|
|||||||
captureScroll={false}
|
captureScroll={false}
|
||||||
enabled
|
enabled
|
||||||
onOutsideRectEvent={this._handleHide}
|
onOutsideRectEvent={this._handleHide}
|
||||||
style={this.props.style}>
|
style={this.props.style}
|
||||||
|
>
|
||||||
<PopoverNavigation
|
<PopoverNavigation
|
||||||
style={{
|
style={{
|
||||||
left: 0,
|
left: 0,
|
||||||
@ -79,8 +84,8 @@ export default class ApplicationControlMenu extends React.Component {
|
|||||||
onSignOut={this._handleSignOut}
|
onSignOut={this._handleSignOut}
|
||||||
navigation={[
|
navigation={[
|
||||||
{
|
{
|
||||||
text: "Account Settings",
|
text: "Profile & account settings",
|
||||||
value: "V1_NAVIGATION_PROFILE",
|
value: "V1_NAVIGATION_PROFILE_EDIT",
|
||||||
},
|
},
|
||||||
/*
|
/*
|
||||||
{
|
{
|
||||||
@ -92,13 +97,17 @@ export default class ApplicationControlMenu extends React.Component {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Dismissible>
|
</Dismissible>
|
||||||
}>
|
}
|
||||||
|
>
|
||||||
<CircleButtonLight
|
<CircleButtonLight
|
||||||
onClick={this._handleClick}
|
onClick={this._handleClick}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: this.state.visible ? Constants.system.brand : Constants.system.white,
|
backgroundColor: this.state.visible
|
||||||
|
? Constants.system.brand
|
||||||
|
: Constants.system.white,
|
||||||
color: this.state.visible ? Constants.system.white : null,
|
color: this.state.visible ? Constants.system.white : null,
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<OldSVG.ChevronDown height="20px" />
|
<OldSVG.ChevronDown height="20px" />
|
||||||
</CircleButtonLight>
|
</CircleButtonLight>
|
||||||
</TooltipWrapper>
|
</TooltipWrapper>
|
||||||
|
@ -3,7 +3,7 @@ import * as Constants from "~/common/constants";
|
|||||||
import * as SVG from "~/common/svg";
|
import * as SVG from "~/common/svg";
|
||||||
|
|
||||||
import { css } from "@emotion/react";
|
import { css } from "@emotion/react";
|
||||||
import { SpotlightSearch } from "~/components/system/modules/SpotlightSearch";
|
import { SearchModal } from "~/components/core/SearchModal";
|
||||||
import { dispatchCustomEvent } from "~/common/custom-events";
|
import { dispatchCustomEvent } from "~/common/custom-events";
|
||||||
|
|
||||||
const STYLES_ICON_ELEMENT = css`
|
const STYLES_ICON_ELEMENT = css`
|
||||||
@ -72,7 +72,7 @@ export default class ApplicationHeader extends React.Component {
|
|||||||
_handleCreateSearch = (e) => {
|
_handleCreateSearch = (e) => {
|
||||||
dispatchCustomEvent({
|
dispatchCustomEvent({
|
||||||
name: "create-modal",
|
name: "create-modal",
|
||||||
detail: { modal: <SpotlightSearch onAction={this.props.onAction} /> },
|
detail: { modal: <SearchModal onAction={this.props.onAction} /> },
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ const IconMap = {
|
|||||||
LOCAL_DATA: <SVG.HardDrive height="20px" />,
|
LOCAL_DATA: <SVG.HardDrive height="20px" />,
|
||||||
PROFILE_PAGE: <SVG.ProfileUser height="20px" />,
|
PROFILE_PAGE: <SVG.ProfileUser height="20px" />,
|
||||||
SETTINGS_DEVELOPER: <SVG.SettingsDeveloper height="20px" />,
|
SETTINGS_DEVELOPER: <SVG.SettingsDeveloper height="20px" />,
|
||||||
|
DIRECTORY: <SVG.Directory height="20px" />,
|
||||||
};
|
};
|
||||||
|
|
||||||
const STYLES_NAVIGATION = css`
|
const STYLES_NAVIGATION = css`
|
||||||
@ -69,6 +70,7 @@ const STYLES_PROFILE = css`
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
transition: 200ms ease all;
|
transition: 200ms ease all;
|
||||||
|
cursor: pointer;
|
||||||
/*
|
/*
|
||||||
:hover {
|
:hover {
|
||||||
color: ${Constants.system.white};
|
color: ${Constants.system.white};
|
||||||
@ -256,11 +258,16 @@ export default class ApplicationNavigation extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<nav css={STYLES_NAVIGATION}>
|
<nav css={STYLES_NAVIGATION}>
|
||||||
<div css={STYLES_NAVIGATION_HEADER}>
|
<div css={STYLES_NAVIGATION_HEADER}>
|
||||||
<a
|
<div
|
||||||
css={STYLES_PROFILE}
|
css={STYLES_PROFILE}
|
||||||
style={{ marginRight: 16 }}
|
style={{ marginRight: 16 }}
|
||||||
href={`/${this.props.viewer.username}`}
|
onClick={() =>
|
||||||
target="_blank"
|
this.props.onAction({
|
||||||
|
type: "NAVIGATE",
|
||||||
|
value: "V1_NAVIGATION_PROFILE",
|
||||||
|
data: this.props.viewer,
|
||||||
|
})
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
css={STYLES_PROFILE_IMAGE}
|
css={STYLES_PROFILE_IMAGE}
|
||||||
@ -269,7 +276,7 @@ export default class ApplicationNavigation extends React.Component {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{this.props.viewer.username}
|
{this.props.viewer.username}
|
||||||
</a>
|
</div>
|
||||||
<ApplicationControlMenu
|
<ApplicationControlMenu
|
||||||
onNavigateTo={this.props.onNavigateTo}
|
onNavigateTo={this.props.onNavigateTo}
|
||||||
onAction={this.props.onAction}
|
onAction={this.props.onAction}
|
||||||
|
78
components/core/Profile.js
Normal file
78
components/core/Profile.js
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import * as Constants from "~/common/constants";
|
||||||
|
|
||||||
|
import { css } from "@emotion/react";
|
||||||
|
|
||||||
|
import SlatePreviewBlock from "~/components/core/SlatePreviewBlock";
|
||||||
|
|
||||||
|
const STYLES_PROFILE = css`
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 16px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const STYLES_PROFILE_IMAGE = css`
|
||||||
|
background-size: cover;
|
||||||
|
background-position: 50% 50%;
|
||||||
|
width: 96px;
|
||||||
|
height: 96px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const STYLES_NAME = css`
|
||||||
|
font-size: ${Constants.typescale.lvl3};
|
||||||
|
margin: 16px 0px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const STYLES_LINK = css`
|
||||||
|
color: ${Constants.system.black};
|
||||||
|
text-decoration: none;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default class Profile extends React.Component {
|
||||||
|
render() {
|
||||||
|
let data = this.props.creator ? this.props.creator : this.props.data;
|
||||||
|
return (
|
||||||
|
<div css={STYLES_PROFILE}>
|
||||||
|
<div
|
||||||
|
css={STYLES_PROFILE_IMAGE}
|
||||||
|
style={{ backgroundImage: `url('${data.data.photo}')` }}
|
||||||
|
/>
|
||||||
|
<div css={STYLES_NAME}>{data.username}</div>
|
||||||
|
{this.props.buttons}
|
||||||
|
<br />
|
||||||
|
{data.slates && data.slates.length ? (
|
||||||
|
<div style={{ width: "100%" }}>
|
||||||
|
{data.slates.map((slate) => {
|
||||||
|
const url = `/${data.username}/${slate.slatename}`;
|
||||||
|
if (this.props.onAction) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={url}
|
||||||
|
onClick={() =>
|
||||||
|
this.props.onAction({
|
||||||
|
type: "NAVIGATE",
|
||||||
|
value: this.props.sceneId,
|
||||||
|
scene: "SLATE",
|
||||||
|
data: { owner: data, ...slate },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SlatePreviewBlock slate={slate} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<a key={url} href={url} css={STYLES_LINK}>
|
||||||
|
<SlatePreviewBlock slate={slate} />
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -11,7 +11,6 @@ const STYLES_ROOT = css`
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
max-width: 648px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -23,6 +22,7 @@ const STYLES_LEFT = css`
|
|||||||
const STYLES_RIGHT = css`
|
const STYLES_RIGHT = css`
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
padding-left: 24px;
|
padding-left: 24px;
|
||||||
|
justify-self: end;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const STYLES_HEADER = css`
|
const STYLES_HEADER = css`
|
||||||
|
@ -17,9 +17,7 @@ const STYLES_DROPDOWN = css`
|
|||||||
background-color: ${Constants.system.white};
|
background-color: ${Constants.system.white};
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
-ms-overflow-style: -ms-autohiding-scrollbar;
|
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
display: none;
|
display: none;
|
||||||
@ -32,8 +30,10 @@ const STYLES_DROPDOWN_ITEM = css`
|
|||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
border: 1px solid ${Constants.system.white};
|
border: 1px solid ${Constants.system.white};
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
:hover {
|
:hover {
|
||||||
border-color: ${Constants.system.border};
|
border-color: ${Constants.system.border} !important;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -72,10 +72,14 @@ const STYLES_INPUT = css`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export class InputMenu extends React.Component {
|
export class SearchDropdown extends React.Component {
|
||||||
_input;
|
_input;
|
||||||
_optionRoot;
|
_optionRoot;
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
defaultResults: [],
|
||||||
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
selectedIndex: -1,
|
selectedIndex: -1,
|
||||||
};
|
};
|
||||||
@ -89,27 +93,15 @@ export class InputMenu extends React.Component {
|
|||||||
window.removeEventListener("keydown", this._handleDocumentKeydown);
|
window.removeEventListener("keydown", this._handleDocumentKeydown);
|
||||||
};
|
};
|
||||||
|
|
||||||
_handleInputChange = (e) => {
|
_handleChange = (e) => {
|
||||||
if (this.state.selectedIndex !== -1) {
|
if (this.state.selectedIndex !== -1) {
|
||||||
this.setState({ selectedIndex: -1 });
|
this.setState({ selectedIndex: -1 });
|
||||||
}
|
}
|
||||||
this.props.onChange({
|
this.props.onChange(e);
|
||||||
target: {
|
|
||||||
value: null,
|
|
||||||
name: this.props.name,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.props.onInputChange(e);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
_handleSelect = (index) => {
|
_handleSelect = (index) => {
|
||||||
let e = {
|
this.props.onSelect(this.props.results[index].value);
|
||||||
target: {
|
|
||||||
value: this.props.options[index].value,
|
|
||||||
name: this.props.name,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
this.props.onChange(e);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
_handleDocumentKeydown = (e) => {
|
_handleDocumentKeydown = (e) => {
|
||||||
@ -119,7 +111,7 @@ export class InputMenu extends React.Component {
|
|||||||
} else if (e.keyCode === 9) {
|
} else if (e.keyCode === 9) {
|
||||||
this._handleDelete();
|
this._handleDelete();
|
||||||
} else if (e.keyCode === 40) {
|
} else if (e.keyCode === 40) {
|
||||||
if (this.state.selectedIndex < this.props.options.length - 1) {
|
if (this.state.selectedIndex < this.props.results.length - 1) {
|
||||||
let listElem = this._optionRoot.children[this.state.selectedIndex + 1];
|
let listElem = this._optionRoot.children[this.state.selectedIndex + 1];
|
||||||
let elemRect = listElem.getBoundingClientRect();
|
let elemRect = listElem.getBoundingClientRect();
|
||||||
let rootRect = this._optionRoot.getBoundingClientRect();
|
let rootRect = this._optionRoot.getBoundingClientRect();
|
||||||
@ -145,7 +137,7 @@ export class InputMenu extends React.Component {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
} else if (e.keyCode === 13) {
|
} else if (e.keyCode === 13) {
|
||||||
if (
|
if (
|
||||||
this.props.options.length > this.state.selectedIndex &&
|
this.props.results.length > this.state.selectedIndex &&
|
||||||
this.state.selectedIndex !== -1
|
this.state.selectedIndex !== -1
|
||||||
) {
|
) {
|
||||||
this._handleSelect(this.state.selectedIndex);
|
this._handleSelect(this.state.selectedIndex);
|
||||||
@ -163,7 +155,7 @@ export class InputMenu extends React.Component {
|
|||||||
value={this.props.inputValue}
|
value={this.props.inputValue}
|
||||||
placeholder={this.props.placeholder}
|
placeholder={this.props.placeholder}
|
||||||
style={this.props.inputStyle}
|
style={this.props.inputStyle}
|
||||||
onChange={this._handleInputChange}
|
onChange={this._handleChange}
|
||||||
ref={(c) => {
|
ref={(c) => {
|
||||||
this._input = c;
|
this._input = c;
|
||||||
}}
|
}}
|
||||||
@ -182,22 +174,23 @@ export class InputMenu extends React.Component {
|
|||||||
css={STYLES_DROPDOWN}
|
css={STYLES_DROPDOWN}
|
||||||
style={this.props.style}
|
style={this.props.style}
|
||||||
>
|
>
|
||||||
{(this.props.options && this.props.options.length
|
{(this.props.results && this.props.results.length
|
||||||
? this.props.options
|
? this.props.results
|
||||||
: this.props.defaultOptions
|
: this.props.defaultResults
|
||||||
).map((each, i) => (
|
).map((each, i) => (
|
||||||
<div
|
<div
|
||||||
key={each.value}
|
key={each.value.data.id}
|
||||||
css={STYLES_DROPDOWN_ITEM}
|
css={STYLES_DROPDOWN_ITEM}
|
||||||
style={{
|
style={{
|
||||||
borderColor:
|
borderColor:
|
||||||
this.state.selectedIndex === i
|
this.state.selectedIndex === i
|
||||||
? Constants.system.border
|
? Constants.system.border
|
||||||
: "auto",
|
: Constants.system.white,
|
||||||
...this.props.itemStyle,
|
...this.props.itemStyle,
|
||||||
}}
|
}}
|
||||||
|
onClick={() => this.props.onSelect(each.value)}
|
||||||
>
|
>
|
||||||
{each.name}
|
{each.component}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
309
components/core/SearchModal.js
Normal file
309
components/core/SearchModal.js
Normal file
@ -0,0 +1,309 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import * as Constants from "~/common/constants";
|
||||||
|
import * as SVG from "~/common/svg";
|
||||||
|
import * as Strings from "~/common/strings";
|
||||||
|
import * as Actions from "~/common/actions";
|
||||||
|
|
||||||
|
import MiniSearch from "minisearch";
|
||||||
|
import SlateMediaObjectPreview from "~/components/core/SlateMediaObjectPreview";
|
||||||
|
|
||||||
|
import { css } from "@emotion/react";
|
||||||
|
import { SearchDropdown } from "~/components/core/SearchDropdown";
|
||||||
|
import { dispatchCustomEvent } from "~/common/custom-events";
|
||||||
|
import { SlatePreviewRow } from "~/components/core/SlatePreviewBlock";
|
||||||
|
import { LoaderSpinner } from "~/components/system/components/Loaders";
|
||||||
|
|
||||||
|
const STYLES_ICON_CIRCLE = css`
|
||||||
|
height: 24px;
|
||||||
|
width: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: ${Constants.system.foreground};
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const STYLES_MODAL = css`
|
||||||
|
width: 95vw;
|
||||||
|
max-width: 600px;
|
||||||
|
height: 60vh;
|
||||||
|
max-height: 500px;
|
||||||
|
padding: 24px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const STYLES_SEARCH_DROPDOWN = {
|
||||||
|
height: "calc(100% - 80px)",
|
||||||
|
width: "calc(100% - 48px)",
|
||||||
|
overflowY: "scroll",
|
||||||
|
};
|
||||||
|
|
||||||
|
const STYLES_USER_ENTRY_CONTAINER = css`
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, auto) 1fr;
|
||||||
|
grid-column-gap: 16px;
|
||||||
|
align-items: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const STYLES_PROFILE_IMAGE = css`
|
||||||
|
background-size: cover;
|
||||||
|
background-position: 50% 50%;
|
||||||
|
height: 24px;
|
||||||
|
width: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const UserEntry = ({ item }) => {
|
||||||
|
return (
|
||||||
|
<div css={STYLES_ENTRY}>
|
||||||
|
<div css={STYLES_USER_ENTRY_CONTAINER}>
|
||||||
|
<div
|
||||||
|
style={{ backgroundImage: `url(${item.data.photo})` }}
|
||||||
|
css={STYLES_PROFILE_IMAGE}
|
||||||
|
/>
|
||||||
|
{item.data.name ? <strong>{item.data.name}</strong> : null}
|
||||||
|
<div>@{item.username}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const STYLES_ENTRY = css`
|
||||||
|
padding: 8px 0px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const STYLES_SLATE_ENTRY_CONTAINER = css`
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, auto) 1fr;
|
||||||
|
grid-column-gap: 16px;
|
||||||
|
align-items: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const STYLES_SLATE_IMAGES_CONTAINER = css`
|
||||||
|
margin-left: 32px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const STYLES_SLATE_IMAGE = css`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 72px;
|
||||||
|
width: 72px;
|
||||||
|
padding: 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin-left: 32px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const STYLES_LINK_HOVER = css`
|
||||||
|
color: ${Constants.system.black};
|
||||||
|
:hover {
|
||||||
|
color: ${Constants.system.brand};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SlateEntry = ({ item }) => {
|
||||||
|
return (
|
||||||
|
<div css={STYLES_ENTRY}>
|
||||||
|
<div css={STYLES_SLATE_ENTRY_CONTAINER}>
|
||||||
|
<div css={STYLES_ICON_CIRCLE}>
|
||||||
|
<SVG.Slate2 height="16px" />
|
||||||
|
</div>
|
||||||
|
<strong>{item.data.name}</strong>
|
||||||
|
<div>@{item.owner.username}</div>
|
||||||
|
</div>
|
||||||
|
{item.data.objects.length ? (
|
||||||
|
<div css={STYLES_SLATE_IMAGES_CONTAINER}>
|
||||||
|
<SlatePreviewRow
|
||||||
|
numItems={4}
|
||||||
|
style={{ width: "72px", height: "72px", padding: "8px" }}
|
||||||
|
containerStyle={{
|
||||||
|
maxHeight: "72px",
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
}}
|
||||||
|
previewStyle={{ fontSize: "12px", padding: "4px" }}
|
||||||
|
slate={item}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const FileEntry = ({ item }) => {
|
||||||
|
return (
|
||||||
|
<div css={STYLES_ENTRY}>
|
||||||
|
<div css={STYLES_USER_ENTRY_CONTAINER}>
|
||||||
|
<div css={STYLES_ICON_CIRCLE}>
|
||||||
|
<SVG.Folder2 height="16px" />
|
||||||
|
</div>
|
||||||
|
<strong>{item.data.file.title || item.data.file.name}</strong>
|
||||||
|
<div css={STYLES_LINK_HOVER}>@{item.data.slate.owner.username}</div>
|
||||||
|
</div>
|
||||||
|
<div css={STYLES_SLATE_IMAGE}>
|
||||||
|
<SlateMediaObjectPreview
|
||||||
|
style={{ fontSize: "12px", padding: "4px" }}
|
||||||
|
url={item.data.file.url}
|
||||||
|
type={item.type}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const STYLES_LOADER = css`
|
||||||
|
position: absolute;
|
||||||
|
top: calc(50% - 22px);
|
||||||
|
left: calc(50% - 22px);
|
||||||
|
`;
|
||||||
|
|
||||||
|
export class SearchModal extends React.Component {
|
||||||
|
state = {
|
||||||
|
loading: true,
|
||||||
|
results: [],
|
||||||
|
inputValue: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount = async () => {
|
||||||
|
await this.fillDirectory();
|
||||||
|
this.setState({ loading: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
fillDirectory = async () => {
|
||||||
|
const response = await Actions.getNetworkDirectory();
|
||||||
|
this.miniSearch = new MiniSearch({
|
||||||
|
fields: ["slatename", "data.name", "username", "filename"],
|
||||||
|
storeFields: [
|
||||||
|
"type",
|
||||||
|
"slatename",
|
||||||
|
"username",
|
||||||
|
"data",
|
||||||
|
"id",
|
||||||
|
"slates",
|
||||||
|
"owner",
|
||||||
|
],
|
||||||
|
extractField: (entry, fieldName) => {
|
||||||
|
return fieldName
|
||||||
|
.split(".")
|
||||||
|
.reduce((doc, key) => doc && doc[key], entry);
|
||||||
|
},
|
||||||
|
searchOptions: {
|
||||||
|
fuzzy: 0.2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
let files = [];
|
||||||
|
if (response.data) {
|
||||||
|
for (let slate of response.data.slates) {
|
||||||
|
if (slate.data.objects.length) {
|
||||||
|
files.push(
|
||||||
|
...slate.data.objects.map((file, i) => {
|
||||||
|
return {
|
||||||
|
type: "FILE",
|
||||||
|
id: file.id,
|
||||||
|
filename: file.title,
|
||||||
|
data: { file, index: i, slate },
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.users = response.data.users;
|
||||||
|
this.slates = response.data.slates;
|
||||||
|
this.miniSearch.addAll(response.data.users);
|
||||||
|
this.miniSearch.addAll(response.data.slates);
|
||||||
|
this.miniSearch.addAll(files);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_handleChange = (e) => {
|
||||||
|
if (!this.state.loading) {
|
||||||
|
this.setState({ inputValue: e.target.value }, () => {
|
||||||
|
let searchResults = this.miniSearch.search(this.state.inputValue);
|
||||||
|
let results = [];
|
||||||
|
for (let item of searchResults) {
|
||||||
|
if (item.type === "USER") {
|
||||||
|
results.push({
|
||||||
|
value: {
|
||||||
|
type: "USER",
|
||||||
|
data: item,
|
||||||
|
},
|
||||||
|
component: <UserEntry item={item} />,
|
||||||
|
});
|
||||||
|
} else if (item.type === "SLATE") {
|
||||||
|
results.push({
|
||||||
|
value: {
|
||||||
|
type: "SLATE",
|
||||||
|
data: item,
|
||||||
|
},
|
||||||
|
component: (
|
||||||
|
<SlateEntry item={item} onAction={this.props.onAction} />
|
||||||
|
),
|
||||||
|
});
|
||||||
|
} else if (item.type === "FILE") {
|
||||||
|
results.push({
|
||||||
|
value: {
|
||||||
|
type: "FILE",
|
||||||
|
data: item,
|
||||||
|
},
|
||||||
|
component: <FileEntry item={item} />,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.setState({ results });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_handleSelect = async (value) => {
|
||||||
|
if (value.type === "SLATE") {
|
||||||
|
value.data.owner = this.users.filter((user) => {
|
||||||
|
return user.username === value.data.owner.username;
|
||||||
|
})[0]; //TODO: slightly hacky way of getting the data. May want to serialize later?
|
||||||
|
this.props.onAction({
|
||||||
|
type: "NAVIGATE",
|
||||||
|
value: "V1_NAVIGATION_SLATE",
|
||||||
|
data: value.data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (value.type === "USER") {
|
||||||
|
this.props.onAction({
|
||||||
|
type: "NAVIGATE",
|
||||||
|
value: "V1_NAVIGATION_PROFILE",
|
||||||
|
data: value.data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (value.type === "FILE") {
|
||||||
|
let slate = value.data.data.slate;
|
||||||
|
slate.owner = this.users.filter((user) => {
|
||||||
|
return user.username === slate.owner.username;
|
||||||
|
})[0]; //TODO: slightly hacky way of getting the data. May want to serialize later?
|
||||||
|
this.props.onAction({
|
||||||
|
type: "NAVIGATE",
|
||||||
|
value: "V1_NAVIGATION_SLATE",
|
||||||
|
data: slate,
|
||||||
|
});
|
||||||
|
dispatchCustomEvent({
|
||||||
|
name: "slate-global-open-carousel",
|
||||||
|
detail: { index: value.data.data.index },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
dispatchCustomEvent({
|
||||||
|
name: "delete-modal",
|
||||||
|
detail: {},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div css={STYLES_MODAL}>
|
||||||
|
<SearchDropdown
|
||||||
|
placeholder="Search..."
|
||||||
|
results={this.state.results}
|
||||||
|
onSelect={this._handleSelect}
|
||||||
|
onChange={this._handleChange}
|
||||||
|
inputValue={this.state.inputValue}
|
||||||
|
style={STYLES_SEARCH_DROPDOWN}
|
||||||
|
/>
|
||||||
|
{this.state.loading ? <LoaderSpinner css={STYLES_LOADER} /> : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -30,26 +30,48 @@ export default class SlateMediaObjectPreview extends React.Component {
|
|||||||
// This is a hack to catch this undefined case I don't want to track down yet.
|
// This is a hack to catch this undefined case I don't want to track down yet.
|
||||||
const url = this.props.url.replace("https://undefined", "https://");
|
const url = this.props.url.replace("https://undefined", "https://");
|
||||||
|
|
||||||
let element = <article css={STYLES_ENTITY}>No Preview</article>;
|
let element = (
|
||||||
|
<article css={STYLES_ENTITY} style={this.props.style}>
|
||||||
|
No Preview
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
|
||||||
if (this.props.type && this.props.type.startsWith("video/")) {
|
if (this.props.type && this.props.type.startsWith("video/")) {
|
||||||
element = <article css={STYLES_ENTITY}>Video</article>;
|
element = (
|
||||||
|
<article css={STYLES_ENTITY} style={this.props.style}>
|
||||||
|
Video
|
||||||
|
</article>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.type && this.props.type.startsWith("audio/")) {
|
if (this.props.type && this.props.type.startsWith("audio/")) {
|
||||||
element = <article css={STYLES_ENTITY}>Audio</article>;
|
element = (
|
||||||
|
<article css={STYLES_ENTITY} style={this.props.style}>
|
||||||
|
Audio
|
||||||
|
</article>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.type && this.props.type.startsWith("application/epub")) {
|
if (this.props.type && this.props.type.startsWith("application/epub")) {
|
||||||
element = <article css={STYLES_ENTITY}>EPub</article>;
|
element = (
|
||||||
|
<article css={STYLES_ENTITY} style={this.props.style}>
|
||||||
|
EPub
|
||||||
|
</article>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.type && this.props.type.startsWith("application/pdf")) {
|
if (this.props.type && this.props.type.startsWith("application/pdf")) {
|
||||||
element = <article css={STYLES_ENTITY}>PDF</article>;
|
element = (
|
||||||
|
<article css={STYLES_ENTITY} style={this.props.style}>
|
||||||
|
PDF
|
||||||
|
</article>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.type && this.props.type.startsWith("image/")) {
|
if (this.props.type && this.props.type.startsWith("image/")) {
|
||||||
element = <img css={STYLES_IMAGE} src={url} />;
|
element = (
|
||||||
|
<img css={STYLES_IMAGE} style={this.props.imageStyle} src={url} />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return element;
|
return element;
|
||||||
|
@ -160,7 +160,7 @@ export default class SlateMediaObjectSidebar extends React.Component {
|
|||||||
const elements = [];
|
const elements = [];
|
||||||
|
|
||||||
if (this.props.data) {
|
if (this.props.data) {
|
||||||
if (this.props.onObjectSave) {
|
if (this.props.editing) {
|
||||||
elements.push(
|
elements.push(
|
||||||
<React.Fragment key="sidebar-media-object-info">
|
<React.Fragment key="sidebar-media-object-info">
|
||||||
<div css={STYLES_SIDEBAR_SECTION}>
|
<div css={STYLES_SIDEBAR_SECTION}>
|
||||||
@ -300,30 +300,29 @@ export default class SlateMediaObjectSidebar extends React.Component {
|
|||||||
|
|
||||||
if (this.props.cid) {
|
if (this.props.cid) {
|
||||||
elements.push(
|
elements.push(
|
||||||
<a
|
<div css={STYLES_SIDEBAR} style={{ height: "auto" }}>
|
||||||
key="sidebar-media-open-file"
|
<a
|
||||||
css={STYLES_BUTTON}
|
key="sidebar-media-open-file"
|
||||||
href={Strings.getCIDGatewayURL(this.props.cid)}
|
css={STYLES_BUTTON}
|
||||||
target="_blank"
|
href={Strings.getCIDGatewayURL(this.props.cid)}
|
||||||
>
|
target="_blank"
|
||||||
Open file in a new browser tab ⭢
|
>
|
||||||
</a>
|
Open file in new tab ⭢
|
||||||
);
|
</a>
|
||||||
|
<a
|
||||||
elements.push(
|
key="sidebar-media-download-file"
|
||||||
<a
|
css={STYLES_BUTTON}
|
||||||
key="sidebar-media-download-file"
|
href={Strings.getCIDGatewayURL(this.props.cid)}
|
||||||
css={STYLES_BUTTON}
|
target="_blank"
|
||||||
href={Strings.getCIDGatewayURL(this.props.cid)}
|
download={this.props.cid}
|
||||||
target="_blank"
|
>
|
||||||
download={this.props.cid}
|
Download file ⭢
|
||||||
>
|
</a>
|
||||||
Download file ⭢
|
</div>
|
||||||
</a>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.onDelete) {
|
if (this.props.onDelete && this.props.editing) {
|
||||||
elements.push(
|
elements.push(
|
||||||
<span
|
<span
|
||||||
key="sidebar-media-object-delete"
|
key="sidebar-media-object-delete"
|
||||||
@ -333,7 +332,7 @@ export default class SlateMediaObjectSidebar extends React.Component {
|
|||||||
{this.props.loading ? (
|
{this.props.loading ? (
|
||||||
<LoaderSpinner style={{ height: 16, width: 16 }} />
|
<LoaderSpinner style={{ height: 16, width: 16 }} />
|
||||||
) : (
|
) : (
|
||||||
<span>Delete Slate object ⭢</span>
|
<span>Delete from slate ⭢</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
75
components/core/SlatePreviewBlock.js
Normal file
75
components/core/SlatePreviewBlock.js
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
//doable with vw. calculate vw - sidebar (if mobile size), divided by numItems = max-width and max-height
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import * as Constants from "~/common/constants";
|
||||||
|
|
||||||
|
import { css } from "@emotion/react";
|
||||||
|
import SlateMediaObjectPreview from "~/components/core/SlateMediaObjectPreview";
|
||||||
|
|
||||||
|
const STYLES_IMAGE_ROW = css`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
max-height: 186px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
@media (max-width: ${Constants.sizes.mobile}px) {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const STYLES_ITEM_BOX = css`
|
||||||
|
width: 186px;
|
||||||
|
height: 186px;
|
||||||
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function SlatePreviewRow(props) {
|
||||||
|
let numItems = props.numItems || 5;
|
||||||
|
let objects =
|
||||||
|
props.slate.data.objects.length > numItems
|
||||||
|
? props.slate.data.objects.slice(0, numItems)
|
||||||
|
: props.slate.data.objects;
|
||||||
|
return (
|
||||||
|
<div css={STYLES_IMAGE_ROW} style={props.containerStyle}>
|
||||||
|
{objects.map((each) => (
|
||||||
|
<div key={each.url} css={STYLES_ITEM_BOX} style={props.style}>
|
||||||
|
<SlateMediaObjectPreview
|
||||||
|
type={each.type}
|
||||||
|
url={each.url}
|
||||||
|
style={props.previewStyle}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const STYLES_BLOCK = css`
|
||||||
|
border: 1px solid ${Constants.system.border};
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: left;
|
||||||
|
margin: 24px auto;
|
||||||
|
width: 95%;
|
||||||
|
max-width: 980px;
|
||||||
|
cursor: pointer;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const STYLES_SLATE_NAME = css`
|
||||||
|
font-size: ${Constants.typescale.lvl1};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default function SlatePreviewBlock(props) {
|
||||||
|
return (
|
||||||
|
<div css={STYLES_BLOCK}>
|
||||||
|
<div css={STYLES_SLATE_NAME}>{props.slate.data.name}</div>
|
||||||
|
<SlatePreviewRow {...props} previewStyle={props.previewStyle} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -16,7 +16,6 @@ import {
|
|||||||
FilecoinRetrievalDealsList,
|
FilecoinRetrievalDealsList,
|
||||||
} from "~/components/system/modules/FilecoinDealsList";
|
} from "~/components/system/modules/FilecoinDealsList";
|
||||||
import { FilecoinSettings } from "~/components/system/modules/FilecoinSettings";
|
import { FilecoinSettings } from "~/components/system/modules/FilecoinSettings";
|
||||||
import { SpotlightSearch } from "~/components/system/modules/SpotlightSearch";
|
|
||||||
|
|
||||||
// NOTE(jim): Global components
|
// NOTE(jim): Global components
|
||||||
import { GlobalModal } from "~/components/system/components/GlobalModal";
|
import { GlobalModal } from "~/components/system/components/GlobalModal";
|
||||||
@ -37,7 +36,6 @@ import { CheckBox } from "~/components/system/components/CheckBox";
|
|||||||
import { CodeTextarea } from "~/components/system/components/CodeTextarea";
|
import { CodeTextarea } from "~/components/system/components/CodeTextarea";
|
||||||
import { DatePicker } from "~/components/system/components/DatePicker";
|
import { DatePicker } from "~/components/system/components/DatePicker";
|
||||||
import { Input } from "~/components/system/components/Input";
|
import { Input } from "~/components/system/components/Input";
|
||||||
import { InputMenu } from "~/components/system/components/InputMenu";
|
|
||||||
import { ListEditor } from "~/components/system/components/ListEditor";
|
import { ListEditor } from "~/components/system/components/ListEditor";
|
||||||
import { HoverTile } from "~/components/system/components/HoverTile";
|
import { HoverTile } from "~/components/system/components/HoverTile";
|
||||||
import { PopoverNavigation } from "~/components/system/components/PopoverNavigation";
|
import { PopoverNavigation } from "~/components/system/components/PopoverNavigation";
|
||||||
@ -121,7 +119,6 @@ export {
|
|||||||
GlobalCarousel,
|
GlobalCarousel,
|
||||||
GlobalNotification,
|
GlobalNotification,
|
||||||
Input,
|
Input,
|
||||||
InputMenu,
|
|
||||||
HoverTile,
|
HoverTile,
|
||||||
ListEditor,
|
ListEditor,
|
||||||
PopoverNavigation,
|
PopoverNavigation,
|
||||||
@ -129,7 +126,6 @@ export {
|
|||||||
SelectCountryMenu,
|
SelectCountryMenu,
|
||||||
SelectMenu,
|
SelectMenu,
|
||||||
Slider,
|
Slider,
|
||||||
SpotlightSearch,
|
|
||||||
StatUpload,
|
StatUpload,
|
||||||
StatDownload,
|
StatDownload,
|
||||||
TabGroup,
|
TabGroup,
|
||||||
|
@ -1,329 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import * as Constants from "~/common/constants";
|
|
||||||
import * as SVG from "~/common/svg";
|
|
||||||
import * as Strings from "~/common/strings";
|
|
||||||
import * as Actions from "~/common/actions";
|
|
||||||
|
|
||||||
import MiniSearch from "minisearch";
|
|
||||||
import SlateMediaObjectPreview from "~/components/core/SlateMediaObjectPreview";
|
|
||||||
|
|
||||||
import { css } from "@emotion/react";
|
|
||||||
import { InputMenu } from "~/components/system/components/InputMenu";
|
|
||||||
import { dispatchCustomEvent } from "~/common/custom-events";
|
|
||||||
|
|
||||||
const STYLES_ICON_CIRCLE = css`
|
|
||||||
height: 24px;
|
|
||||||
width: 24px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: ${Constants.system.foreground};
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const STYLES_MODAL = css`
|
|
||||||
width: 95vw;
|
|
||||||
max-width: 600px;
|
|
||||||
height: 60vh;
|
|
||||||
max-height: 500px;
|
|
||||||
padding: 24px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const STYLES_INPUT_MENU = {
|
|
||||||
height: "calc(100% - 80px)",
|
|
||||||
width: "calc(100% - 48px)",
|
|
||||||
overflowY: "scroll",
|
|
||||||
};
|
|
||||||
|
|
||||||
const STYLES_USER_ENTRY_CONTAINER = css`
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, auto) 1fr;
|
|
||||||
grid-column-gap: 16px;
|
|
||||||
align-items: center;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const STYLES_PROFILE_IMAGE = css`
|
|
||||||
background-size: cover;
|
|
||||||
background-position: 50% 50%;
|
|
||||||
height: 24px;
|
|
||||||
width: 24px;
|
|
||||||
border-radius: 50%;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const UserEntry = ({ item }) => {
|
|
||||||
return (
|
|
||||||
<a css={STYLES_LINK} href={`/${item.username}`}>
|
|
||||||
<div css={STYLES_ENTRY}>
|
|
||||||
<div css={STYLES_USER_ENTRY_CONTAINER}>
|
|
||||||
<div
|
|
||||||
style={{ backgroundImage: `url(${item.data.photo})` }}
|
|
||||||
css={STYLES_PROFILE_IMAGE}
|
|
||||||
/>
|
|
||||||
{item.data.name ? <strong>{item.data.name}</strong> : null}
|
|
||||||
<a css={STYLES_LINK_HOVER} href={`/${item.username}`}>
|
|
||||||
@{item.username}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const STYLES_ENTRY = css`
|
|
||||||
padding: 8px 0px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const STYLES_SLATE_ENTRY_CONTAINER = css`
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, auto) 1fr;
|
|
||||||
grid-column-gap: 16px;
|
|
||||||
align-items: center;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const STYLES_SLATE_IMAGES_CONTAINER = css`
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, auto) 1fr;
|
|
||||||
grid-column-gap: 16px;
|
|
||||||
margin: 8px 0px;
|
|
||||||
margin-left: 40px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const STYLES_SLATE_IMAGE = css`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 72px;
|
|
||||||
width: 72px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const STYLES_LINK = css`
|
|
||||||
color: ${Constants.system.black};
|
|
||||||
text-decoration: none;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const STYLES_LINK_HOVER = css`
|
|
||||||
color: ${Constants.system.black};
|
|
||||||
text-decoration: none;
|
|
||||||
:hover {
|
|
||||||
color: ${Constants.system.brand};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const SlateEntry = ({ item, onAction }) => {
|
|
||||||
//TODO: utilize auto suggest feature of minisearch
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
// onClick={() => {
|
|
||||||
// onAction({ type: "NAVIGATE", value: 17, data: item });
|
|
||||||
// }}
|
|
||||||
>
|
|
||||||
<div css={STYLES_ENTRY}>
|
|
||||||
<div css={STYLES_SLATE_ENTRY_CONTAINER}>
|
|
||||||
<div css={STYLES_ICON_CIRCLE}>
|
|
||||||
<SVG.Slate2 height="16px" />
|
|
||||||
</div>
|
|
||||||
<strong>{item.data.name}</strong>
|
|
||||||
{/* <div>
|
|
||||||
<a css={STYLES_LINK_HOVER} href={`/${item.username}`}>
|
|
||||||
@{item.data.username} TODO: add the owner to the slate entries
|
|
||||||
</a>
|
|
||||||
</div> */}
|
|
||||||
</div>
|
|
||||||
{item.data.objects.length ? (
|
|
||||||
<div css={STYLES_SLATE_IMAGES_CONTAINER}>
|
|
||||||
{item.data.objects.slice(0, 4).map((each) => (
|
|
||||||
<div css={STYLES_SLATE_IMAGE}>
|
|
||||||
<SlateMediaObjectPreview type={each.type} url={each.url} />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const FileEntry = ({ item, onAction }) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
css={STYLES_LINK}
|
|
||||||
// onClick={() => {
|
|
||||||
// onAction({ type: "NAVIGATE", value: 15, data: { url: item.url } });
|
|
||||||
// }}
|
|
||||||
>
|
|
||||||
<div css={STYLES_ENTRY}>
|
|
||||||
<div css={STYLES_USER_ENTRY_CONTAINER}>
|
|
||||||
<div css={STYLES_ICON_CIRCLE}>
|
|
||||||
<SVG.Folder2 height="16px" />
|
|
||||||
</div>
|
|
||||||
<strong>{item.name}</strong>
|
|
||||||
<a href={`/${item.username}`} css={STYLES_LINK_HOVER}>
|
|
||||||
@{item.username}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
backgroundImage: `url(${
|
|
||||||
item.type === "image" ? item.url : fileImg
|
|
||||||
})`,
|
|
||||||
margin: "8px 0px 8px 40px",
|
|
||||||
}}
|
|
||||||
css={STYLES_SLATE_IMAGE}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const STYLES_DROPDOWN_ITEM = css`
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 56px 1fr;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const options = [
|
|
||||||
{
|
|
||||||
name: "Send money",
|
|
||||||
link: null,
|
|
||||||
icon: <SVG.Wallet2 height="16px" />,
|
|
||||||
action: { type: "NAVIGATE", value: 2 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "New slate",
|
|
||||||
link: null,
|
|
||||||
icon: <SVG.Slate2 height="16px" />,
|
|
||||||
action: { type: "NAVIGATE", value: 3 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Upload file",
|
|
||||||
link: null,
|
|
||||||
icon: <SVG.Folder2 height="16px" />,
|
|
||||||
action: { type: "NAVIGATE", value: "data" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Account settings",
|
|
||||||
link: null,
|
|
||||||
icon: <SVG.Tool2 height="16px" />,
|
|
||||||
action: { type: "NAVIGATE", value: 13 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Filecoin settings",
|
|
||||||
link: null,
|
|
||||||
icon: <SVG.Tool2 height="16px" />,
|
|
||||||
action: { type: "NAVIGATE", value: 14 },
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export class SpotlightSearch extends React.Component {
|
|
||||||
state = {
|
|
||||||
options: [],
|
|
||||||
value: null,
|
|
||||||
inputValue: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount = async () => {
|
|
||||||
const response = await Actions.getNetworkDirectory();
|
|
||||||
console.log(response.data);
|
|
||||||
this.miniSearch = new MiniSearch({
|
|
||||||
fields: ["slatename", "data.name", "username"], // fields to index for full-text search
|
|
||||||
storeFields: ["type", "slatename", "username", "data", "id"], // fields to return with search results
|
|
||||||
extractField: (entry, fieldName) => {
|
|
||||||
return fieldName
|
|
||||||
.split(".")
|
|
||||||
.reduce((doc, key) => doc && doc[key], entry); // Access nested fields
|
|
||||||
},
|
|
||||||
searchOptions: {
|
|
||||||
// boost: { "data.name": 2 },
|
|
||||||
fuzzy: 0.2,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.miniSearch.addAll(response.data.users);
|
|
||||||
this.miniSearch.addAll(response.data.slates);
|
|
||||||
//TODO: unpack slates => slate object files and add those too
|
|
||||||
};
|
|
||||||
|
|
||||||
_handleChange = (e) => {
|
|
||||||
// if (e.target.value !== null) {
|
|
||||||
// if (e.target.value.substring(0, 1) === "/") {
|
|
||||||
// window.location.pathname = e.target.value;
|
|
||||||
// } else {
|
|
||||||
// window.location.href = e.target.value;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
};
|
|
||||||
|
|
||||||
_handleInputChange = (e) => {
|
|
||||||
this.setState({ inputValue: e.target.value }, () => {
|
|
||||||
let results = this.miniSearch.search(this.state.inputValue);
|
|
||||||
let options = [];
|
|
||||||
for (let item of results) {
|
|
||||||
if (item.type === "USER") {
|
|
||||||
options.push({
|
|
||||||
value: `/${item.username}`,
|
|
||||||
name: <UserEntry item={item} onAction={this.props.onAction} />,
|
|
||||||
});
|
|
||||||
} else if (item.type === "SLATE") {
|
|
||||||
options.push({
|
|
||||||
value: `/${item.slatename}`, //change this format for input menu to something more appropriate
|
|
||||||
name: <SlateEntry item={item} onAction={this.props.onAction} />,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// else if (item.type === "image" || item.type == "file") {
|
|
||||||
// options.push({
|
|
||||||
// value: `${item.url}`,
|
|
||||||
// name: <FileEntry item={item} onAction={this.props.onAction} />,
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
this.setState({ options });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
_handleAction = (action) => {
|
|
||||||
this.props.onAction(action);
|
|
||||||
dispatchCustomEvent({
|
|
||||||
name: "delete-modal",
|
|
||||||
detail: {},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div css={STYLES_MODAL}>
|
|
||||||
<InputMenu
|
|
||||||
show
|
|
||||||
search
|
|
||||||
name="exampleThree"
|
|
||||||
placeholder="Search..."
|
|
||||||
options={this.state.options}
|
|
||||||
onChange={this._handleChange}
|
|
||||||
value={this.state.value}
|
|
||||||
onInputChange={this._handleInputChange}
|
|
||||||
inputValue={this.state.inputValue}
|
|
||||||
style={STYLES_INPUT_MENU}
|
|
||||||
defaultOptions={[]}
|
|
||||||
// defaultOptions={options.map((option) => {
|
|
||||||
// return {
|
|
||||||
// name: (
|
|
||||||
// <div
|
|
||||||
// css={STYLES_DROPDOWN_ITEM}
|
|
||||||
// onClick={() => this._handleAction(option.action)}
|
|
||||||
// >
|
|
||||||
// <div
|
|
||||||
// css={STYLES_ICON_CIRCLE}
|
|
||||||
// style={{ height: "40px", width: "40px" }}
|
|
||||||
// >
|
|
||||||
// {option.icon}
|
|
||||||
// </div>
|
|
||||||
// <div>{option.name}</div>
|
|
||||||
// </div>
|
|
||||||
// ),
|
|
||||||
// value: option.name,
|
|
||||||
// };
|
|
||||||
// })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -48,7 +48,7 @@ export const getById = async ({ id }) => {
|
|||||||
|
|
||||||
// NOTE(jim): This should be the cheapest call.
|
// NOTE(jim): This should be the cheapest call.
|
||||||
const pendingTrusted = await Data.getPendingTrustedRelationshipsByUserId({
|
const pendingTrusted = await Data.getPendingTrustedRelationshipsByUserId({
|
||||||
userId: [],
|
userId: id,
|
||||||
});
|
});
|
||||||
const r3 = await Serializers.doPendingTrusted({
|
const r3 = await Serializers.doPendingTrusted({
|
||||||
users: [id],
|
users: [id],
|
||||||
|
@ -67,6 +67,7 @@ export default class IntegrationPage extends React.Component {
|
|||||||
|
|
||||||
_handleDelete = async (entity) => {
|
_handleDelete = async (entity) => {
|
||||||
const response = await Actions.deleteTrustRelationship({ id: entity.id });
|
const response = await Actions.deleteTrustRelationship({ id: entity.id });
|
||||||
|
console.log(response);
|
||||||
|
|
||||||
await this._handleUpdate();
|
await this._handleUpdate();
|
||||||
};
|
};
|
||||||
|
@ -2,6 +2,7 @@ import * as React from "react";
|
|||||||
import * as Constants from "~/common/constants";
|
import * as Constants from "~/common/constants";
|
||||||
|
|
||||||
import { css } from "@emotion/react";
|
import { css } from "@emotion/react";
|
||||||
|
import Profile from "~/components/core/Profile";
|
||||||
|
|
||||||
import WebsitePrototypeWrapper from "~/components/core/WebsitePrototypeWrapper";
|
import WebsitePrototypeWrapper from "~/components/core/WebsitePrototypeWrapper";
|
||||||
import WebsitePrototypeHeader from "~/components/core/WebsitePrototypeHeader";
|
import WebsitePrototypeHeader from "~/components/core/WebsitePrototypeHeader";
|
||||||
@ -14,67 +15,11 @@ export const getServerSideProps = async (context) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const STYLES_ROOT = css`
|
const STYLES_ROOT = css`
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-direction: column;
|
grid-template-rows: auto 1fr auto;
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
height: 100vh;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
`;
|
min-height: 100vh;
|
||||||
|
|
||||||
const STYLES_MIDDLE = css`
|
|
||||||
position: relative;
|
|
||||||
min-height: 10%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 24px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const STYLES_CARD = css`
|
|
||||||
margin: 0 auto 0 auto;
|
|
||||||
max-width: 360px;
|
|
||||||
width: 100%;
|
|
||||||
background: ${Constants.system.pitchBlack};
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 0;
|
|
||||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.7);
|
|
||||||
`;
|
|
||||||
|
|
||||||
const STYLES_CARD_IMAGE = css`
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
border-radius: 8px 8px 8px 8px;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const STYLES_CARD_PARAGRAPH = css`
|
|
||||||
font-family: ${Constants.font.code};
|
|
||||||
padding: 24px;
|
|
||||||
font-size: 12px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
text-align: left;
|
|
||||||
color: ${Constants.system.white};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const STYLES_LINK = css`
|
|
||||||
color: ${Constants.system.white};
|
|
||||||
text-decoration: none;
|
|
||||||
transition: 200ms ease color;
|
|
||||||
display: block;
|
|
||||||
margin-top: 4px;
|
|
||||||
|
|
||||||
:visited {
|
|
||||||
color: ${Constants.system.white};
|
|
||||||
}
|
|
||||||
|
|
||||||
:hover {
|
|
||||||
color: ${Constants.system.brand};
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default class ProfilePage extends React.Component {
|
export default class ProfilePage extends React.Component {
|
||||||
@ -92,26 +37,9 @@ export default class ProfilePage extends React.Component {
|
|||||||
>
|
>
|
||||||
<div css={STYLES_ROOT}>
|
<div css={STYLES_ROOT}>
|
||||||
<WebsitePrototypeHeader />
|
<WebsitePrototypeHeader />
|
||||||
|
<div style={{ marginTop: "80px" }}>
|
||||||
<div css={STYLES_CARD}>
|
<Profile {...this.props} />
|
||||||
<img css={STYLES_CARD_IMAGE} src={this.props.creator.data.photo} />
|
|
||||||
{this.props.creator.slates && this.props.creator.slates.length ? (
|
|
||||||
<p css={STYLES_CARD_PARAGRAPH}>
|
|
||||||
{this.props.creator.slates.map((row) => {
|
|
||||||
const url = `/${this.props.creator.username}/${
|
|
||||||
row.slatename
|
|
||||||
}`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<a key={url} css={STYLES_LINK} href={url}>
|
|
||||||
{url}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<WebsitePrototypeFooter />
|
<WebsitePrototypeFooter />
|
||||||
</div>
|
</div>
|
||||||
</WebsitePrototypeWrapper>
|
</WebsitePrototypeWrapper>
|
||||||
|
@ -110,6 +110,7 @@ export default class SlatePage extends React.Component {
|
|||||||
return {
|
return {
|
||||||
id: each.id,
|
id: each.id,
|
||||||
data: each,
|
data: each,
|
||||||
|
editing: false,
|
||||||
component: (
|
component: (
|
||||||
<SlateMediaObject key={each.id} useImageFallback data={each} />
|
<SlateMediaObject key={each.id} useImageFallback data={each} />
|
||||||
),
|
),
|
||||||
@ -126,9 +127,7 @@ export default class SlatePage extends React.Component {
|
|||||||
});
|
});
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const title = `${this.props.creator.username}/${
|
const title = `${this.props.creator.username}/${this.props.slate.slatename}`;
|
||||||
this.props.slate.slatename
|
|
||||||
}`;
|
|
||||||
const url = `https://slate.host/${this.props.creator.username}`;
|
const url = `https://slate.host/${this.props.creator.username}`;
|
||||||
const description = this.props.slate.data.body;
|
const description = this.props.slate.data.body;
|
||||||
|
|
||||||
@ -159,7 +158,7 @@ export default class SlatePage extends React.Component {
|
|||||||
</WebsitePrototypeHeaderGeneric>
|
</WebsitePrototypeHeaderGeneric>
|
||||||
<div css={STYLES_SLATE}>
|
<div css={STYLES_SLATE}>
|
||||||
<Slate
|
<Slate
|
||||||
editable={false}
|
editing={false}
|
||||||
layouts={this.state.layouts}
|
layouts={this.state.layouts}
|
||||||
items={this.props.slate.data.objects}
|
items={this.props.slate.data.objects}
|
||||||
onSelect={this._handleSelect}
|
onSelect={this._handleSelect}
|
||||||
|
@ -1,19 +1,285 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as System from "~/components/system";
|
import * as System from "~/components/system";
|
||||||
|
import * as Actions from "~/common/actions";
|
||||||
|
import * as Constants from "~/common/constants";
|
||||||
|
import * as SVG from "~/components/system/svg";
|
||||||
|
|
||||||
import { css } from "@emotion/react";
|
import { css } from "@emotion/react";
|
||||||
|
|
||||||
import ScenePage from "~/components/core/ScenePage";
|
import ScenePage from "~/components/core/ScenePage";
|
||||||
import ScenePageHeader from "~/components/core/ScenePageHeader";
|
import ScenePageHeader from "~/components/core/ScenePageHeader";
|
||||||
|
|
||||||
// TODO(jim): Figure out the activity story.
|
const STYLES_TAB = css`
|
||||||
|
padding: 8px 8px 8px 0px;
|
||||||
|
margin-right: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: ${Constants.typescale.lvl2};
|
||||||
|
|
||||||
|
@media (max-width: ${Constants.sizes.mobile}px) {
|
||||||
|
font-size: ${Constants.typescale.lvl1};
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const STYLES_TAB_GROUP = css`
|
||||||
|
border-bottom: 1px solid ${Constants.system.gray};
|
||||||
|
margin: 24px 0px 24px 0px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const STYLES_USER_ENTRY = css`
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
align-items: center;
|
||||||
|
font-size: ${Constants.typescale.lvl1};
|
||||||
|
cursor: pointer;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const STYLES_USER = css`
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
align-items: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const STYLES_ACTION_BUTTON = css`
|
||||||
|
padding: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
justify-self: end;
|
||||||
|
|
||||||
|
:hover {
|
||||||
|
color: ${Constants.system.brand};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const STYLES_PROFILE_IMAGE = css`
|
||||||
|
background-size: cover;
|
||||||
|
background-position: 50% 50%;
|
||||||
|
height: 24px;
|
||||||
|
width: 24px;
|
||||||
|
margin: 8px 24px 8px 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
function UserEntry({ user, button, onClick }) {
|
||||||
|
return (
|
||||||
|
<div key={user.username} css={STYLES_USER_ENTRY}>
|
||||||
|
<div css={STYLES_USER} onClick={onClick}>
|
||||||
|
<div
|
||||||
|
css={STYLES_PROFILE_IMAGE}
|
||||||
|
style={{ backgroundImage: `url(${user.data.photo})` }}
|
||||||
|
/>
|
||||||
|
<div>@{user.username}</div>
|
||||||
|
</div>
|
||||||
|
{button}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default class SceneDirectory extends React.Component {
|
export default class SceneDirectory extends React.Component {
|
||||||
|
state = {
|
||||||
|
loading: false,
|
||||||
|
tab: "requests",
|
||||||
|
viewer: this.props.viewer,
|
||||||
|
};
|
||||||
|
|
||||||
|
_handleUpdate = async (e) => {
|
||||||
|
let response = await this.props.onRehydrate();
|
||||||
|
// const response = await Actions.hydrateAuthenticatedUser();
|
||||||
|
|
||||||
|
if (!response || response.error) {
|
||||||
|
alert("TODO: error fetching authenticated viewer");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let viewer = response.data;
|
||||||
|
|
||||||
|
this.setState({ viewer });
|
||||||
|
};
|
||||||
|
|
||||||
|
_handleDelete = async (id) => {
|
||||||
|
const response = await Actions.deleteTrustRelationship({
|
||||||
|
id: id,
|
||||||
|
});
|
||||||
|
await this._handleUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
|
_handleAccept = async (id) => {
|
||||||
|
const response = await Actions.updateTrustRelationship({
|
||||||
|
userId: id,
|
||||||
|
});
|
||||||
|
await this._handleUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
|
_handleFollow = async (id) => {
|
||||||
|
const response = await Actions.createSubscription({
|
||||||
|
userId: id,
|
||||||
|
});
|
||||||
|
await this._handleUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<ScenePage>
|
<ScenePage>
|
||||||
<ScenePageHeader title="Directory [WIP]">
|
<ScenePageHeader title="Directory" />
|
||||||
This scene is currently a work in progress.
|
<div css={STYLES_TAB_GROUP}>
|
||||||
</ScenePageHeader>
|
<div
|
||||||
|
css={STYLES_TAB}
|
||||||
|
style={{
|
||||||
|
color:
|
||||||
|
this.state.tab === "requests"
|
||||||
|
? Constants.system.pitchBlack
|
||||||
|
: Constants.system.gray,
|
||||||
|
}}
|
||||||
|
onClick={() => this.setState({ tab: "requests" })}
|
||||||
|
>
|
||||||
|
Requests
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
css={STYLES_TAB}
|
||||||
|
style={{
|
||||||
|
color:
|
||||||
|
this.state.tab === "peers"
|
||||||
|
? Constants.system.pitchBlack
|
||||||
|
: Constants.system.gray,
|
||||||
|
}}
|
||||||
|
onClick={() => this.setState({ tab: "peers" })}
|
||||||
|
>
|
||||||
|
Trusted
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
css={STYLES_TAB}
|
||||||
|
style={{
|
||||||
|
marginRight: "0px",
|
||||||
|
color:
|
||||||
|
this.state.tab === "following"
|
||||||
|
? Constants.system.pitchBlack
|
||||||
|
: Constants.system.gray,
|
||||||
|
}}
|
||||||
|
onClick={() => this.setState({ tab: "following" })}
|
||||||
|
>
|
||||||
|
Following
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{this.state.tab === "requests"
|
||||||
|
? this.state.viewer.pendingTrusted
|
||||||
|
.filter((relation) => {
|
||||||
|
return !relation.data.verified;
|
||||||
|
})
|
||||||
|
.map((relation) => {
|
||||||
|
let button = (
|
||||||
|
<div
|
||||||
|
css={STYLES_ACTION_BUTTON}
|
||||||
|
onClick={() => this._handleAccept(relation.owner.id)}
|
||||||
|
>
|
||||||
|
Accept
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<UserEntry
|
||||||
|
user={relation.owner}
|
||||||
|
button={button}
|
||||||
|
onClick={() => {
|
||||||
|
this.props.onAction({
|
||||||
|
type: "NAVIGATE",
|
||||||
|
value: this.props.sceneId,
|
||||||
|
scene: "PROFILE",
|
||||||
|
data: relation.owner,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: null}
|
||||||
|
{this.state.tab === "peers" ? (
|
||||||
|
<div>
|
||||||
|
{this.state.viewer.pendingTrusted
|
||||||
|
.filter((relation) => {
|
||||||
|
return relation.data.verified;
|
||||||
|
})
|
||||||
|
.map((relation) => {
|
||||||
|
let button = (
|
||||||
|
<div
|
||||||
|
css={STYLES_ACTION_BUTTON}
|
||||||
|
onClick={() => this._handleDelete(relation.id)}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<UserEntry
|
||||||
|
user={relation.owner}
|
||||||
|
button={button}
|
||||||
|
onClick={() => {
|
||||||
|
this.props.onAction({
|
||||||
|
type: "NAVIGATE",
|
||||||
|
value: this.props.sceneId,
|
||||||
|
scene: "PROFILE",
|
||||||
|
data: relation.owner,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{this.state.viewer.trusted
|
||||||
|
.filter((relation) => {
|
||||||
|
return relation.data.verified;
|
||||||
|
})
|
||||||
|
.map((relation) => {
|
||||||
|
let button = (
|
||||||
|
<div
|
||||||
|
css={STYLES_ACTION_BUTTON}
|
||||||
|
onClick={() => this._handleDelete(relation.id)}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<UserEntry
|
||||||
|
user={relation.user}
|
||||||
|
button={button}
|
||||||
|
onClick={() => {
|
||||||
|
this.props.onAction({
|
||||||
|
type: "NAVIGATE",
|
||||||
|
value: this.props.sceneId,
|
||||||
|
scene: "PROFILE",
|
||||||
|
data: relation.user,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{this.state.tab === "following"
|
||||||
|
? this.state.viewer.subscriptions
|
||||||
|
.filter((relation) => {
|
||||||
|
return !!relation.target_user_id;
|
||||||
|
})
|
||||||
|
.map((relation) => {
|
||||||
|
let button = (
|
||||||
|
<div
|
||||||
|
css={STYLES_ACTION_BUTTON}
|
||||||
|
onClick={() => this._handleFollow(relation.user.id)}
|
||||||
|
>
|
||||||
|
Unfollow
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<UserEntry
|
||||||
|
user={relation.user}
|
||||||
|
button={button}
|
||||||
|
onClick={() => {
|
||||||
|
this.props.onAction({
|
||||||
|
type: "NAVIGATE",
|
||||||
|
value: this.props.sceneId,
|
||||||
|
scene: "PROFILE",
|
||||||
|
data: relation.user,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: null}
|
||||||
</ScenePage>
|
</ScenePage>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
198
scenes/SceneProfile.js
Normal file
198
scenes/SceneProfile.js
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import * as System from "~/components/system";
|
||||||
|
import * as Actions from "~/common/actions";
|
||||||
|
import * as Constants from "~/common/constants";
|
||||||
|
import * as SVG from "~/components/system/svg";
|
||||||
|
|
||||||
|
import { css } from "@emotion/react";
|
||||||
|
import { ButtonPrimary } from "~/components/system/components/Buttons";
|
||||||
|
|
||||||
|
import ScenePage from "~/components/core/ScenePage";
|
||||||
|
import Profile from "~/components/core/Profile";
|
||||||
|
import CircleButtonLight from "~/components/core/CircleButtonLight";
|
||||||
|
|
||||||
|
const BUTTON_STYLES = {
|
||||||
|
border: `1px solid ${Constants.system.border}`,
|
||||||
|
boxShadow: "none",
|
||||||
|
fontFamily: Constants.font.text,
|
||||||
|
margin: "8px",
|
||||||
|
padding: "8px 16px",
|
||||||
|
minHeight: "30px",
|
||||||
|
};
|
||||||
|
|
||||||
|
const BUTTON_SECONDARY_STYLE = {
|
||||||
|
...BUTTON_STYLES,
|
||||||
|
backgroundColor: Constants.system.white,
|
||||||
|
color: Constants.system.brand,
|
||||||
|
};
|
||||||
|
|
||||||
|
const BUTTON_PRIMARY_STYLE = {
|
||||||
|
...BUTTON_STYLES,
|
||||||
|
backgroundColor: Constants.system.brand,
|
||||||
|
color: Constants.system.white,
|
||||||
|
};
|
||||||
|
|
||||||
|
const STATUS_BUTTON_MAP = {
|
||||||
|
trusted: "Remove peer",
|
||||||
|
untrusted: "Add peer",
|
||||||
|
sent: "Cancel request",
|
||||||
|
received: "Accept request",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class SceneProfile extends React.Component {
|
||||||
|
state = {
|
||||||
|
loading: false,
|
||||||
|
trustStatus: "untrusted",
|
||||||
|
followStatus: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount = () => {
|
||||||
|
this.setStatus(this.props.viewer);
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
const isNewScene = prevProps.data.username !== this.props.data.username;
|
||||||
|
|
||||||
|
let isUpdated = false;
|
||||||
|
// if (
|
||||||
|
// this.props.data.data.objects.length !== prevProps.data.data.objects.length
|
||||||
|
// ) {
|
||||||
|
// isUpdated = true;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (this.props.data.data.body !== prevProps.data.data.body) {
|
||||||
|
// isUpdated = true;
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (isNewScene || isUpdated) {
|
||||||
|
this.setStatus(this.props.viewer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setStatus = (viewer) => {
|
||||||
|
let newState = { trustStatus: "untrusted", followStatus: false };
|
||||||
|
let trust = viewer.trusted.filter((entry) => {
|
||||||
|
return entry.target_user_id === this.props.data.id;
|
||||||
|
});
|
||||||
|
if (trust.length) {
|
||||||
|
let relation = trust[0];
|
||||||
|
newState.trustId = relation.id;
|
||||||
|
if (relation.data.verified) {
|
||||||
|
newState.trustStatus = "trusted";
|
||||||
|
} else {
|
||||||
|
newState.trustStatus = "sent";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let pendingTrust = viewer.pendingTrusted.filter((entry) => {
|
||||||
|
return entry.owner_user_id === this.props.data.id;
|
||||||
|
});
|
||||||
|
if (pendingTrust.length) {
|
||||||
|
let relation = pendingTrust[0];
|
||||||
|
newState.trustId = relation.id;
|
||||||
|
if (pendingTrust[0].data.verified) {
|
||||||
|
newState.trustStatus = "trusted";
|
||||||
|
} else {
|
||||||
|
newState.trustStatus = "received";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
viewer.subscriptions.filter((entry) => {
|
||||||
|
return entry.target_user_id === this.props.data.id;
|
||||||
|
}).length
|
||||||
|
) {
|
||||||
|
newState.followStatus = true;
|
||||||
|
}
|
||||||
|
this.setState(newState);
|
||||||
|
};
|
||||||
|
|
||||||
|
_handleUpdate = async (e) => {
|
||||||
|
let response = await this.props.onRehydrate();
|
||||||
|
if (!response || response.error) {
|
||||||
|
alert("TODO: error fetching authenticated viewer");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let viewer = response.data;
|
||||||
|
|
||||||
|
this.setStatus(viewer);
|
||||||
|
};
|
||||||
|
|
||||||
|
_handleTrust = async () => {
|
||||||
|
let response;
|
||||||
|
if (
|
||||||
|
this.state.trustStatus === "untrusted" ||
|
||||||
|
this.state.trustStatus === "sent"
|
||||||
|
) {
|
||||||
|
response = await Actions.createTrustRelationship({
|
||||||
|
userId: this.props.data.id,
|
||||||
|
});
|
||||||
|
console.log(response);
|
||||||
|
} else if (this.state.trustStatus === "received") {
|
||||||
|
response = await Actions.updateTrustRelationship({
|
||||||
|
userId: this.props.data.id,
|
||||||
|
});
|
||||||
|
console.log(response);
|
||||||
|
} else {
|
||||||
|
response = await Actions.deleteTrustRelationship({
|
||||||
|
id: this.state.trustId,
|
||||||
|
});
|
||||||
|
console.log(response);
|
||||||
|
}
|
||||||
|
await this._handleUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
|
_handleFollow = async () => {
|
||||||
|
let response = await Actions.createSubscription({
|
||||||
|
userId: this.props.data.id,
|
||||||
|
});
|
||||||
|
console.log(response);
|
||||||
|
await this._handleUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let buttons = (
|
||||||
|
<div>
|
||||||
|
<ButtonPrimary
|
||||||
|
style={
|
||||||
|
this.state.followStatus
|
||||||
|
? BUTTON_SECONDARY_STYLE
|
||||||
|
: BUTTON_PRIMARY_STYLE
|
||||||
|
}
|
||||||
|
onClick={this._handleFollow}
|
||||||
|
>
|
||||||
|
{this.state.followStatus ? "Unfollow" : "Follow"}
|
||||||
|
</ButtonPrimary>
|
||||||
|
<ButtonPrimary
|
||||||
|
style={
|
||||||
|
this.state.trustStatus === "untrusted" ||
|
||||||
|
this.state.trustStatus === "received"
|
||||||
|
? BUTTON_PRIMARY_STYLE
|
||||||
|
: BUTTON_SECONDARY_STYLE
|
||||||
|
}
|
||||||
|
onClick={this._handleTrust}
|
||||||
|
>
|
||||||
|
{STATUS_BUTTON_MAP[this.state.trustStatus]}
|
||||||
|
</ButtonPrimary>
|
||||||
|
{this.state.isTrusted ? (
|
||||||
|
<ButtonPrimary style={BUTTON_STYLE} onClick={this._handleSendMoney}>
|
||||||
|
Send Money
|
||||||
|
</ButtonPrimary>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<ScenePage style={{ padding: `88px 24px 128px 24px` }}>
|
||||||
|
<Profile
|
||||||
|
onAction={this.props.onAction}
|
||||||
|
creator={this.props.data}
|
||||||
|
sceneId={this.props.sceneId}
|
||||||
|
buttons={
|
||||||
|
this.props.viewer.username === this.props.data.username
|
||||||
|
? null
|
||||||
|
: buttons
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ScenePage>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -12,6 +12,14 @@ import Slate, { generateLayout } from "~/components/core/Slate";
|
|||||||
import SlateMediaObject from "~/components/core/SlateMediaObject";
|
import SlateMediaObject from "~/components/core/SlateMediaObject";
|
||||||
import CircleButtonGray from "~/components/core/CircleButtonGray";
|
import CircleButtonGray from "~/components/core/CircleButtonGray";
|
||||||
|
|
||||||
|
const STYLES_USERNAME = css`
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
:hover {
|
||||||
|
color: ${Constants.system.brand};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
const moveIndex = (set, fromIndex, toIndex) => {
|
const moveIndex = (set, fromIndex, toIndex) => {
|
||||||
const element = set[fromIndex];
|
const element = set[fromIndex];
|
||||||
set.splice(fromIndex, 1);
|
set.splice(fromIndex, 1);
|
||||||
@ -22,15 +30,17 @@ const moveIndex = (set, fromIndex, toIndex) => {
|
|||||||
|
|
||||||
export default class SceneSlate extends React.Component {
|
export default class SceneSlate extends React.Component {
|
||||||
state = {
|
state = {
|
||||||
name: this.props.current.data.name,
|
name: this.props.data.data.name,
|
||||||
slatename: this.props.current.slatename,
|
username: this.props.data.owner ? this.props.data.owner.username : null,
|
||||||
public: this.props.current.data.public,
|
slatename: this.props.data.slatename,
|
||||||
objects: this.props.current.data.objects,
|
public: this.props.data.data.public,
|
||||||
body: this.props.current.data.body,
|
objects: this.props.data.data.objects,
|
||||||
layouts: this.props.current.data.layouts
|
body: this.props.data.data.body,
|
||||||
? this.props.current.data.layouts
|
layouts: this.props.data.data.layouts
|
||||||
: { lg: generateLayout(this.props.current.data.objects) },
|
? this.props.data.data.layouts
|
||||||
|
: { lg: generateLayout(this.props.data.data.objects) },
|
||||||
loading: false,
|
loading: false,
|
||||||
|
editing: this.props.data.data.ownerId === this.props.viewer.id,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@ -39,38 +49,41 @@ export default class SceneSlate extends React.Component {
|
|||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
const isNewSlateScene =
|
const isNewSlateScene =
|
||||||
prevProps.current.slatename !== this.props.current.slatename;
|
prevProps.data.slatename !== this.props.data.slatename;
|
||||||
|
|
||||||
let isUpdated = false;
|
let isUpdated = false;
|
||||||
if (
|
if (
|
||||||
this.props.current.data.objects.length !==
|
this.props.data.data.objects.length !== prevProps.data.data.objects.length
|
||||||
prevProps.current.data.objects.length
|
|
||||||
) {
|
) {
|
||||||
isUpdated = true;
|
isUpdated = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.current.data.body !== prevProps.current.data.body) {
|
if (this.props.data.data.body !== prevProps.data.data.body) {
|
||||||
isUpdated = true;
|
isUpdated = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isNewSlateScene || isUpdated) {
|
if (isNewSlateScene || isUpdated) {
|
||||||
let layouts = this.props.current.data.layouts;
|
let layouts = this.props.data.data.layouts;
|
||||||
if (!layouts) {
|
if (!layouts) {
|
||||||
layouts = { lg: generateLayout(this.props.current.data.objects) };
|
layouts = { lg: generateLayout(this.props.data.data.objects) };
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
slatename: this.props.current.slatename,
|
username: this.props.data.owner ? this.props.data.owner.username : null,
|
||||||
public: this.props.current.data.public,
|
slatename: this.props.data.slatename,
|
||||||
objects: this.props.current.data.objects,
|
public: this.props.data.data.public,
|
||||||
body: this.props.current.data.body,
|
objects: this.props.data.data.objects,
|
||||||
name: this.props.current.data.name,
|
body: this.props.data.data.body,
|
||||||
|
name: this.props.data.data.name,
|
||||||
layouts: layouts,
|
layouts: layouts,
|
||||||
loading: false,
|
loading: false,
|
||||||
|
editing: this.props.viewer.slates
|
||||||
|
.map((slate) => slate.id)
|
||||||
|
.includes(this.props.data.slateId),
|
||||||
});
|
});
|
||||||
|
|
||||||
this._handleUpdateCarousel({
|
this._handleUpdateCarousel({
|
||||||
objects: this.props.current.data.objects,
|
objects: this.props.data.data.objects,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -93,7 +106,7 @@ export default class SceneSlate extends React.Component {
|
|||||||
this.setState({ loading: true });
|
this.setState({ loading: true });
|
||||||
|
|
||||||
const response = await Actions.updateSlate({
|
const response = await Actions.updateSlate({
|
||||||
id: this.props.current.slateId,
|
id: this.props.data.id,
|
||||||
data: {
|
data: {
|
||||||
objects: objects ? objects : this.state.objects,
|
objects: objects ? objects : this.state.objects,
|
||||||
layouts: layouts ? layouts : this.state.layouts,
|
layouts: layouts ? layouts : this.state.layouts,
|
||||||
@ -164,6 +177,7 @@ export default class SceneSlate extends React.Component {
|
|||||||
id: data.id,
|
id: data.id,
|
||||||
cid,
|
cid,
|
||||||
data,
|
data,
|
||||||
|
editing: this.state.editing,
|
||||||
component: (
|
component: (
|
||||||
<SlateMediaObject key={each.id} useImageFallback data={data} />
|
<SlateMediaObject key={each.id} useImageFallback data={data} />
|
||||||
),
|
),
|
||||||
@ -199,7 +213,7 @@ export default class SceneSlate extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const response = await Actions.updateSlate({
|
const response = await Actions.updateSlate({
|
||||||
id: this.props.current.slateId,
|
id: this.props.data.slateId,
|
||||||
data: {
|
data: {
|
||||||
objects,
|
objects,
|
||||||
layouts,
|
layouts,
|
||||||
@ -245,7 +259,7 @@ export default class SceneSlate extends React.Component {
|
|||||||
return this.props.onAction({
|
return this.props.onAction({
|
||||||
type: "SIDEBAR",
|
type: "SIDEBAR",
|
||||||
value: "SIDEBAR_ADD_FILE_TO_BUCKET",
|
value: "SIDEBAR_ADD_FILE_TO_BUCKET",
|
||||||
data: this.props.current,
|
data: this.props.data,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -253,41 +267,70 @@ export default class SceneSlate extends React.Component {
|
|||||||
return this.props.onAction({
|
return this.props.onAction({
|
||||||
type: "SIDEBAR",
|
type: "SIDEBAR",
|
||||||
value: "SIDEBAR_SINGLE_SLATE_SETTINGS",
|
value: "SIDEBAR_SINGLE_SLATE_SETTINGS",
|
||||||
data: this.props.current,
|
data: this.props.data,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { slatename, objects, body = "A slate.", name } = this.state;
|
const {
|
||||||
|
username,
|
||||||
|
slatename,
|
||||||
|
objects,
|
||||||
|
body = "A slate.",
|
||||||
|
name,
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScenePage style={{ padding: `88px 24px 128px 24px` }}>
|
<ScenePage style={{ padding: `88px 24px 128px 24px` }}>
|
||||||
<ScenePageHeader
|
<ScenePageHeader
|
||||||
style={{ padding: `0 24px 0 24px` }}
|
style={{ padding: `0 24px 0 24px` }}
|
||||||
title={name}
|
title={
|
||||||
|
username ? (
|
||||||
|
<span>
|
||||||
|
<span
|
||||||
|
onClick={() =>
|
||||||
|
this.props.onAction({
|
||||||
|
type: "NAVIGATE",
|
||||||
|
value: this.props.sceneId,
|
||||||
|
scene: "PROFILE",
|
||||||
|
data: this.props.data.owner,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
css={STYLES_USERNAME}
|
||||||
|
>
|
||||||
|
{username}
|
||||||
|
</span>{" "}
|
||||||
|
/ {slatename}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
slatename
|
||||||
|
)
|
||||||
|
}
|
||||||
actions={
|
actions={
|
||||||
<React.Fragment>
|
this.state.editing ? (
|
||||||
<CircleButtonGray
|
<React.Fragment>
|
||||||
onMouseUp={this._handleAdd}
|
<CircleButtonGray
|
||||||
onTouchEnd={this._handleAdd}
|
onMouseUp={this._handleAdd}
|
||||||
style={{ marginLeft: 12, marginRight: 12 }}
|
onTouchEnd={this._handleAdd}
|
||||||
>
|
style={{ marginLeft: 12, marginRight: 12 }}
|
||||||
<SVG.Plus height="16px" />
|
>
|
||||||
</CircleButtonGray>
|
<SVG.Plus height="16px" />
|
||||||
<CircleButtonGray
|
</CircleButtonGray>
|
||||||
onMouseUp={this._handleShowSettings}
|
<CircleButtonGray
|
||||||
onTouchEnd={this._handleShowSettings}
|
onMouseUp={this._handleShowSettings}
|
||||||
>
|
onTouchEnd={this._handleShowSettings}
|
||||||
<SVG.Settings height="16px" />
|
>
|
||||||
</CircleButtonGray>
|
<SVG.Settings height="16px" />
|
||||||
</React.Fragment>
|
</CircleButtonGray>
|
||||||
|
</React.Fragment>
|
||||||
|
) : null
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{body}
|
{body}
|
||||||
</ScenePageHeader>
|
</ScenePageHeader>
|
||||||
<Slate
|
<Slate
|
||||||
key={slatename}
|
key={slatename}
|
||||||
editing
|
editing={this.state.editing}
|
||||||
items={objects}
|
items={objects}
|
||||||
layouts={this.state.layouts}
|
layouts={this.state.layouts}
|
||||||
onLayoutChange={this._handleChangeLayout}
|
onLayoutChange={this._handleChangeLayout}
|
||||||
|
@ -7,72 +7,98 @@ import { css } from "@emotion/react";
|
|||||||
import ScenePage from "~/components/core/ScenePage";
|
import ScenePage from "~/components/core/ScenePage";
|
||||||
import ScenePageHeader from "~/components/core/ScenePageHeader";
|
import ScenePageHeader from "~/components/core/ScenePageHeader";
|
||||||
import Section from "~/components/core/Section";
|
import Section from "~/components/core/Section";
|
||||||
|
import SlatePreviewBlock from "~/components/core/SlatePreviewBlock";
|
||||||
|
|
||||||
const STYLES_NUMBER = css`
|
const STYLES_NUMBER = css`
|
||||||
font-family: ${Constants.font.semiBold};
|
font-family: ${Constants.font.semiBold};
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const STYLES_ACTIONS = css`
|
||||||
|
z-index: ${Constants.zindex.navigation};
|
||||||
|
bottom: 16px;
|
||||||
|
right: 8px;
|
||||||
|
position: absolute;
|
||||||
|
flex-direction: column;
|
||||||
|
display: flex;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const STYLES_ACTION_BUTTON = css`
|
||||||
|
font-family: ${Constants.font.code};
|
||||||
|
font-size: 10px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
user-select: none;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 16px 0 16px;
|
||||||
|
border-radius: 32px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: ${Constants.zindex.modal};
|
||||||
|
background: ${Constants.system.pitchBlack};
|
||||||
|
transition: 200ms ease all;
|
||||||
|
color: ${Constants.system.white};
|
||||||
|
cursor: pointer;
|
||||||
|
margin: auto;
|
||||||
|
margin: 4px 16px 4px 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
text-decoration: none;
|
||||||
|
:hover {
|
||||||
|
background-color: ${Constants.system.black};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
// TODO(jim): Slates design.
|
// TODO(jim): Slates design.
|
||||||
export default class SceneSlates extends React.Component {
|
export default class SceneSlates extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
// TODO(jim): Refactor later.
|
// TODO(jim): Refactor later.
|
||||||
const slates = {
|
const slates = this.props.viewer.slates.map((each) => {
|
||||||
columns: [
|
return {
|
||||||
{
|
...each,
|
||||||
key: "name",
|
url: `https://slate.host/${this.props.viewer.username}/${each.slatename}`,
|
||||||
name: "Slate Name",
|
public: each.data.public,
|
||||||
width: "100%",
|
objects: <span css={STYLES_NUMBER}>{each.data.objects.length}</span>,
|
||||||
type: "SLATE_LINK",
|
};
|
||||||
},
|
});
|
||||||
{ key: "url", name: "URL", width: "268px", type: "NEW_WINDOW" },
|
|
||||||
{ key: "id", id: "id", name: "Slate ID", width: "296px" },
|
|
||||||
{
|
|
||||||
key: "objects",
|
|
||||||
name: "Objects",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "public",
|
|
||||||
name: "Public",
|
|
||||||
type: "SLATE_PUBLIC_TEXT_TAG",
|
|
||||||
width: "188px",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
rows: this.props.viewer.slates.map((each) => {
|
|
||||||
return {
|
|
||||||
...each,
|
|
||||||
name: each.data.name,
|
|
||||||
url: `https://slate.host/${this.props.viewer.username}/${
|
|
||||||
each.slatename
|
|
||||||
}`,
|
|
||||||
public: each.data.public,
|
|
||||||
objects: <span css={STYLES_NUMBER}>{each.data.objects.length}</span>,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO(jim): Refactor later.
|
// TODO(jim): Refactor later.
|
||||||
const slateButtons = [
|
const slateButtons = [
|
||||||
{ name: "Create slate", type: "SIDEBAR", value: "SIDEBAR_CREATE_SLATE" },
|
{ name: "Create slate", type: "SIDEBAR", value: "SIDEBAR_CREATE_SLATE" },
|
||||||
];
|
];
|
||||||
|
console.log(this.props);
|
||||||
return (
|
return (
|
||||||
<ScenePage>
|
<ScenePage>
|
||||||
<ScenePageHeader title="Slates [WIP]">
|
<ScenePageHeader title="Slates">
|
||||||
This scene is currently a work in progress.
|
This scene is currently a work in progress.
|
||||||
</ScenePageHeader>
|
</ScenePageHeader>
|
||||||
<Section
|
{this.props.data.children.map((slate) => (
|
||||||
title="Slates"
|
<div
|
||||||
buttons={slateButtons}
|
key={slate.id}
|
||||||
onAction={this.props.onAction}
|
onClick={() =>
|
||||||
>
|
this.props.onAction({
|
||||||
<System.Table
|
type: "NAVIGATE",
|
||||||
data={slates}
|
value: slate.id,
|
||||||
name="slate"
|
data: slate,
|
||||||
onAction={this.props.onAction}
|
})
|
||||||
onNavigateTo={this.props.onNavigateTo}
|
}
|
||||||
/>
|
>
|
||||||
</Section>
|
<SlatePreviewBlock slate={slate} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div css={STYLES_ACTIONS}>
|
||||||
|
<span
|
||||||
|
css={STYLES_ACTION_BUTTON}
|
||||||
|
onClick={() =>
|
||||||
|
this.props.onAction({
|
||||||
|
name: "Create slate",
|
||||||
|
type: "SIDEBAR",
|
||||||
|
value: "SIDEBAR_CREATE_SLATE",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Create slate
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</ScenePage>
|
</ScenePage>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user