Summary: A new onboarding page is now shown to all new users visiting the doc menu for the first time. Tutorial cards on the doc menu have been replaced with a new version that tracks completion progress, alongside a new card that opens the orientation video. Test Plan: Browser tests. Reviewers: jarek Reviewed By: jarek Differential Revision: https://phab.getgrist.com/D4296pull/1118/head
@ -0,0 +1,232 @@
|
|||||||
|
import {makeT} from 'app/client/lib/localization';
|
||||||
|
import {urlState} from 'app/client/models/gristUrlState';
|
||||||
|
import {HomeModel} from 'app/client/models/HomeModel';
|
||||||
|
import {openVideoTour} from 'app/client/ui/OpenVideoTour';
|
||||||
|
import {bigPrimaryButtonLink} from 'app/client/ui2018/buttons';
|
||||||
|
import {colors, theme} from 'app/client/ui2018/cssVars';
|
||||||
|
import {icon} from 'app/client/ui2018/icons';
|
||||||
|
import {isFeatureEnabled} from 'app/common/gristUrls';
|
||||||
|
import {getGristConfig} from 'app/common/urlUtils';
|
||||||
|
import {Computed, dom, IDisposableOwner, makeTestId, styled, subscribeElem} from 'grainjs';
|
||||||
|
|
||||||
|
interface BuildOnboardingCardsOptions {
|
||||||
|
homeModel: HomeModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
const t = makeT('OnboardingCards');
|
||||||
|
|
||||||
|
const testId = makeTestId('test-onboarding-');
|
||||||
|
|
||||||
|
export function buildOnboardingCards(
|
||||||
|
owner: IDisposableOwner,
|
||||||
|
{homeModel}: BuildOnboardingCardsOptions
|
||||||
|
) {
|
||||||
|
const {templateOrg, onboardingTutorialDocId} = getGristConfig();
|
||||||
|
if (!isFeatureEnabled('tutorials') || !templateOrg || !onboardingTutorialDocId) { return null; }
|
||||||
|
|
||||||
|
const percentComplete = Computed.create(owner, (use) => {
|
||||||
|
if (!homeModel.app.currentValidUser) { return 0; }
|
||||||
|
|
||||||
|
const tutorial = use(homeModel.onboardingTutorial);
|
||||||
|
if (!tutorial) { return undefined; }
|
||||||
|
|
||||||
|
return tutorial.forks?.[0]?.options?.tutorial?.percentComplete ?? 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
const shouldShowCards = Computed.create(owner, (use) =>
|
||||||
|
!use(homeModel.app.dismissedPopups).includes('onboardingCards'));
|
||||||
|
|
||||||
|
let videoPlayButtonElement: HTMLElement;
|
||||||
|
|
||||||
|
return dom.maybe(shouldShowCards, () =>
|
||||||
|
cssOnboardingCards(
|
||||||
|
cssTutorialCard(
|
||||||
|
cssDismissCardsButton(
|
||||||
|
icon('CrossBig'),
|
||||||
|
dom.on('click', () => homeModel.app.dismissPopup('onboardingCards', true)),
|
||||||
|
testId('dismiss-cards'),
|
||||||
|
),
|
||||||
|
cssTutorialCardHeader(
|
||||||
|
t('Complete our basics tutorial'),
|
||||||
|
),
|
||||||
|
cssTutorialCardSubHeader(
|
||||||
|
t('Learn the basic of reference columns, linked widgets, column types, & cards.')
|
||||||
|
),
|
||||||
|
cssTutorialCardBody(
|
||||||
|
cssTutorialProgress(
|
||||||
|
cssTutorialProgressText(
|
||||||
|
cssProgressPercentage(
|
||||||
|
dom.domComputed(percentComplete, (percent) => percent !== undefined ? `${percent}%` : null),
|
||||||
|
testId('tutorial-percent-complete'),
|
||||||
|
),
|
||||||
|
cssStarIcon('Star'),
|
||||||
|
),
|
||||||
|
cssTutorialProgressBar(
|
||||||
|
(elem) => subscribeElem(elem, percentComplete, (val) => {
|
||||||
|
elem.style.setProperty('--percent-complete', String(val ?? 0));
|
||||||
|
})
|
||||||
|
),
|
||||||
|
),
|
||||||
|
bigPrimaryButtonLink(
|
||||||
|
t('Complete the tutorial'),
|
||||||
|
urlState().setLinkUrl({org: templateOrg, doc: onboardingTutorialDocId}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
testId('tutorial-card'),
|
||||||
|
),
|
||||||
|
cssVideoCard(
|
||||||
|
cssVideoThumbnail(
|
||||||
|
cssVideoThumbnailSpacer(),
|
||||||
|
videoPlayButtonElement = cssVideoPlayButton(
|
||||||
|
cssPlayIcon('VideoPlay2'),
|
||||||
|
),
|
||||||
|
cssVideoThumbnailText(t('3 minute video tour')),
|
||||||
|
),
|
||||||
|
dom.on('click', () => openVideoTour(videoPlayButtonElement)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cssOnboardingCards = styled('div', `
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(220px, max-content));
|
||||||
|
gap: 24px;
|
||||||
|
margin: 24px 0;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssTutorialCard = styled('div', `
|
||||||
|
position: relative;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: ${theme.announcementPopupFg};
|
||||||
|
background-color: ${theme.announcementPopupBg};
|
||||||
|
padding: 16px 24px;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssTutorialCardHeader = styled('div', `
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 18px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssDismissCardsButton = styled('div', `
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
--icon-color: ${theme.popupCloseButtonFg};
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: ${theme.hover};
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssTutorialCardSubHeader = styled('div', `
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
margin: 8px 0;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssTutorialCardBody = styled('div', `
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 24px;
|
||||||
|
margin: 16px 0;
|
||||||
|
align-items: end;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssTutorialProgress = styled('div', `
|
||||||
|
flex: auto;
|
||||||
|
min-width: 120px;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssTutorialProgressText = styled('div', `
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssProgressPercentage = styled('div', `
|
||||||
|
font-size: 20px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssStarIcon = styled(icon, `
|
||||||
|
--icon-color: ${theme.accentIcon};
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssTutorialProgressBar = styled('div', `
|
||||||
|
margin-top: 4px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: ${theme.mainPanelBg};
|
||||||
|
--percent-complete: 0;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
border-radius: 8px;
|
||||||
|
background: ${theme.progressBarFg};
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
width: calc((var(--percent-complete) / 100) * 100%);
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssVideoCard = styled('div', `
|
||||||
|
width: 220px;
|
||||||
|
height: 158px;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssVideoThumbnail = styled('div', `
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 36px 32px;
|
||||||
|
background-image: url("img/youtube-screenshot.png");
|
||||||
|
background-color: rgba(0, 0, 0, 0.4);
|
||||||
|
background-blend-mode: multiply;
|
||||||
|
background-size: cover;
|
||||||
|
transform: scale(1.2);
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssVideoThumbnailSpacer = styled('div', ``);
|
||||||
|
|
||||||
|
const cssVideoPlayButton = styled('div', `
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
align-self: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
background-color: ${theme.controlPrimaryBg};
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
.${cssVideoThumbnail.className}:hover & {
|
||||||
|
background-color: ${theme.controlPrimaryHoverBg};
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssPlayIcon = styled(icon, `
|
||||||
|
--icon-color: ${theme.controlPrimaryFg};
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssVideoThumbnailText = styled('div', `
|
||||||
|
color: ${colors.light};
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: center;
|
||||||
|
`);
|
@ -0,0 +1,747 @@
|
|||||||
|
import {FocusLayer} from 'app/client/lib/FocusLayer';
|
||||||
|
import {makeT} from 'app/client/lib/localization';
|
||||||
|
import {AppModel} from 'app/client/models/AppModel';
|
||||||
|
import {logError} from 'app/client/models/errors';
|
||||||
|
import {getMainOrgUrl, urlState} from 'app/client/models/gristUrlState';
|
||||||
|
import {getUserPrefObs} from 'app/client/models/UserPrefs';
|
||||||
|
import {textInput} from 'app/client/ui/inputs';
|
||||||
|
import {PlayerState, YouTubePlayer} from 'app/client/ui/YouTubePlayer';
|
||||||
|
import {bigBasicButton, bigPrimaryButton, bigPrimaryButtonLink} from 'app/client/ui2018/buttons';
|
||||||
|
import {colors, mediaMedium, mediaXSmall, theme} from 'app/client/ui2018/cssVars';
|
||||||
|
import {icon} from 'app/client/ui2018/icons';
|
||||||
|
import {IconName} from 'app/client/ui2018/IconList';
|
||||||
|
import {modal} from 'app/client/ui2018/modals';
|
||||||
|
import {BaseAPI} from 'app/common/BaseAPI';
|
||||||
|
import {getPageTitleSuffix, ONBOARDING_VIDEO_YOUTUBE_EMBED_ID} from 'app/common/gristUrls';
|
||||||
|
import {UserPrefs} from 'app/common/Prefs';
|
||||||
|
import {getGristConfig} from 'app/common/urlUtils';
|
||||||
|
import {
|
||||||
|
Computed,
|
||||||
|
Disposable,
|
||||||
|
dom,
|
||||||
|
DomContents,
|
||||||
|
IDisposableOwner,
|
||||||
|
input,
|
||||||
|
makeTestId,
|
||||||
|
Observable,
|
||||||
|
styled,
|
||||||
|
subscribeElem,
|
||||||
|
} from 'grainjs';
|
||||||
|
|
||||||
|
const t = makeT('OnboardingPage');
|
||||||
|
|
||||||
|
const testId = makeTestId('test-onboarding-');
|
||||||
|
|
||||||
|
const choices: Array<{icon: IconName, color: string, textKey: string}> = [
|
||||||
|
{icon: 'UseProduct', color: `${colors.lightGreen}`, textKey: 'Product Development' },
|
||||||
|
{icon: 'UseFinance', color: '#0075A2', textKey: 'Finance & Accounting'},
|
||||||
|
{icon: 'UseMedia', color: '#F7B32B', textKey: 'Media Production' },
|
||||||
|
{icon: 'UseMonitor', color: '#F2545B', textKey: 'IT & Technology' },
|
||||||
|
{icon: 'UseChart', color: '#7141F9', textKey: 'Marketing' },
|
||||||
|
{icon: 'UseScience', color: '#231942', textKey: 'Research' },
|
||||||
|
{icon: 'UseSales', color: '#885A5A', textKey: 'Sales' },
|
||||||
|
{icon: 'UseEducate', color: '#4A5899', textKey: 'Education' },
|
||||||
|
{icon: 'UseHr', color: '#688047', textKey: 'HR & Management' },
|
||||||
|
{icon: 'UseOther', color: '#929299', textKey: 'Other' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function shouldShowOnboardingPage(userPrefsObs: Observable<UserPrefs>): boolean {
|
||||||
|
return Boolean(getGristConfig().survey && userPrefsObs.get()?.showNewUserQuestions);
|
||||||
|
}
|
||||||
|
|
||||||
|
type IncrementStep = (delta?: 1 | -1) => void;
|
||||||
|
|
||||||
|
interface Step {
|
||||||
|
state?: QuestionsState | VideoState;
|
||||||
|
buildDom(): DomContents;
|
||||||
|
onNavigateAway?(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface QuestionsState {
|
||||||
|
organization: Observable<string>;
|
||||||
|
role: Observable<string>;
|
||||||
|
useCases: Array<Observable<boolean>>;
|
||||||
|
useOther: Observable<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VideoState {
|
||||||
|
watched: Observable<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class OnboardingPage extends Disposable {
|
||||||
|
private _steps: Array<Step>;
|
||||||
|
private _stepIndex: Observable<number> = Observable.create(this, 0);
|
||||||
|
|
||||||
|
constructor(private _appModel: AppModel) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.autoDispose(this._stepIndex.addListener((_, prevIndex) => {
|
||||||
|
this._steps[prevIndex].onNavigateAway?.();
|
||||||
|
}));
|
||||||
|
|
||||||
|
const incrementStep: IncrementStep = (delta: -1 | 1 = 1) => {
|
||||||
|
this._stepIndex.set(this._stepIndex.get() + delta);
|
||||||
|
};
|
||||||
|
|
||||||
|
this._steps = [
|
||||||
|
{
|
||||||
|
state: {
|
||||||
|
organization: Observable.create(this, ''),
|
||||||
|
role: Observable.create(this, ''),
|
||||||
|
useCases: choices.map(() => Observable.create(this, false)),
|
||||||
|
useOther: Observable.create(this, ''),
|
||||||
|
},
|
||||||
|
buildDom() { return dom.create(buildQuestions, incrementStep, this.state as QuestionsState); },
|
||||||
|
onNavigateAway() { saveQuestions(this.state as QuestionsState); },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
state: {
|
||||||
|
watched: Observable.create(this, false),
|
||||||
|
},
|
||||||
|
buildDom() { return dom.create(buildVideo, incrementStep, this.state as VideoState); },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
buildDom() { return dom.create(buildTutorial, incrementStep); },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
document.title = `Welcome${getPageTitleSuffix(getGristConfig())}`;
|
||||||
|
|
||||||
|
getUserPrefObs(this._appModel.userPrefsObs, 'showNewUserQuestions').set(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
public buildDom() {
|
||||||
|
return cssPageContainer(
|
||||||
|
cssOnboardingPage(
|
||||||
|
cssSidebar(
|
||||||
|
cssSidebarContent(
|
||||||
|
cssSidebarHeading1(t('Welcome')),
|
||||||
|
cssSidebarHeading2(this._appModel.currentUser!.name + '!'),
|
||||||
|
testId('sidebar'),
|
||||||
|
),
|
||||||
|
cssGetStarted(
|
||||||
|
cssGetStartedImg({src: 'img/get-started.png'}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
cssMainPanel(
|
||||||
|
buildStepper(this._steps, this._stepIndex),
|
||||||
|
dom.domComputed(this._stepIndex, index => {
|
||||||
|
return this._steps[index].buildDom();
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
testId('page'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildStepper(steps: Step[], stepIndex: Observable<number>) {
|
||||||
|
return cssStepper(
|
||||||
|
steps.map((_, i) =>
|
||||||
|
cssStep(
|
||||||
|
cssStepCircle(
|
||||||
|
cssStepCircle.cls('-done', use => (i < use(stepIndex))),
|
||||||
|
dom.domComputed(use => i < use(stepIndex), (done) => done ? icon('Tick') : String(i + 1)),
|
||||||
|
cssStepCircle.cls('-current', use => (i === use(stepIndex))),
|
||||||
|
dom.on('click', () => { stepIndex.set(i); }),
|
||||||
|
testId(`step-${i + 1}`)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveQuestions(state: QuestionsState) {
|
||||||
|
const {organization, role, useCases, useOther} = state;
|
||||||
|
if (!organization.get() && !role.get() && !useCases.map(useCase => useCase.get()).includes(true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const org_name = organization.get();
|
||||||
|
const org_role = role.get();
|
||||||
|
const use_cases = choices.filter((c, i) => useCases[i].get()).map(c => c.textKey);
|
||||||
|
const use_other = use_cases.includes('Other') ? useOther.get() : '';
|
||||||
|
const submitUrl = new URL(window.location.href);
|
||||||
|
submitUrl.pathname = '/welcome/info';
|
||||||
|
BaseAPI.request(submitUrl.href, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({org_name, org_role, use_cases, use_other})
|
||||||
|
}).catch((e) => logError(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildQuestions(owner: IDisposableOwner, incrementStep: IncrementStep, state: QuestionsState) {
|
||||||
|
const {organization, role, useCases, useOther} = state;
|
||||||
|
const isFilled = Computed.create(owner, (use) => {
|
||||||
|
return Boolean(use(organization) || use(role) || useCases.map(useCase => use(useCase)).includes(true));
|
||||||
|
});
|
||||||
|
|
||||||
|
return cssQuestions(
|
||||||
|
cssHeading(t("Tell us who you are")),
|
||||||
|
cssQuestion(
|
||||||
|
cssFieldHeading(t('What organization are you with?')),
|
||||||
|
cssInput(
|
||||||
|
organization,
|
||||||
|
{type: 'text', placeholder: t('Your organization')},
|
||||||
|
testId('questions-organization'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
cssQuestion(
|
||||||
|
cssFieldHeading(t('What is your role?')),
|
||||||
|
cssInput(
|
||||||
|
role,
|
||||||
|
{type: 'text', placeholder: t('Your role')},
|
||||||
|
testId('questions-role'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
cssQuestion(
|
||||||
|
cssFieldHeading(t("What brings you to Grist (you can select multiple)?")),
|
||||||
|
cssUseCases(
|
||||||
|
choices.map((item, i) => cssUseCase(
|
||||||
|
cssUseCaseIcon(icon(item.icon)),
|
||||||
|
cssUseCase.cls('-selected', useCases[i]),
|
||||||
|
dom.on('click', () => useCases[i].set(!useCases[i].get())),
|
||||||
|
(item.icon !== 'UseOther' ?
|
||||||
|
t(item.textKey) :
|
||||||
|
[
|
||||||
|
cssOtherLabel(t(item.textKey)),
|
||||||
|
cssOtherInput(useOther, {}, {type: 'text', placeholder: t("Type here")},
|
||||||
|
// The following subscribes to changes to selection observable, and focuses the input when
|
||||||
|
// this item is selected.
|
||||||
|
(elem) => subscribeElem(elem, useCases[i], val => val && setTimeout(() => elem.focus(), 0)),
|
||||||
|
// It's annoying if clicking into the input toggles selection; better to turn that
|
||||||
|
// off (user can click icon to deselect).
|
||||||
|
dom.on('click', ev => ev.stopPropagation()),
|
||||||
|
// Similarly, ignore Enter/Escape in "Other" textbox, so that they don't submit/close the form.
|
||||||
|
dom.onKeyDown({
|
||||||
|
Enter: (ev, elem) => elem.blur(),
|
||||||
|
Escape: (ev, elem) => elem.blur(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
),
|
||||||
|
testId('questions-use-case'),
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
cssContinue(
|
||||||
|
bigPrimaryButton(
|
||||||
|
t('Next step'),
|
||||||
|
dom.show(isFilled),
|
||||||
|
dom.on('click', () => incrementStep()),
|
||||||
|
testId('next-step'),
|
||||||
|
),
|
||||||
|
bigBasicButton(
|
||||||
|
t('Skip step'),
|
||||||
|
dom.hide(isFilled),
|
||||||
|
dom.on('click', () => incrementStep()),
|
||||||
|
testId('skip-step'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
testId('questions'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildVideo(_owner: IDisposableOwner, incrementStep: IncrementStep, state: VideoState) {
|
||||||
|
const {watched} = state;
|
||||||
|
|
||||||
|
function onPlay() {
|
||||||
|
watched.set(true);
|
||||||
|
|
||||||
|
return modal((ctl, modalOwner) => {
|
||||||
|
const youtubePlayer = YouTubePlayer.create(modalOwner,
|
||||||
|
ONBOARDING_VIDEO_YOUTUBE_EMBED_ID,
|
||||||
|
{
|
||||||
|
onPlayerReady: (player) => player.playVideo(),
|
||||||
|
onPlayerStateChange(_player, {data}) {
|
||||||
|
if (data !== PlayerState.Ended) { return; }
|
||||||
|
|
||||||
|
ctl.close();
|
||||||
|
},
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
origin: getMainOrgUrl(),
|
||||||
|
},
|
||||||
|
cssYouTubePlayer.cls(''),
|
||||||
|
);
|
||||||
|
|
||||||
|
return [
|
||||||
|
dom.on('click', () => ctl.close()),
|
||||||
|
elem => { FocusLayer.create(modalOwner, {defaultFocusElem: elem, pauseMousetrap: true}); },
|
||||||
|
dom.onKeyDown({
|
||||||
|
Escape: () => ctl.close(),
|
||||||
|
' ': () => youtubePlayer.playPause(),
|
||||||
|
}),
|
||||||
|
cssModalHeader(
|
||||||
|
cssModalCloseButton(
|
||||||
|
cssCloseIcon('CrossBig'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
cssModalBody(
|
||||||
|
cssVideoPlayer(
|
||||||
|
dom.on('click', (ev) => ev.stopPropagation()),
|
||||||
|
youtubePlayer.buildDom(),
|
||||||
|
testId('video-player'),
|
||||||
|
),
|
||||||
|
cssModalButtons(
|
||||||
|
bigPrimaryButton(
|
||||||
|
t('Next step'),
|
||||||
|
dom.on('click', (ev) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
ctl.close();
|
||||||
|
incrementStep();
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
cssVideoPlayerModal.cls(''),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return dom('div',
|
||||||
|
cssHeading(t('Discover Grist in 3 minutes')),
|
||||||
|
cssScreenshot(
|
||||||
|
dom.on('click', onPlay),
|
||||||
|
dom('div',
|
||||||
|
cssScreenshotImg({src: 'img/youtube-screenshot.png'}),
|
||||||
|
cssActionOverlay(
|
||||||
|
cssAction(
|
||||||
|
cssRoundButton(cssVideoPlayIcon('VideoPlay')),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
testId('video-thumbnail'),
|
||||||
|
),
|
||||||
|
cssContinue(
|
||||||
|
cssBackButton(
|
||||||
|
t('Back'),
|
||||||
|
dom.on('click', () => incrementStep(-1)),
|
||||||
|
testId('back'),
|
||||||
|
),
|
||||||
|
bigPrimaryButton(
|
||||||
|
t('Next step'),
|
||||||
|
dom.show(watched),
|
||||||
|
dom.on('click', () => incrementStep()),
|
||||||
|
testId('next-step'),
|
||||||
|
),
|
||||||
|
bigBasicButton(
|
||||||
|
t('Skip step'),
|
||||||
|
dom.hide(watched),
|
||||||
|
dom.on('click', () => incrementStep()),
|
||||||
|
testId('skip-step'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
testId('video'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTutorial(_owner: IDisposableOwner, incrementStep: IncrementStep) {
|
||||||
|
const {templateOrg, onboardingTutorialDocId} = getGristConfig();
|
||||||
|
return dom('div',
|
||||||
|
cssHeading(
|
||||||
|
t('Go hands-on with the Grist Basics tutorial'),
|
||||||
|
cssSubHeading(
|
||||||
|
t("Grist may look like a spreadsheet, but it doesn't always "
|
||||||
|
+ "act like one. Discover what makes Grist different."
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
cssTutorial(
|
||||||
|
cssScreenshot(
|
||||||
|
dom.on('click', () => urlState().pushUrl({org: templateOrg!, doc: onboardingTutorialDocId})),
|
||||||
|
cssTutorialScreenshotImg({src: 'img/tutorial-screenshot.png'}),
|
||||||
|
cssTutorialOverlay(
|
||||||
|
cssAction(
|
||||||
|
cssTutorialButton(t('Go to the tutorial!')),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
testId('tutorial-thumbnail'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
cssContinue(
|
||||||
|
cssBackButton(
|
||||||
|
t('Back'),
|
||||||
|
dom.on('click', () => incrementStep(-1)),
|
||||||
|
testId('back'),
|
||||||
|
),
|
||||||
|
bigBasicButton(
|
||||||
|
t('Skip tutorial'),
|
||||||
|
dom.on('click', () => window.location.href = urlState().makeUrl(urlState().state.get())),
|
||||||
|
testId('skip-tutorial'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
testId('tutorial'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cssPageContainer = styled('div', `
|
||||||
|
overflow: auto;
|
||||||
|
height: 100%;
|
||||||
|
background-color: ${theme.mainPanelBg};
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssOnboardingPage = styled('div', `
|
||||||
|
display: flex;
|
||||||
|
min-height: 100%;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssSidebar = styled('div', `
|
||||||
|
width: 460px;
|
||||||
|
background-color: ${colors.lightGreen};
|
||||||
|
color: ${colors.light};
|
||||||
|
background-image:
|
||||||
|
linear-gradient(to bottom, rgb(41, 185, 131) 32px, transparent 32px),
|
||||||
|
linear-gradient(to right, rgb(41, 185, 131) 32px, transparent 32px);
|
||||||
|
background-size: 240px 120px;
|
||||||
|
background-position: 0 0, 40%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
@media ${mediaMedium} {
|
||||||
|
& {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssGetStarted = styled('div', `
|
||||||
|
width: 500px;
|
||||||
|
height: 350px;
|
||||||
|
margin: auto -77px 0 37px;
|
||||||
|
overflow: hidden;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssGetStartedImg = styled('img', `
|
||||||
|
display: block;
|
||||||
|
width: 500px;
|
||||||
|
height: auto;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssSidebarContent = styled('div', `
|
||||||
|
line-height: 32px;
|
||||||
|
margin: 112px 16px 64px 16px;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 48px;
|
||||||
|
font-weight: 500;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssSidebarHeading1 = styled('div', `
|
||||||
|
font-size: 32px;
|
||||||
|
text-align: center;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssSidebarHeading2 = styled('div', `
|
||||||
|
font-size: 28px;
|
||||||
|
text-align: center;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssMainPanel = styled('div', `
|
||||||
|
margin: 56px auto;
|
||||||
|
padding: 0px 96px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
@media ${mediaMedium} {
|
||||||
|
& {
|
||||||
|
padding: 0px 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssHeading = styled('div', `
|
||||||
|
color: ${theme.text};
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin: 32px 0px;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssSubHeading = styled(cssHeading, `
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 400;
|
||||||
|
margin-top: 16px;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssStep = styled('div', `
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: default;
|
||||||
|
|
||||||
|
&:not(:last-child)::after {
|
||||||
|
content: "";
|
||||||
|
width: 50px;
|
||||||
|
height: 2px;
|
||||||
|
background-color: var(--grist-color-light-green);
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssStepCircle = styled('div', `
|
||||||
|
--icon-color: ${theme.controlPrimaryFg};
|
||||||
|
--step-color: ${theme.controlPrimaryBg};
|
||||||
|
display: inline-block;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 30px;
|
||||||
|
border: 1px solid var(--step-color);
|
||||||
|
color: var(--step-color);
|
||||||
|
margin: 4px;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
--step-color: ${theme.controlPrimaryHoverBg};
|
||||||
|
}
|
||||||
|
&-current {
|
||||||
|
background-color: var(--step-color);
|
||||||
|
color: ${theme.controlPrimaryFg};
|
||||||
|
outline: 3px solid ${theme.cursorInactive};
|
||||||
|
}
|
||||||
|
&-done {
|
||||||
|
background-color: var(--step-color);
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssQuestions = styled('div', `
|
||||||
|
max-width: 500px;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssQuestion = styled('div', `
|
||||||
|
margin: 16px 0 8px 0;
|
||||||
|
text-align: left;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssFieldHeading = styled('div', `
|
||||||
|
color: ${theme.text};
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssContinue = styled('div', `
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 40px;
|
||||||
|
gap: 16px;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssUseCases = styled('div', `
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
margin: -8px -4px;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssUseCase = styled('div', `
|
||||||
|
flex: 1 0 40%;
|
||||||
|
min-width: 200px;
|
||||||
|
margin: 8px 4px 0 4px;
|
||||||
|
height: 40px;
|
||||||
|
border: 1px solid ${theme.inputBorder};
|
||||||
|
border-radius: 3px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
color: ${theme.text};
|
||||||
|
--icon-color: ${theme.accentIcon};
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: ${theme.hover};
|
||||||
|
}
|
||||||
|
&-selected {
|
||||||
|
border: 2px solid ${theme.controlFg};
|
||||||
|
}
|
||||||
|
&-selected:hover {
|
||||||
|
border: 2px solid ${theme.controlHoverFg};
|
||||||
|
}
|
||||||
|
&-selected:focus-within {
|
||||||
|
box-shadow: 0 0 2px 0px ${theme.controlFg};
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssUseCaseIcon = styled('div', `
|
||||||
|
margin: 0 16px;
|
||||||
|
--icon-color: ${theme.accentIcon};
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssOtherLabel = styled('div', `
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
.${cssUseCase.className}-selected & {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssInput = styled(textInput, `
|
||||||
|
height: 40px;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssOtherInput = styled(input, `
|
||||||
|
color: ${theme.inputFg};
|
||||||
|
display: none;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
outline: none;
|
||||||
|
padding: 0px;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: ${theme.inputPlaceholderFg};
|
||||||
|
}
|
||||||
|
.${cssUseCase.className}-selected & {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssTutorial = styled('div', `
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssScreenshot = styled('div', `
|
||||||
|
max-width: 720px;
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 3px solid ${colors.lightGreen};
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssActionOverlay = styled('div', `
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.20);
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssTutorialOverlay = styled(cssActionOverlay, `
|
||||||
|
background-color: transparent;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssAction = styled('div', `
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: auto;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssVideoPlayIcon = styled(icon, `
|
||||||
|
--icon-color: ${colors.light};
|
||||||
|
width: 38px;
|
||||||
|
height: 33.25px;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssCloseIcon = styled(icon, `
|
||||||
|
--icon-color: ${colors.light};
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssYouTubePlayer = styled('iframe', `
|
||||||
|
border-radius: 4px;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssModalHeader = styled('div', `
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
justify-content: flex-end;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssModalBody = styled('div', `
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssBackButton = styled(bigBasicButton, `
|
||||||
|
border: none;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssModalButtons = styled('div', `
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 24px;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssVideoPlayer = styled('div', `
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1280px;
|
||||||
|
height: 100%;
|
||||||
|
max-height: 720px;
|
||||||
|
|
||||||
|
@media ${mediaXSmall} {
|
||||||
|
& {
|
||||||
|
max-height: 240px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssVideoPlayerModal = styled('div', `
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
background-color: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssModalCloseButton = styled('div', `
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: ${theme.hover};
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssScreenshotImg = styled('img', `
|
||||||
|
transform: scale(1.2);
|
||||||
|
width: 100%;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssTutorialScreenshotImg = styled('img', `
|
||||||
|
width: 100%;
|
||||||
|
opacity: 0.4;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssRoundButton = styled('div', `
|
||||||
|
width: 75px;
|
||||||
|
height: 75px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-radius: 100px;
|
||||||
|
background: ${colors.lightGreen};
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
--icon-color: var(--light, #FFF);
|
||||||
|
|
||||||
|
.${cssScreenshot.className}:hover & {
|
||||||
|
background: ${colors.darkGreen};
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssStepper = styled('div', `
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 20px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssTutorialButton = styled(bigPrimaryButtonLink, `
|
||||||
|
.${cssScreenshot.className}:hover & {
|
||||||
|
background-color: ${theme.controlPrimaryHoverBg};
|
||||||
|
border-color: ${theme.controlPrimaryHoverBg};
|
||||||
|
}
|
||||||
|
`);
|
@ -1,217 +0,0 @@
|
|||||||
import {AppModel} from 'app/client/models/AppModel';
|
|
||||||
import {bigPrimaryButton} from 'app/client/ui2018/buttons';
|
|
||||||
import {isNarrowScreenObs, theme} from 'app/client/ui2018/cssVars';
|
|
||||||
import {icon} from 'app/client/ui2018/icons';
|
|
||||||
import {commonUrls, isFeatureEnabled} from 'app/common/gristUrls';
|
|
||||||
import {Computed, dom, IDisposableOwner, makeTestId, styled} from 'grainjs';
|
|
||||||
|
|
||||||
const testId = makeTestId('test-tutorial-card-');
|
|
||||||
|
|
||||||
interface Options {
|
|
||||||
app: AppModel,
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildTutorialCard(owner: IDisposableOwner, options: Options) {
|
|
||||||
if (!isFeatureEnabled('tutorials')) { return null; }
|
|
||||||
|
|
||||||
const {app} = options;
|
|
||||||
function onClose() {
|
|
||||||
app.dismissPopup('tutorialFirstCard', true);
|
|
||||||
}
|
|
||||||
const visible = Computed.create(owner, (use) =>
|
|
||||||
!use(app.dismissedPopups).includes('tutorialFirstCard')
|
|
||||||
&& !use(isNarrowScreenObs())
|
|
||||||
);
|
|
||||||
return dom.maybe(visible, () => {
|
|
||||||
return cssCard(
|
|
||||||
cssCaption(
|
|
||||||
dom('div', cssNewToGrist("New to Grist?")),
|
|
||||||
cssRelative(
|
|
||||||
cssStartHere("Start here."),
|
|
||||||
cssArrow()
|
|
||||||
),
|
|
||||||
),
|
|
||||||
cssContent(
|
|
||||||
testId('content'),
|
|
||||||
cssImage({src: commonUrls.basicTutorialImage}),
|
|
||||||
cssCardText(
|
|
||||||
cssLine(cssTitle("Grist Basics Tutorial")),
|
|
||||||
cssLine("Learn the basics of reference columns, linked widgets, column types, & cards."),
|
|
||||||
cssLine(cssSub('Beginner - 10 mins')),
|
|
||||||
cssButtonWrapper(
|
|
||||||
cssButtonWrapper.cls('-small'),
|
|
||||||
cssHeroButton("Start Tutorial"),
|
|
||||||
{href: commonUrls.basicTutorial, target: '_blank'},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
cssButtonWrapper(
|
|
||||||
cssButtonWrapper.cls('-big'),
|
|
||||||
cssHeroButton("Start Tutorial"),
|
|
||||||
{href: commonUrls.basicTutorial, target: '_blank'},
|
|
||||||
),
|
|
||||||
cssCloseButton(icon('CrossBig'), dom.on('click', () => onClose?.()), testId('close')),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const cssContent = styled('div', `
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
padding-top: 24px;
|
|
||||||
padding-bottom: 20px;
|
|
||||||
padding-right: 20px;
|
|
||||||
max-width: 460px;
|
|
||||||
`);
|
|
||||||
|
|
||||||
const cssCardText = styled('div', `
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-self: stretch;
|
|
||||||
margin-left: 12px;
|
|
||||||
`);
|
|
||||||
|
|
||||||
const cssRelative = styled('div', `
|
|
||||||
position: relative;
|
|
||||||
`);
|
|
||||||
|
|
||||||
const cssNewToGrist = styled('span', `
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
font-size: 24px;
|
|
||||||
line-height: 16px;
|
|
||||||
letter-spacing: 0.2px;
|
|
||||||
white-space: nowrap;
|
|
||||||
`);
|
|
||||||
|
|
||||||
const cssStartHere = styled('span', `
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 24px;
|
|
||||||
line-height: 16px;
|
|
||||||
letter-spacing: 0.2px;
|
|
||||||
white-space: nowrap;
|
|
||||||
`);
|
|
||||||
|
|
||||||
const cssCaption = styled('div', `
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
margin-left: 32px;
|
|
||||||
margin-top: 42px;
|
|
||||||
margin-right: 64px;
|
|
||||||
`);
|
|
||||||
|
|
||||||
const cssTitle = styled('span', `
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 20px;
|
|
||||||
`);
|
|
||||||
|
|
||||||
const cssSub = styled('span', `
|
|
||||||
font-size: 12px;
|
|
||||||
color: ${theme.lightText};
|
|
||||||
`);
|
|
||||||
|
|
||||||
const cssLine = styled('div', `
|
|
||||||
margin-bottom: 6px;
|
|
||||||
`);
|
|
||||||
|
|
||||||
const cssHeroButton = styled(bigPrimaryButton, `
|
|
||||||
`);
|
|
||||||
|
|
||||||
const cssButtonWrapper = styled('a', `
|
|
||||||
flex-grow: 1;
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
margin-right: 60px;
|
|
||||||
align-items: center;
|
|
||||||
text-decoration: none;
|
|
||||||
&:hover {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
&-big .${cssHeroButton.className} {
|
|
||||||
padding: 16px 28px;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 20px;
|
|
||||||
line-height: 1em;
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
|
|
||||||
const cssCloseButton = styled('div', `
|
|
||||||
flex-shrink: 0;
|
|
||||||
align-self: flex-end;
|
|
||||||
cursor: pointer;
|
|
||||||
--icon-color: ${theme.controlSecondaryFg};
|
|
||||||
margin: 8px 8px 4px 0px;
|
|
||||||
padding: 2px;
|
|
||||||
border-radius: 4px;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
&:hover {
|
|
||||||
background-color: ${theme.lightHover};
|
|
||||||
}
|
|
||||||
&:active {
|
|
||||||
background-color: ${theme.hover};
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
|
|
||||||
const cssImage = styled('img', `
|
|
||||||
width: 187px;
|
|
||||||
height: 145px;
|
|
||||||
flex: none;
|
|
||||||
`);
|
|
||||||
|
|
||||||
const cssArrow = styled('div', `
|
|
||||||
position: absolute;
|
|
||||||
background-image: var(--icon-GreenArrow);
|
|
||||||
width: 94px;
|
|
||||||
height: 12px;
|
|
||||||
top: calc(50% - 6px);
|
|
||||||
left: calc(100% - 12px);
|
|
||||||
z-index: 1;
|
|
||||||
`);
|
|
||||||
|
|
||||||
|
|
||||||
const cssCard = styled('div', `
|
|
||||||
display: flex;
|
|
||||||
position: relative;
|
|
||||||
color: ${theme.text};
|
|
||||||
border-radius: 3px;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
max-width: 1000px;
|
|
||||||
box-shadow: 0 2px 18px 0 ${theme.modalInnerShadow}, 0 0 1px 0 ${theme.modalOuterShadow};
|
|
||||||
& .${cssButtonWrapper.className}-small {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
@media (max-width: 1320px) {
|
|
||||||
& .${cssButtonWrapper.className}-small {
|
|
||||||
flex-direction: column;
|
|
||||||
display: flex;
|
|
||||||
margin-top: 14px;
|
|
||||||
align-self: flex-start;
|
|
||||||
}
|
|
||||||
& .${cssButtonWrapper.className}-big {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media (max-width: 1000px) {
|
|
||||||
& .${cssArrow.className} {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
& .${cssCaption.className} {
|
|
||||||
flex-direction: row;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
& {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
& .${cssContent.className} {
|
|
||||||
padding: 12px;
|
|
||||||
max-width: 100%;
|
|
||||||
margin-bottom: 28px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`);
|
|
@ -1,176 +0,0 @@
|
|||||||
import {makeT} from 'app/client/lib/localization';
|
|
||||||
import * as commands from 'app/client/components/commands';
|
|
||||||
import {getUserPrefObs} from 'app/client/models/UserPrefs';
|
|
||||||
import {colors, testId} from 'app/client/ui2018/cssVars';
|
|
||||||
import {icon} from 'app/client/ui2018/icons';
|
|
||||||
import {IconName} from 'app/client/ui2018/IconList';
|
|
||||||
import {ISaveModalOptions, saveModal} from 'app/client/ui2018/modals';
|
|
||||||
import {BaseAPI} from 'app/common/BaseAPI';
|
|
||||||
import {UserPrefs} from 'app/common/Prefs';
|
|
||||||
import {getGristConfig} from 'app/common/urlUtils';
|
|
||||||
import {dom, input, Observable, styled, subscribeElem} from 'grainjs';
|
|
||||||
|
|
||||||
const t = makeT('WelcomeQuestions');
|
|
||||||
|
|
||||||
export function shouldShowWelcomeQuestions(userPrefsObs: Observable<UserPrefs>): boolean {
|
|
||||||
return Boolean(getGristConfig().survey && userPrefsObs.get()?.showNewUserQuestions);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows a modal with welcome questions if surveying is enabled and the user hasn't
|
|
||||||
* dismissed the modal before.
|
|
||||||
*/
|
|
||||||
export function showWelcomeQuestions(userPrefsObs: Observable<UserPrefs>) {
|
|
||||||
saveModal((ctl, owner): ISaveModalOptions => {
|
|
||||||
const selection = choices.map(c => Observable.create(owner, false));
|
|
||||||
const otherText = Observable.create(owner, '');
|
|
||||||
const showQuestions = getUserPrefObs(userPrefsObs, 'showNewUserQuestions');
|
|
||||||
|
|
||||||
async function onConfirm() {
|
|
||||||
const use_cases = choices.filter((c, i) => selection[i].get()).map(c => c.textKey);
|
|
||||||
const use_other = use_cases.includes("Other") ? otherText.get() : '';
|
|
||||||
|
|
||||||
const submitUrl = new URL(window.location.href);
|
|
||||||
submitUrl.pathname = '/welcome/info';
|
|
||||||
return BaseAPI.request(submitUrl.href,
|
|
||||||
{method: 'POST', body: JSON.stringify({use_cases, use_other})});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
owner.onDispose(async () => {
|
|
||||||
// Whichever way the modal is closed, don't show the questions again. (We set the value to
|
|
||||||
// undefined to remove it from the JSON prefs object entirely; it's never used again.)
|
|
||||||
showQuestions.set(undefined);
|
|
||||||
|
|
||||||
// Show the Grist video tour when the modal is closed.
|
|
||||||
await commands.allCommands.leftPanelOpen.run();
|
|
||||||
commands.allCommands.videoTourToolsOpen.run();
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
title: [cssLogo(), dom('div', t("Welcome to Grist!"))],
|
|
||||||
body: buildInfoForm(selection, otherText),
|
|
||||||
saveLabel: 'Start using Grist',
|
|
||||||
saveFunc: onConfirm,
|
|
||||||
hideCancel: true,
|
|
||||||
width: 'fixed-wide',
|
|
||||||
modalArgs: cssModalCentered.cls(''),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const choices: Array<{icon: IconName, color: string, textKey: string}> = [
|
|
||||||
{icon: 'UseProduct', color: `${colors.lightGreen}`, textKey: 'Product Development' },
|
|
||||||
{icon: 'UseFinance', color: '#0075A2', textKey: 'Finance & Accounting' },
|
|
||||||
{icon: 'UseMedia', color: '#F7B32B', textKey: 'Media Production' },
|
|
||||||
{icon: 'UseMonitor', color: '#F2545B', textKey: 'IT & Technology' },
|
|
||||||
{icon: 'UseChart', color: '#7141F9', textKey: 'Marketing' },
|
|
||||||
{icon: 'UseScience', color: '#231942', textKey: 'Research' },
|
|
||||||
{icon: 'UseSales', color: '#885A5A', textKey: 'Sales' },
|
|
||||||
{icon: 'UseEducate', color: '#4A5899', textKey: 'Education' },
|
|
||||||
{icon: 'UseHr', color: '#688047', textKey: 'HR & Management' },
|
|
||||||
{icon: 'UseOther', color: '#929299', textKey: 'Other' },
|
|
||||||
];
|
|
||||||
|
|
||||||
function buildInfoForm(selection: Observable<boolean>[], otherText: Observable<string>) {
|
|
||||||
return [
|
|
||||||
dom('span', t("What brings you to Grist? Please help us serve you better.")),
|
|
||||||
cssChoices(
|
|
||||||
choices.map((item, i) => cssChoice(
|
|
||||||
cssIcon(icon(item.icon), {style: `--icon-color: ${item.color}`}),
|
|
||||||
cssChoice.cls('-selected', selection[i]),
|
|
||||||
dom.on('click', () => selection[i].set(!selection[i].get())),
|
|
||||||
(item.icon !== 'UseOther' ?
|
|
||||||
t(item.textKey) :
|
|
||||||
[
|
|
||||||
cssOtherLabel(t(item.textKey)),
|
|
||||||
cssOtherInput(otherText, {}, {type: 'text', placeholder: t("Type here")},
|
|
||||||
// The following subscribes to changes to selection observable, and focuses the input when
|
|
||||||
// this item is selected.
|
|
||||||
(elem) => subscribeElem(elem, selection[i], val => val && setTimeout(() => elem.focus(), 0)),
|
|
||||||
// It's annoying if clicking into the input toggles selection; better to turn that
|
|
||||||
// off (user can click icon to deselect).
|
|
||||||
dom.on('click', ev => ev.stopPropagation()),
|
|
||||||
// Similarly, ignore Enter/Escape in "Other" textbox, so that they don't submit/close the form.
|
|
||||||
dom.onKeyDown({
|
|
||||||
Enter: (ev, elem) => elem.blur(),
|
|
||||||
Escape: (ev, elem) => elem.blur(),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
)),
|
|
||||||
testId('welcome-questions'),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
const cssModalCentered = styled('div', `
|
|
||||||
text-align: center;
|
|
||||||
`);
|
|
||||||
|
|
||||||
const cssLogo = styled('div', `
|
|
||||||
display: inline-block;
|
|
||||||
height: 48px;
|
|
||||||
width: 48px;
|
|
||||||
background-image: var(--icon-GristLogo);
|
|
||||||
background-size: 32px 32px;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: center;
|
|
||||||
`);
|
|
||||||
|
|
||||||
const cssChoices = styled('div', `
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
margin-top: 24px;
|
|
||||||
`);
|
|
||||||
|
|
||||||
const cssChoice = styled('div', `
|
|
||||||
flex: 1 0 40%;
|
|
||||||
min-width: 0px;
|
|
||||||
margin: 8px 4px 0 4px;
|
|
||||||
height: 40px;
|
|
||||||
border: 1px solid ${colors.darkGrey};
|
|
||||||
border-radius: 3px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
text-align: left;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: ${colors.lightGreen};
|
|
||||||
}
|
|
||||||
&-selected {
|
|
||||||
background-color: ${colors.mediumGrey};
|
|
||||||
}
|
|
||||||
&-selected:hover {
|
|
||||||
border-color: ${colors.darkGreen};
|
|
||||||
}
|
|
||||||
&-selected:focus-within {
|
|
||||||
box-shadow: 0 0 2px 0px var(--grist-color-cursor);
|
|
||||||
border-color: ${colors.lightGreen};
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
|
|
||||||
const cssIcon = styled('div', `
|
|
||||||
margin: 0 16px;
|
|
||||||
`);
|
|
||||||
|
|
||||||
const cssOtherLabel = styled('div', `
|
|
||||||
display: block;
|
|
||||||
.${cssChoice.className}-selected & {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
|
|
||||||
const cssOtherInput = styled(input, `
|
|
||||||
display: none;
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
outline: none;
|
|
||||||
padding: 0px;
|
|
||||||
.${cssChoice.className}-selected & {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
`);
|
|
After Width: | Height: | Size: 56 KiB |
After Width: | Height: | Size: 180 KiB |
After Width: | Height: | Size: 272 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 615 B |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 359 B |