mirror of
https://github.com/ilyakooo0/urbit.git
synced 2025-01-04 13:19:48 +03:00
Merge pull request #4388 from urbit/lf/tutorial
interface: add overlay tutorial
This commit is contained in:
commit
548f2143f8
@ -31,7 +31,10 @@
|
||||
^- (quip card _this)
|
||||
=/ old !<(versioned-state old-vase)
|
||||
?- -.old
|
||||
%0 [~ this(state old)]
|
||||
%0
|
||||
:_ this(state old)
|
||||
=- [%pass / %agent [our dap]:bowl %poke -]~
|
||||
settings-event+!>([%put-entry %tutorial %seen b+%|])
|
||||
==
|
||||
::
|
||||
++ on-poke
|
||||
|
@ -1,4 +1,5 @@
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
// const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
// const { CleanWebpackPlugin } = require('clean-webpack-plugin');
|
||||
const urbitrc = require('./urbitrc');
|
||||
@ -117,7 +118,15 @@ module.exports = {
|
||||
devtool: 'inline-source-map',
|
||||
devServer: devServer,
|
||||
plugins: [
|
||||
new UrbitShipPlugin(urbitrc)
|
||||
new UrbitShipPlugin(urbitrc),
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.TUTORIAL_HOST': JSON.stringify('~hastuc-dibtux'),
|
||||
'process.env.TUTORIAL_GROUP': JSON.stringify('beginner-island'),
|
||||
'process.env.TUTORIAL_CHAT': JSON.stringify('chat-8401'),
|
||||
'process.env.TUTORIAL_BOOK': JSON.stringify('notebook-9148'),
|
||||
'process.env.TUTORIAL_LINKS': JSON.stringify('link-4353'),
|
||||
})
|
||||
|
||||
// new CleanWebpackPlugin(),
|
||||
// new HtmlWebpackPlugin({
|
||||
// title: 'Hot Module Replacement',
|
||||
|
@ -56,8 +56,13 @@ module.exports = {
|
||||
new CleanWebpackPlugin(),
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.LANDSCAPE_STREAM': JSON.stringify(process.env.LANDSCAPE_STREAM),
|
||||
'process.env.LANDSCAPE_SHORTHASH': JSON.stringify(process.env.LANDSCAPE_SHORTHASH)
|
||||
})
|
||||
'process.env.LANDSCAPE_SHORTHASH': JSON.stringify(process.env.LANDSCAPE_SHORTHASH),
|
||||
'process.env.TUTORIAL_HOST': JSON.stringify('~hastuc-dibtux'),
|
||||
'process.env.TUTORIAL_GROUP': JSON.stringify('beginner-island'),
|
||||
'process.env.TUTORIAL_CHAT': JSON.stringify('chat-8401'),
|
||||
'process.env.TUTORIAL_BOOK': JSON.stringify('notebook-9148'),
|
||||
'process.env.TUTORIAL_LINKS': JSON.stringify('links-4353'),
|
||||
}),
|
||||
// new HtmlWebpackPlugin({
|
||||
// title: 'Hot Module Replacement',
|
||||
// template: './public/index.html',
|
||||
|
55
pkg/interface/src/logic/lib/relativePosition.tsx
Normal file
55
pkg/interface/src/logic/lib/relativePosition.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
import _ from "lodash";
|
||||
|
||||
export const alignY = ["top", "bottom"] as const;
|
||||
export type AlignY = typeof alignY[number];
|
||||
export const alignX = ["left", "right"] as const;
|
||||
export type AlignX = typeof alignX[number];
|
||||
|
||||
export function getRelativePosition(
|
||||
relativeTo: HTMLElement | null,
|
||||
alignX: AlignX | AlignX[],
|
||||
alignY: AlignY | AlignY[],
|
||||
offsetX: number = 0,
|
||||
offsetY: number = 0
|
||||
) {
|
||||
const rect = relativeTo?.getBoundingClientRect();
|
||||
if (!rect) {
|
||||
return {};
|
||||
}
|
||||
const bounds = {
|
||||
top: rect.top - offsetY,
|
||||
left: rect.left - offsetX,
|
||||
bottom: document.documentElement.clientHeight - rect.bottom - offsetY,
|
||||
right: document.documentElement.clientWidth - rect.right - offsetX,
|
||||
};
|
||||
const alignXArr = _.isArray(alignX) ? alignX : [alignX];
|
||||
const alignYArr = _.isArray(alignY) ? alignY : [alignY];
|
||||
|
||||
return {
|
||||
..._.reduce(
|
||||
alignXArr,
|
||||
(acc, a, idx) => ({
|
||||
...acc,
|
||||
[a]: _.zipWith(
|
||||
[...Array(idx), `${bounds[a]}px`],
|
||||
acc[a] || [],
|
||||
(a, b) => a || b || null
|
||||
),
|
||||
}),
|
||||
{}
|
||||
),
|
||||
..._.reduce(
|
||||
alignYArr,
|
||||
(acc, a, idx) => ({
|
||||
...acc,
|
||||
[a]: _.zipWith(
|
||||
[...Array(idx), `${bounds[a]}px`],
|
||||
acc[a] || [],
|
||||
(a, b) => a || b || null
|
||||
),
|
||||
}),
|
||||
{}
|
||||
),
|
||||
} as Record<AlignY | AlignX, string[]>;
|
||||
}
|
||||
|
133
pkg/interface/src/logic/lib/tutorialModal.ts
Normal file
133
pkg/interface/src/logic/lib/tutorialModal.ts
Normal file
@ -0,0 +1,133 @@
|
||||
import { TutorialProgress, Associations } from "~/types";
|
||||
import { AlignX, AlignY } from "~/logic/lib/relativePosition";
|
||||
|
||||
export const MODAL_WIDTH = 256;
|
||||
export const MODAL_HEIGHT = 180;
|
||||
export const MODAL_WIDTH_PX = `${MODAL_WIDTH}px`;
|
||||
export const MODAL_HEIGHT_PX = `${MODAL_HEIGHT}px`;
|
||||
|
||||
export const TUTORIAL_HOST = process.env.TUTORIAL_HOST!;
|
||||
export const TUTORIAL_GROUP = process.env.TUTORIAL_GROUP!;
|
||||
export const TUTORIAL_CHAT = process.env.TUTORIAL_CHAT!;
|
||||
export const TUTORIAL_BOOK = process.env.TUTORIAL_BOOK!;
|
||||
export const TUTORIAL_LINKS = process.env.TUTORIAL_LINKS!;
|
||||
|
||||
interface StepDetail {
|
||||
title: string;
|
||||
description: string;
|
||||
url: string;
|
||||
alignX: AlignX | AlignX[];
|
||||
alignY: AlignY | AlignY[];
|
||||
offsetX: number;
|
||||
offsetY: number;
|
||||
}
|
||||
|
||||
export function hasTutorialGroup(props: { associations: Associations }) {
|
||||
return (
|
||||
`/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}` in props.associations.groups
|
||||
);
|
||||
}
|
||||
|
||||
export const progressDetails: Record<TutorialProgress, StepDetail> = {
|
||||
hidden: {} as any,
|
||||
done: {
|
||||
title: "End",
|
||||
description:
|
||||
"This tutorial is finished. Would you like to leave Beginner Island?",
|
||||
url: "/",
|
||||
alignX: "right",
|
||||
alignY: "top",
|
||||
offsetX: MODAL_WIDTH + 8,
|
||||
offsetY: 0,
|
||||
},
|
||||
start: {
|
||||
title: "New group added",
|
||||
description:
|
||||
"We just added you to the Beginner island group to show you around. This group is public, but other groups can be private",
|
||||
url: "/",
|
||||
alignX: "right",
|
||||
alignY: "top",
|
||||
offsetX: MODAL_WIDTH + 8,
|
||||
offsetY: 0,
|
||||
},
|
||||
"group-desc": {
|
||||
title: "What's a group",
|
||||
description:
|
||||
"A group contains members and tends to be centered around a topic or multiple topics.",
|
||||
url: `/~landscape/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}`,
|
||||
alignX: "left",
|
||||
alignY: "top",
|
||||
offsetX: MODAL_WIDTH + 8,
|
||||
offsetY: MODAL_HEIGHT / 2 - 8,
|
||||
},
|
||||
channels: {
|
||||
title: "Channels",
|
||||
description:
|
||||
"Inside a group you have three types of Channels: Chat, Collection, or Notebook. Mix and match these depending on your group context!",
|
||||
url: `/~landscape/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}`,
|
||||
alignY: "top",
|
||||
alignX: "right",
|
||||
offsetX: MODAL_WIDTH + 8,
|
||||
offsetY: -8,
|
||||
},
|
||||
chat: {
|
||||
title: "Chat",
|
||||
description:
|
||||
"Chat channels are for messaging within your group. Direct Messages are also supported, and are accessible from the “DMs” tile on the homescreen",
|
||||
url: `/~landscape/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}/resource/chat/ship/${TUTORIAL_HOST}/${TUTORIAL_CHAT}`,
|
||||
alignY: "top",
|
||||
alignX: "right",
|
||||
offsetX: 0,
|
||||
offsetY: -32,
|
||||
},
|
||||
link: {
|
||||
title: "Collection",
|
||||
description:
|
||||
"A collection is where you can share and view links, images, and other media within your group. Every item in a Collection can have it’s own comment thread.",
|
||||
url: `/~landscape/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}/resource/link/ship/${TUTORIAL_HOST}/${TUTORIAL_LINKS}`,
|
||||
alignY: "top",
|
||||
alignX: "right",
|
||||
offsetX: 0,
|
||||
offsetY: -32,
|
||||
},
|
||||
publish: {
|
||||
title: "Notebook",
|
||||
description:
|
||||
"Notebooks are for creating long-form content within your group. Use markdown to create rich posts with headers, lists and images.",
|
||||
url: `/~landscape/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}/resource/publish/ship/${TUTORIAL_HOST}/${TUTORIAL_BOOK}`,
|
||||
alignY: "top",
|
||||
alignX: "right",
|
||||
offsetX: 0,
|
||||
offsetY: -32,
|
||||
},
|
||||
notifications: {
|
||||
title: "Notifications",
|
||||
description:
|
||||
"Subscribing to a channel will send you notifications when there are new updates. You will also receive a notification when someone mentions your name in a channel.",
|
||||
url: `/~landscape/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}/resource/publish/ship/${TUTORIAL_HOST}/${TUTORIAL_BOOK}/settings#notifications`,
|
||||
alignY: "top",
|
||||
alignX: "right",
|
||||
offsetX: 0,
|
||||
offsetY: -32,
|
||||
},
|
||||
profile: {
|
||||
title: "Profile",
|
||||
description:
|
||||
"Your profile is customizable and can be shared with other ships. Enter as much or as little information as you’d like.",
|
||||
url: `/~profile/~${window.ship}`,
|
||||
alignY: "top",
|
||||
alignX: "right",
|
||||
offsetX: -300 + MODAL_WIDTH / 2,
|
||||
offsetY: -120 + MODAL_HEIGHT / 2,
|
||||
},
|
||||
leap: {
|
||||
title: "Leap",
|
||||
description:
|
||||
"Leap allows you to go to a specific channel, message, collection, profile or group simply by typing in a command or selecting a shortcut from the dropdown menu.",
|
||||
url: `/~profile/~${window.ship}`,
|
||||
alignY: "top",
|
||||
alignX: "left",
|
||||
offsetX: 0,
|
||||
offsetY: -32,
|
||||
},
|
||||
};
|
@ -60,6 +60,8 @@ export function useModal(props: UseModalProps): UseModalResult {
|
||||
display="flex"
|
||||
alignItems="stretch"
|
||||
flexDirection="column"
|
||||
spacing="2"
|
||||
|
||||
>
|
||||
{inner}
|
||||
</ModalOverlay>
|
||||
|
@ -1,13 +1,21 @@
|
||||
import React, { ReactNode } from "react";
|
||||
import f from 'lodash/fp';
|
||||
import create, { State } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import produce from 'immer';
|
||||
import { BackgroundConfig, RemoteContentPolicy } from "~/types/local-update";
|
||||
import { BackgroundConfig, RemoteContentPolicy, TutorialProgress, tutorialProgress } from "~/types/local-update";
|
||||
|
||||
|
||||
export interface LocalState extends State {
|
||||
hideAvatars: boolean;
|
||||
hideNicknames: boolean;
|
||||
remoteContentPolicy: RemoteContentPolicy;
|
||||
tutorialProgress: TutorialProgress;
|
||||
tutorialRef: HTMLElement | null,
|
||||
hideTutorial: () => void;
|
||||
nextTutStep: () => void;
|
||||
prevTutStep: () => void;
|
||||
setTutorialRef: (el: HTMLElement | null) => void;
|
||||
dark: boolean;
|
||||
background: BackgroundConfig;
|
||||
omniboxShown: boolean;
|
||||
@ -15,12 +23,35 @@ export interface LocalState extends State {
|
||||
toggleOmnibox: () => void;
|
||||
set: (fn: (state: LocalState) => void) => void
|
||||
};
|
||||
export const selectLocalState =
|
||||
<K extends keyof LocalState>(keys: K[]) => f.pick<LocalState, K>(keys);
|
||||
|
||||
const useLocalState = create<LocalState>(persist((set, get) => ({
|
||||
dark: false,
|
||||
background: undefined,
|
||||
hideAvatars: false,
|
||||
hideNicknames: false,
|
||||
tutorialProgress: 'hidden',
|
||||
tutorialRef: null,
|
||||
setTutorialRef: (el: HTMLElement | null) => set(produce(state => {
|
||||
state.tutorialRef = el;
|
||||
})),
|
||||
hideTutorial: () => set(produce(state => {
|
||||
state.tutorialProgress = 'hidden';
|
||||
state.tutorialRef = null;
|
||||
})),
|
||||
nextTutStep: () => set(produce(state => {
|
||||
const currIdx = tutorialProgress.findIndex(p => p === state.tutorialProgress)
|
||||
if(currIdx < tutorialProgress.length) {
|
||||
state.tutorialProgress = tutorialProgress[currIdx + 1];
|
||||
}
|
||||
})),
|
||||
prevTutStep: () => set(produce(state => {
|
||||
const currIdx = tutorialProgress.findIndex(p => p === state.tutorialProgress)
|
||||
if(currIdx > 0) {
|
||||
state.tutorialProgress = tutorialProgress[currIdx - 1];
|
||||
}
|
||||
})),
|
||||
remoteContentPolicy: {
|
||||
imageShown: true,
|
||||
audioShown: true,
|
||||
@ -41,7 +72,10 @@ const useLocalState = create<LocalState>(persist((set, get) => ({
|
||||
})),
|
||||
set: fn => set(produce(fn))
|
||||
}), {
|
||||
blacklist: ['suspendedFocus', 'toggleOmnibox', 'omniboxShown'],
|
||||
blacklist: [
|
||||
'suspendedFocus', 'toggleOmnibox', 'omniboxShown', 'tutorialProgress',
|
||||
'prevTutStep', 'nextTutStep', 'tutorialRef', 'setTutorialRef'
|
||||
],
|
||||
name: 'localReducer'
|
||||
}));
|
||||
|
||||
|
@ -1,3 +1,6 @@
|
||||
export const tutorialProgress = ['hidden', 'start', 'group-desc', 'channels', 'chat', 'link', 'publish', 'notifications', 'profile', 'leap', 'done'] as const;
|
||||
|
||||
export type TutorialProgress = typeof tutorialProgress[number];
|
||||
interface LocalUpdateSetDark {
|
||||
setDark: boolean;
|
||||
}
|
||||
@ -48,4 +51,4 @@ export type LocalUpdate =
|
||||
| LocalUpdateHideAvatars
|
||||
| LocalUpdateHideNicknames
|
||||
| LocalUpdateSetOmniboxShown
|
||||
| RemoteContentPolicy;
|
||||
| RemoteContentPolicy;
|
||||
|
@ -20,6 +20,7 @@ import { Content } from './landscape/components/Content';
|
||||
import StatusBar from './components/StatusBar';
|
||||
import Omnibox from './components/leap/Omnibox';
|
||||
import ErrorBoundary from '~/views/components/ErrorBoundary';
|
||||
import { TutorialModal } from '~/views/landscape/components/TutorialModal';
|
||||
|
||||
import GlobalStore from '~/logic/store/store';
|
||||
import GlobalSubscription from '~/logic/subscription/global';
|
||||
@ -149,6 +150,7 @@ class App extends React.Component {
|
||||
</Helmet>
|
||||
<Root background={background}>
|
||||
<Router>
|
||||
<TutorialModal api={this.api} />
|
||||
<ErrorBoundary>
|
||||
<StatusBarWithRouter
|
||||
props={this.props}
|
||||
|
@ -1,19 +1,33 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import f from 'lodash/fp';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { Box, Row, Icon, Text } from '@tlon/indigo-react';
|
||||
import { Col, Button, Box, Row, Icon, Text } from '@tlon/indigo-react';
|
||||
|
||||
import './css/custom.css';
|
||||
|
||||
import Tiles from './components/tiles';
|
||||
import Tile from './components/tiles/tile';
|
||||
import Welcome from './components/welcome';
|
||||
import Groups from './components/Groups';
|
||||
import ModalButton from './components/ModalButton';
|
||||
import { StatelessAsyncButton } from '~/views/components/StatelessAsyncButton';
|
||||
import { writeText } from '~/logic/lib/util';
|
||||
import { useModal } from "~/logic/lib/useModal";
|
||||
import { NewGroup } from "~/views/landscape/components/NewGroup";
|
||||
import { JoinGroup } from "~/views/landscape/components/JoinGroup";
|
||||
import { Helmet } from 'react-helmet';
|
||||
import useLocalState from "~/logic/state/local";
|
||||
import { useWaitForProps } from '~/logic/lib/useWaitForProps';
|
||||
import {
|
||||
hasTutorialGroup,
|
||||
TUTORIAL_GROUP,
|
||||
TUTORIAL_HOST,
|
||||
TUTORIAL_BOOK,
|
||||
TUTORIAL_CHAT,
|
||||
TUTORIAL_LINKS
|
||||
} from '~/logic/lib/tutorialModal';
|
||||
|
||||
const ScrollbarLessBox = styled(Box)`
|
||||
scrollbar-width: none !important;
|
||||
@ -23,7 +37,10 @@ const ScrollbarLessBox = styled(Box)`
|
||||
}
|
||||
`;
|
||||
|
||||
const tutSelector = f.pick(['tutorialProgress', 'nextTutStep']);
|
||||
|
||||
export default function LaunchApp(props) {
|
||||
const history = useHistory();
|
||||
const [hashText, setHashText] = useState(props.baseHash);
|
||||
const hashBox = (
|
||||
<Box
|
||||
@ -51,13 +68,68 @@ export default function LaunchApp(props) {
|
||||
<Text color="gray">{hashText || props.baseHash}</Text>
|
||||
</Box>
|
||||
);
|
||||
|
||||
const { tutorialProgress, nextTutStep } = useLocalState(tutSelector);
|
||||
|
||||
const waiter = useWaitForProps(props);
|
||||
|
||||
const { modal, showModal } = useModal({
|
||||
modal: (dismiss) => {
|
||||
const onDismiss = (e) => {
|
||||
e.stopPropagation();
|
||||
props.api.settings.putEntry('tutorial', 'seen', true);
|
||||
dismiss();
|
||||
};
|
||||
const onContinue = async (e) => {
|
||||
e.stopPropagation();
|
||||
if(!hasTutorialGroup(props)) {
|
||||
await props.api.groups.join(TUTORIAL_HOST, TUTORIAL_GROUP);
|
||||
await waiter(hasTutorialGroup);
|
||||
await Promise.all(
|
||||
[TUTORIAL_BOOK, TUTORIAL_CHAT, TUTORIAL_LINKS].map(graph =>
|
||||
api.graph.join(TUTORIAL_HOST, graph)));
|
||||
|
||||
await waiter(p => {
|
||||
return `/ship/${TUTORIAL_HOST}/${TUTORIAL_CHAT}` in p.associations.graph &&
|
||||
`/ship/${TUTORIAL_HOST}/${TUTORIAL_BOOK}` in p.associations.graph &&
|
||||
`/ship/${TUTORIAL_HOST}/${TUTORIAL_LINKS}` in p.associations.graph;
|
||||
});
|
||||
}
|
||||
nextTutStep();
|
||||
dismiss();
|
||||
}
|
||||
return (
|
||||
<Col gapY="2" p="3">
|
||||
<Text lineHeight="tall" fontWeight="medium">Welcome</Text>
|
||||
<Text lineHeight="tall">
|
||||
You have been invited to use Landscape, an interface to chat
|
||||
and interact with communities
|
||||
<br />
|
||||
Would you like a tour of Landscape?
|
||||
</Text>
|
||||
<Row gapX="2" justifyContent="flex-end">
|
||||
<Button onClick={onDismiss}>Skip</Button>
|
||||
<StatelessAsyncButton primary onClick={onContinue}>
|
||||
Yes
|
||||
</StatelessAsyncButton>
|
||||
</Row>
|
||||
</Col>
|
||||
)}
|
||||
});
|
||||
useEffect(() => {
|
||||
const seenTutorial = _.get(props.settings, ['tutorial', 'seen'], true);
|
||||
if(!seenTutorial && tutorialProgress === 'hidden') {
|
||||
showModal();
|
||||
}
|
||||
}, [props.settings]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet defer={false}>
|
||||
<title>{ props.notificationsCount ? `(${String(props.notificationsCount) }) `: '' }Landscape</title>
|
||||
</Helmet>
|
||||
<ScrollbarLessBox height='100%' overflowY='scroll' display="flex" flexDirection="column">
|
||||
<Welcome firstTime={props.launch.firstTime} api={props.api} />
|
||||
{modal}
|
||||
<Box
|
||||
mx='2'
|
||||
display='grid'
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, {useRef} from "react";
|
||||
import { Box, Text, Col } from "@tlon/indigo-react";
|
||||
import f from "lodash/fp";
|
||||
import _ from "lodash";
|
||||
@ -7,6 +7,8 @@ import { Associations, Association, Unreads, UnreadStats } from "~/types";
|
||||
import { alphabeticalOrder } from "~/logic/lib/util";
|
||||
import { getUnreadCount, getNotificationCount } from "~/logic/lib/hark";
|
||||
import Tile from "../components/tiles/tile";
|
||||
import { useTutorialModal } from "~/views/components/useTutorialModal";
|
||||
import {TUTORIAL_HOST, TUTORIAL_GROUP} from "~/logic/lib/tutorialModal";
|
||||
|
||||
interface GroupsProps {
|
||||
associations: Associations;
|
||||
@ -73,8 +75,15 @@ interface GroupProps {
|
||||
}
|
||||
function Group(props: GroupProps) {
|
||||
const { path, title, unreads, updates, first = false } = props;
|
||||
const anchorRef = useRef<HTMLElement>(null);
|
||||
const isTutorialGroup = path === `/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}`;
|
||||
useTutorialModal(
|
||||
'start',
|
||||
isTutorialGroup,
|
||||
anchorRef.current
|
||||
);
|
||||
return (
|
||||
<Tile to={`/~landscape${path}`} gridColumnStart={first ? '1' : null}>
|
||||
<Tile ref={anchorRef} position="relative" bg={isTutorialGroup ? 'lightBlue' : undefined} to={`/~landscape${path}`} gridColumnStart={first ? '1' : null}>
|
||||
<Col height="100%" justifyContent="space-between">
|
||||
<Text>{title}</Text>
|
||||
<Col>
|
||||
|
@ -22,43 +22,45 @@ const SquareBox = styled(Box)`
|
||||
`;
|
||||
const routeList = defaultApps.map(a => `/~${a}`);
|
||||
|
||||
export default class Tile extends React.Component {
|
||||
render() {
|
||||
const { bg, to, href, p, boxShadow, gridColumnStart, ...props } = this.props;
|
||||
const Tile = React.forwardRef((props, ref) => {
|
||||
const { bg, to, href, p, boxShadow, gridColumnStart, ...rest } = props;
|
||||
|
||||
let childElement = (
|
||||
<Box p={typeof p === 'undefined' ? 2 : p} width="100%" height="100%">
|
||||
{props.children}
|
||||
</Box>
|
||||
);
|
||||
|
||||
if (to) {
|
||||
if (routeList.indexOf(to) !== -1 || to === '/~profile' || to.startsWith('/~landscape/')) {
|
||||
childElement= (<Link to={to}>{childElement}</Link>);
|
||||
} else {
|
||||
childElement= (<a href={to}>{childElement}</a>);
|
||||
}
|
||||
let childElement = (
|
||||
<Box p={typeof p === 'undefined' ? 2 : p} width="100%" height="100%">
|
||||
{props.children}
|
||||
</Box>
|
||||
);
|
||||
|
||||
if (to) {
|
||||
if (routeList.indexOf(to) !== -1 || to === '/~profile' || to.startsWith('/~landscape/')) {
|
||||
childElement= (<Link to={to}>{childElement}</Link>);
|
||||
} else {
|
||||
childElement= (<a href={to}>{childElement}</a>);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<SquareBox
|
||||
borderRadius={2}
|
||||
overflow="hidden"
|
||||
bg={bg || "white"}
|
||||
color={props?.color || 'washedGray'}
|
||||
boxShadow={boxShadow || '0 0 0px 1px inset'}
|
||||
style={{ gridColumnStart }}
|
||||
>
|
||||
<Box
|
||||
{...props}
|
||||
height="100%"
|
||||
width="100%"
|
||||
>
|
||||
{childElement}
|
||||
</Box>
|
||||
</SquareBox>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<SquareBox
|
||||
ref={ref}
|
||||
position="relative"
|
||||
borderRadius={2}
|
||||
overflow="hidden"
|
||||
bg={bg || "white"}
|
||||
color={props?.color || 'washedGray'}
|
||||
boxShadow={boxShadow || '0 0 0px 1px inset'}
|
||||
style={{ gridColumnStart }}
|
||||
>
|
||||
<Box
|
||||
{...rest}
|
||||
height="100%"
|
||||
width="100%"
|
||||
>
|
||||
{childElement}
|
||||
</Box>
|
||||
</SquareBox>
|
||||
);
|
||||
});
|
||||
|
||||
export default Tile;
|
||||
|
@ -1,49 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Box, Text } from '@tlon/indigo-react';
|
||||
|
||||
export default class Welcome extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
show: true
|
||||
};
|
||||
this.disableWelcome = this.disableWelcome.bind(this);
|
||||
}
|
||||
|
||||
disableWelcome() {
|
||||
this.props.api.launch.changeFirstTime(false);
|
||||
this.setState({ show: false });
|
||||
}
|
||||
|
||||
render() {
|
||||
const firstTime = this.props.firstTime;
|
||||
return (firstTime && this.state.show) ? (
|
||||
<Box
|
||||
bg='white'
|
||||
border={1}
|
||||
margin={3}
|
||||
padding={3}
|
||||
display='flex'
|
||||
flexDirection='column'
|
||||
alignItems='flex-start'
|
||||
>
|
||||
<Text>Welcome. This virtual computer belongs to you completely. The Urbit ID you used to boot it is yours as well.</Text>
|
||||
<Text pt={2}>Since your ID and OS belong to you, it’s up to you to keep them safe. Be sure your ID is somewhere you won’t lose it and you keep your OS on a machine you trust.</Text>
|
||||
<Text pt={2}>Urbit OS is designed to keep your data secure and hard to lose. But the system is still young — so don’t put anything critical in here just yet.</Text>
|
||||
<Text pt={2}>To begin exploring, you should probably pop into a chat and verify there are signs of life in this new place. If you were invited by a friend, you probably already have access to a few groups.</Text>
|
||||
<Text pt={2}>If you don't know where to go, feel free to <Link className="no-underline bb b--black b--gray1-d dib" to="/~landscape/join/~bitbet-bolbel/urbit-community">join the Urbit Community group</Link>.
|
||||
</Text>
|
||||
<Text pt={2}>Have fun!</Text>
|
||||
<Text pt={2} className='pointer bb'
|
||||
onClick={(() => {
|
||||
this.disableWelcome();
|
||||
})}
|
||||
>
|
||||
Close this note
|
||||
</Text>
|
||||
</Box>
|
||||
) : null;
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, {useEffect} from "react";
|
||||
import React, {useEffect, useRef} from "react";
|
||||
import { Sigil } from "~/logic/lib/sigil";
|
||||
import { ViewProfile } from './ViewProfile';
|
||||
import { EditProfile } from './EditProfile';
|
||||
@ -15,6 +15,7 @@ import {
|
||||
} from "@tlon/indigo-react";
|
||||
import useLocalState from "~/logic/state/local";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import {useTutorialModal} from "~/views/components/useTutorialModal";
|
||||
|
||||
|
||||
export function Profile(props: any) {
|
||||
@ -43,6 +44,10 @@ export function Profile(props: any) {
|
||||
? <BaseImage src={contact.avatar} width='100%' height='100%' style={{ objectFit: 'cover' }} />
|
||||
: <Sigil padding={24} ship={ship} size={128} color={hexColor} />;
|
||||
|
||||
const anchorRef = useRef<HTMLElement | null>(null);
|
||||
|
||||
useTutorialModal('profile', ship === `~${window.ship}`, anchorRef.current);
|
||||
|
||||
return (
|
||||
<Center
|
||||
p={4}
|
||||
@ -55,7 +60,7 @@ export function Profile(props: any) {
|
||||
<SetStatus ship={ship} contact={contact} api={props.api} />
|
||||
) : null
|
||||
}
|
||||
<Row width="100%" height="300px">
|
||||
<Row ref={anchorRef} width="100%" height="300px">
|
||||
{cover}
|
||||
</Row>
|
||||
<Row
|
||||
|
@ -11,9 +11,7 @@ import { Box, Col } from "@tlon/indigo-react";
|
||||
import { useOutsideClick } from "~/logic/lib/useOutsideClick";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { Portal } from "./Portal";
|
||||
|
||||
type AlignY = "top" | "bottom";
|
||||
type AlignX = "left" | "right";
|
||||
import { getRelativePosition, AlignY, AlignX } from "~/logic/lib/relativePosition";
|
||||
|
||||
interface DropdownProps {
|
||||
children: ReactNode;
|
||||
@ -44,46 +42,11 @@ export function Dropdown(props: DropdownProps) {
|
||||
const [coords, setCoords] = useState({});
|
||||
|
||||
const updatePos = useCallback(() => {
|
||||
const rect = anchorRef.current?.getBoundingClientRect();
|
||||
if (rect) {
|
||||
const bounds = {
|
||||
top: rect.top,
|
||||
left: rect.left,
|
||||
bottom: document.documentElement.clientHeight - rect.bottom,
|
||||
right: document.documentElement.clientWidth - rect.right,
|
||||
};
|
||||
const alignX = _.isArray(props.alignX) ? props.alignX : [props.alignX];
|
||||
const alignY = _.isArray(props.alignY) ? props.alignY : [props.alignY];
|
||||
|
||||
let newCoords = {
|
||||
..._.reduce(
|
||||
alignX,
|
||||
(acc, a, idx) => ({
|
||||
...acc,
|
||||
[a]: _.zipWith(
|
||||
[...Array(idx), `${bounds[a]}px`],
|
||||
acc[a] || [],
|
||||
(a, b) => a || b || null
|
||||
),
|
||||
}),
|
||||
{}
|
||||
),
|
||||
..._.reduce(
|
||||
alignY,
|
||||
(acc, a, idx) => ({
|
||||
...acc,
|
||||
[a]: _.zipWith(
|
||||
[...Array(idx), `${bounds[a]}px`],
|
||||
acc[a] || [],
|
||||
(a, b) => a || b || null
|
||||
),
|
||||
}),
|
||||
{}
|
||||
),
|
||||
};
|
||||
const newCoords = getRelativePosition(anchorRef.current, props.alignX, props.alignY);
|
||||
if(newCoords) {
|
||||
setCoords(newCoords);
|
||||
}
|
||||
}, [setCoords, anchorRef.current]);
|
||||
}, [setCoords, anchorRef.current, props.alignY, props.alignX]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
|
@ -21,12 +21,12 @@ interface HoverBoxLinkProps {
|
||||
to: string;
|
||||
}
|
||||
|
||||
export const HoverBoxLink = ({
|
||||
export const HoverBoxLink = React.forwardRef(({
|
||||
to,
|
||||
children,
|
||||
...rest
|
||||
}: HoverBoxLinkProps & PropFunc<typeof HoverBox>) => (
|
||||
<Link to={to}>
|
||||
}: HoverBoxLinkProps & PropFunc<typeof HoverBox>, ref) => (
|
||||
<Link ref={ref} to={to}>
|
||||
<HoverBox {...rest}>{children}</HoverBox>
|
||||
</Link>
|
||||
);
|
||||
));
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React, {
|
||||
useState,
|
||||
useEffect
|
||||
useEffect,
|
||||
useRef
|
||||
} from 'react';
|
||||
|
||||
import {
|
||||
@ -18,6 +19,7 @@ import { StatusBarItem } from './StatusBarItem';
|
||||
import { Sigil } from '~/logic/lib/sigil';
|
||||
import { uxToHex } from "~/logic/lib/util";
|
||||
import { SetStatusBarModal } from './SetStatusBarModal';
|
||||
import { useTutorialModal } from './useTutorialModal';
|
||||
|
||||
import useLocalState from '~/logic/state/local';
|
||||
|
||||
@ -43,6 +45,10 @@ const StatusBar = (props) => {
|
||||
style={{ objectFit: 'cover' }} />
|
||||
) : <Sigil ship={ship} size={16} color={color} icon />;
|
||||
|
||||
const anchorRef = useRef(null);
|
||||
|
||||
useTutorialModal('leap', true, anchorRef.current);
|
||||
|
||||
return (
|
||||
<Box
|
||||
display='grid'
|
||||
@ -64,7 +70,7 @@ const StatusBar = (props) => {
|
||||
</Box>
|
||||
)}
|
||||
<Icon icon='LeapArrow'/>
|
||||
<Text ml={2} color='black'>
|
||||
<Text ref={anchorRef} ml={2} color='black'>
|
||||
Leap
|
||||
</Text>
|
||||
<Text display={['none', 'inline']} ml={2} color='gray'>
|
||||
|
21
pkg/interface/src/views/components/useTutorialModal.tsx
Normal file
21
pkg/interface/src/views/components/useTutorialModal.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { useEffect } from "react";
|
||||
import { TutorialProgress } from "~/types";
|
||||
import useLocalState, { selectLocalState } from "~/logic/state/local";
|
||||
|
||||
const localSelector = selectLocalState(["tutorialProgress", "setTutorialRef"]);
|
||||
|
||||
export function useTutorialModal(
|
||||
onProgress: TutorialProgress,
|
||||
show: boolean,
|
||||
anchorRef: HTMLElement | null
|
||||
) {
|
||||
const { tutorialProgress, setTutorialRef } = useLocalState(localSelector);
|
||||
|
||||
useEffect(() => {
|
||||
if (show && onProgress === tutorialProgress && anchorRef) {
|
||||
setTutorialRef(anchorRef);
|
||||
}
|
||||
}, [onProgress, tutorialProgress, show, anchorRef]);
|
||||
|
||||
return show && onProgress === tutorialProgress;
|
||||
}
|
@ -1,8 +1,9 @@
|
||||
import React from "react";
|
||||
import React, {useRef} from "react";
|
||||
import { Col, Text, BaseLabel, Label } from "@tlon/indigo-react";
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
import { Association, NotificationGraphConfig } from "~/types";
|
||||
import { StatelessAsyncToggle } from "~/views/components/StatelessAsyncToggle";
|
||||
import {useTutorialModal} from "~/views/components/useTutorialModal";
|
||||
|
||||
interface ChannelNotificationsProps {
|
||||
api: GlobalApi;
|
||||
@ -24,9 +25,13 @@ export function ChannelNotifications(props: ChannelNotificationsProps) {
|
||||
await api.hark[func](rid, "/");
|
||||
};
|
||||
|
||||
const anchorRef = useRef<HTMLElement | null>(null)
|
||||
|
||||
useTutorialModal('notifications', true, anchorRef.current);
|
||||
|
||||
return (
|
||||
<Col mb="6" gapY="4" flexShrink={0}>
|
||||
<Text id="notifications" fontSize="2" fontWeight="bold">
|
||||
<Text ref={anchorRef} id="notifications" fontSize="2" fontWeight="bold">
|
||||
Channel Notifications
|
||||
</Text>
|
||||
<BaseLabel display="flex" cursor="pointer">
|
||||
|
@ -1,19 +1,28 @@
|
||||
import React, { ReactNode } from "react";
|
||||
import React, { ReactNode, useRef } from "react";
|
||||
import { Metadata } from "~/types";
|
||||
import { Col, Row, Text } from "@tlon/indigo-react";
|
||||
import { MetadataIcon } from "./MetadataIcon";
|
||||
import { useTutorialModal } from "~/views/components/useTutorialModal";
|
||||
import {TUTORIAL_HOST, TUTORIAL_GROUP} from "~/logic/lib/tutorialModal";
|
||||
|
||||
interface GroupSummaryProps {
|
||||
metadata: Metadata;
|
||||
memberCount: number;
|
||||
channelCount: number;
|
||||
resource?: string;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export function GroupSummary(props: GroupSummaryProps) {
|
||||
const { channelCount, memberCount, metadata, children } = props;
|
||||
const { channelCount, memberCount, metadata, resource, children } = props;
|
||||
const anchorRef = useRef<HTMLElement | null>(null);
|
||||
useTutorialModal(
|
||||
"group-desc",
|
||||
resource === `/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}`,
|
||||
anchorRef.current
|
||||
);
|
||||
return (
|
||||
<Col maxWidth="300px" gapY="4">
|
||||
<Col ref={anchorRef} maxWidth="300px" gapY="4">
|
||||
<Row gapX="2">
|
||||
<MetadataIcon
|
||||
borderRadius="1"
|
||||
|
@ -199,6 +199,7 @@ export function GroupsPane(props: GroupsPaneProps) {
|
||||
memberCount={memberCount}
|
||||
channelCount={0}
|
||||
metadata={groupAssociation.metadata}
|
||||
resource={groupAssociation.group}
|
||||
/>
|
||||
} else {
|
||||
summary = (<Box p="4"><Text fontSize="0" color='gray'>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import React, { ReactNode, useRef } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
Col
|
||||
@ -19,6 +19,7 @@ import { getGroupFromWorkspace } from '~/logic/lib/workspace';
|
||||
import { SidebarAppConfigs } from './types';
|
||||
import { SidebarList } from './SidebarList';
|
||||
import { roleForShip } from '~/logic/lib/group';
|
||||
import {useTutorialModal} from '~/views/components/useTutorialModal';
|
||||
|
||||
const ScrollbarLessCol = styled(Col)`
|
||||
scrollbar-width: none !important;
|
||||
@ -64,8 +65,12 @@ export function Sidebar(props: SidebarProps) {
|
||||
const role = props.groups?.[groupPath] ? roleForShip(props.groups[groupPath], window.ship) : undefined;
|
||||
const isAdmin = (role === 'admin') || (workspace?.type === 'home');
|
||||
|
||||
const anchorRef = useRef<HTMLElement | null>(null);
|
||||
useTutorialModal('channels', true, anchorRef.current);
|
||||
|
||||
return (
|
||||
<ScrollbarLessCol
|
||||
ref={anchorRef}
|
||||
display={display}
|
||||
width="100%"
|
||||
gridRow="1/2"
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, {useRef} from "react";
|
||||
import _ from 'lodash';
|
||||
|
||||
import { Icon, Row, Box, Text, BaseImage } from "@tlon/indigo-react";
|
||||
@ -9,6 +9,8 @@ import { Groups, Association } from "~/types";
|
||||
import { Sigil } from '~/logic/lib/sigil';
|
||||
import urbitOb from 'urbit-ob';
|
||||
import { getModuleIcon, getItemTitle, uxToHex } from "~/logic/lib/util";
|
||||
import {useTutorialModal} from "~/views/components/useTutorialModal";
|
||||
import {TUTORIAL_HOST, TUTORIAL_GROUP} from "~/logic/lib/tutorialModal";
|
||||
|
||||
function SidebarItemIndicator(props: { status?: SidebarItemStatus }) {
|
||||
switch (props.status) {
|
||||
@ -41,6 +43,12 @@ export function SidebarItem(props: {
|
||||
const mod = association?.metadata?.module || appName;
|
||||
const rid = association?.resource
|
||||
const groupPath = association?.group;
|
||||
const anchorRef = useRef<HTMLElement | null>(null)
|
||||
useTutorialModal(
|
||||
mod as any,
|
||||
groupPath === `/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}`,
|
||||
anchorRef.current
|
||||
);
|
||||
const app = apps[appName];
|
||||
const isUnmanaged = groups?.[groupPath]?.hidden || false;
|
||||
if (!app) {
|
||||
@ -87,6 +95,7 @@ export function SidebarItem(props: {
|
||||
|
||||
return (
|
||||
<HoverBoxLink
|
||||
ref={anchorRef}
|
||||
to={to}
|
||||
bg="white"
|
||||
bgActive="washedGray"
|
||||
|
189
pkg/interface/src/views/landscape/components/TutorialModal.tsx
Normal file
189
pkg/interface/src/views/landscape/components/TutorialModal.tsx
Normal file
@ -0,0 +1,189 @@
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { Box, Col, Row, Button, Text, Icon, Action } from "@tlon/indigo-react";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { TutorialProgress, tutorialProgress as progress } from "~/types";
|
||||
|
||||
import { Portal } from "~/views/components/Portal";
|
||||
import useLocalState, { selectLocalState } from "~/logic/state/local";
|
||||
import {
|
||||
progressDetails,
|
||||
MODAL_HEIGHT_PX,
|
||||
MODAL_WIDTH_PX,
|
||||
MODAL_WIDTH,
|
||||
MODAL_HEIGHT,
|
||||
TUTORIAL_HOST,
|
||||
TUTORIAL_GROUP,
|
||||
} from "~/logic/lib/tutorialModal";
|
||||
import { getRelativePosition } from "~/logic/lib/relativePosition";
|
||||
import { StatelessAsyncButton } from "~/views/components/StatelessAsyncButton";
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
|
||||
const localSelector = selectLocalState([
|
||||
"tutorialProgress",
|
||||
"nextTutStep",
|
||||
"prevTutStep",
|
||||
"tutorialRef",
|
||||
"hideTutorial",
|
||||
]);
|
||||
|
||||
export function TutorialModal(props: { api: GlobalApi }) {
|
||||
const {
|
||||
tutorialProgress,
|
||||
tutorialRef,
|
||||
nextTutStep,
|
||||
prevTutStep,
|
||||
hideTutorial,
|
||||
} = useLocalState(localSelector);
|
||||
const {
|
||||
title,
|
||||
description,
|
||||
alignX,
|
||||
alignY,
|
||||
offsetX,
|
||||
offsetY,
|
||||
} = progressDetails[tutorialProgress];
|
||||
|
||||
const [coords, setCoords] = useState({});
|
||||
|
||||
const history = useHistory();
|
||||
|
||||
const next = useCallback(
|
||||
(e: React.MouseEvent<HTMLElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
const idx = progress.findIndex((p) => p === tutorialProgress);
|
||||
const { url } = progressDetails[progress[idx + 1]];
|
||||
history.push(url);
|
||||
nextTutStep();
|
||||
},
|
||||
[nextTutStep, history, tutorialProgress, setCoords]
|
||||
);
|
||||
const prev = useCallback(() => {
|
||||
const idx = progress.findIndex((p) => p === tutorialProgress);
|
||||
history.push(progressDetails[progress[idx - 1]].url);
|
||||
prevTutStep();
|
||||
}, [prevTutStep, history, tutorialProgress]);
|
||||
|
||||
const updatePos = useCallback(() => {
|
||||
const newCoords = getRelativePosition(
|
||||
tutorialRef,
|
||||
alignX,
|
||||
alignY,
|
||||
offsetX,
|
||||
offsetY
|
||||
);
|
||||
const withMobile: any = _.mapValues(newCoords, (value: string[], key: string) => {
|
||||
if(key === 'bottom' || key === 'left') {
|
||||
return ['0px', ...value];
|
||||
}
|
||||
return [null, ...value];
|
||||
});
|
||||
if(!('bottom' in withMobile)) {
|
||||
withMobile.bottom = ['0px', null];
|
||||
}
|
||||
if(!('left' in withMobile)) {
|
||||
withMobile.left = ['0px', null];
|
||||
}
|
||||
|
||||
if (newCoords) {
|
||||
setCoords(withMobile);
|
||||
}
|
||||
}, [tutorialRef]);
|
||||
|
||||
const dismiss = useCallback(() => {
|
||||
hideTutorial();
|
||||
props.api.settings.putEntry("tutorial", "seen", true);
|
||||
}, [hideTutorial, props.api]);
|
||||
|
||||
const leaveGroup = useCallback(async () => {
|
||||
await props.api.groups.leaveGroup(TUTORIAL_HOST, TUTORIAL_GROUP);
|
||||
}, [props.api]);
|
||||
|
||||
const progressIdx = progress.findIndex((p) => p === tutorialProgress);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
tutorialProgress !== "hidden" &&
|
||||
tutorialProgress !== "done" &&
|
||||
tutorialRef
|
||||
) {
|
||||
const interval = setInterval(updatePos, 100);
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}
|
||||
return () => {};
|
||||
}, [tutorialRef, tutorialProgress, updatePos]);
|
||||
|
||||
// manually center final window
|
||||
useEffect(() => {
|
||||
if (tutorialProgress === "done") {
|
||||
const { innerWidth, innerHeight } = window;
|
||||
const left = ["0px", `${(innerWidth - MODAL_WIDTH) / 2}px`];
|
||||
const top = [null, `${(innerHeight - MODAL_HEIGHT) / 2}px`];
|
||||
const bottom = ["0px", null];
|
||||
setCoords({ top, left, bottom });
|
||||
}
|
||||
}, [tutorialProgress]);
|
||||
|
||||
if (tutorialProgress === "hidden") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Box
|
||||
position="fixed"
|
||||
{...coords}
|
||||
bg="white"
|
||||
zIndex={50}
|
||||
height={MODAL_HEIGHT_PX}
|
||||
width={["100%", MODAL_WIDTH_PX]}
|
||||
borderRadius="2"
|
||||
>
|
||||
<Col
|
||||
position="relative"
|
||||
justifyContent="space-between"
|
||||
height="100%"
|
||||
width="100%"
|
||||
borderRadius="2"
|
||||
p="2"
|
||||
bg="lightBlue"
|
||||
>
|
||||
<Box
|
||||
right="8px"
|
||||
top="8px"
|
||||
position="absolute"
|
||||
cursor="pointer"
|
||||
onClick={dismiss}
|
||||
>
|
||||
<Icon icon="X" />
|
||||
</Box>
|
||||
<Text lineHeight="tall" fontWeight="medium">
|
||||
{title}
|
||||
</Text>
|
||||
<Text lineHeight="tall">{description}</Text>
|
||||
{tutorialProgress !== "done" ? (
|
||||
<Row justifyContent="space-between">
|
||||
<Action bg="transparent" onClick={prev}>
|
||||
<Icon icon="ArrowWest" />
|
||||
</Action>
|
||||
<Text>
|
||||
{progressIdx}/{progress.length - 1}
|
||||
</Text>
|
||||
<Action bg="transparent" onClick={next}>
|
||||
<Icon icon="ArrowEast" />
|
||||
</Action>
|
||||
</Row>
|
||||
) : (
|
||||
<Row justifyContent="space-between">
|
||||
<StatelessAsyncButton primary onClick={leaveGroup}>
|
||||
Leave Group
|
||||
</StatelessAsyncButton>
|
||||
<Button onClick={dismiss}>Later</Button>
|
||||
</Row>
|
||||
)}
|
||||
</Col>
|
||||
</Box>
|
||||
</Portal>
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue
Block a user