mirror of
https://github.com/discourse/discourse.git
synced 2025-01-16 04:52:43 +08:00
FEATURE: Improve wizard quality and rearrange steps (#30055)
This commit contains various quality improvements to our site setup wizard, along with some rearrangement of steps to improve the admin setup experience and encourage admins to customize the site early to avoid "all sites look the same" sentiment. #### Step rearrangement * “Your site is ready” from 3 → 4 * “Logos” from 4 → 5 * “Look and feel” from 5 → 3 #### Font selector improvements Changes the wizard font selector dropdown to show a preview of all fonts with a CSS class so you don't have to choose the font to get a preview. Also makes the fonts appear in alphabetical order. #### Preview improvements Placeholder text changed from lorem ipsum to actual topic titles, category names, and post content. This makes it feel more "real". Fixes "undefined" categories. Added a date to the topic timeline. Fixes button rectangles and other UI elements not changing in size when the font changed, leading to cut off text which looked super messy. Also fixed some font color issues. Fixed table header alignment for Latest topic list. #### Homepage style selector improvements Limited the big list of homepage styles to Latest, Hot, Categories with latest topics, and Category boxes based on research into the most common options. #### Preview header Changed the preview header to move the hamburger to the left and add a chat icon #### And more! Changed the background of the wizard to use our branded blob style.
This commit is contained in:
parent
c2282439b3
commit
3135f472e2
|
@ -0,0 +1,73 @@
|
||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { hash } from "@ember/helper";
|
||||||
|
import { action, set } from "@ember/object";
|
||||||
|
import ColorPalettes from "select-kit/components/color-palettes";
|
||||||
|
import ComboBox from "select-kit/components/combo-box";
|
||||||
|
import FontSelector from "select-kit/components/font-selector";
|
||||||
|
import HomepageStyleSelector from "select-kit/components/homepage-style-selector";
|
||||||
|
|
||||||
|
export default class Dropdown extends Component {
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
|
||||||
|
if (this.args.field.id === "color_scheme") {
|
||||||
|
for (let choice of this.args.field.choices) {
|
||||||
|
if (choice?.data?.colors) {
|
||||||
|
set(choice, "colors", choice.data.colors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.args.field.id === "body_font") {
|
||||||
|
for (let choice of this.args.field.choices) {
|
||||||
|
set(choice, "classNames", `body-font-${choice.id.replace(/_/g, "-")}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.args.field.id === "heading_font") {
|
||||||
|
for (let choice of this.args.field.choices) {
|
||||||
|
set(
|
||||||
|
choice,
|
||||||
|
"classNames",
|
||||||
|
`heading-font-${choice.id.replace(/_/g, "-")}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get component() {
|
||||||
|
switch (this.args.field.id) {
|
||||||
|
case "color_scheme":
|
||||||
|
return ColorPalettes;
|
||||||
|
case "body_font":
|
||||||
|
case "heading_font":
|
||||||
|
return FontSelector;
|
||||||
|
case "homepage_style":
|
||||||
|
return HomepageStyleSelector;
|
||||||
|
default:
|
||||||
|
return ComboBox;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keyPress(event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
onChangeValue(value) {
|
||||||
|
this.set("field.value", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
{{component
|
||||||
|
this.component
|
||||||
|
class="wizard-container__dropdown"
|
||||||
|
value=@field.value
|
||||||
|
content=@field.choices
|
||||||
|
nameProperty="label"
|
||||||
|
tabindex="9"
|
||||||
|
onChange=this.onChangeValug
|
||||||
|
options=(hash translatedNone=false)
|
||||||
|
}}
|
||||||
|
</template>
|
||||||
|
}
|
|
@ -1,10 +0,0 @@
|
||||||
{{component
|
|
||||||
this.component
|
|
||||||
class="wizard-container__dropdown"
|
|
||||||
value=this.field.value
|
|
||||||
content=this.field.choices
|
|
||||||
nameProperty="label"
|
|
||||||
tabindex="9"
|
|
||||||
onChange=(action "onChangeValue")
|
|
||||||
options=(hash translatedNone=false)
|
|
||||||
}}
|
|
|
@ -1,33 +0,0 @@
|
||||||
import Component from "@ember/component";
|
|
||||||
import { action, set } from "@ember/object";
|
|
||||||
import discourseComputed from "discourse-common/utils/decorators";
|
|
||||||
import ColorPalettes from "select-kit/components/color-palettes";
|
|
||||||
import ComboBox from "select-kit/components/combo-box";
|
|
||||||
|
|
||||||
export default class Dropdown extends Component {
|
|
||||||
init() {
|
|
||||||
super.init(...arguments);
|
|
||||||
|
|
||||||
if (this.field.id === "color_scheme") {
|
|
||||||
for (let choice of this.field.choices) {
|
|
||||||
if (choice?.data?.colors) {
|
|
||||||
set(choice, "colors", choice.data.colors);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@discourseComputed("field.id")
|
|
||||||
component(id) {
|
|
||||||
return id === "color_scheme" ? ColorPalettes : ComboBox;
|
|
||||||
}
|
|
||||||
|
|
||||||
keyPress(e) {
|
|
||||||
e.stopPropagation();
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
onChangeValue(value) {
|
|
||||||
this.set("field.value", value);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { action } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
import { drawHeader, LOREM } from "../../../lib/preview";
|
import { i18n } from "discourse-i18n";
|
||||||
|
import { drawHeader } from "../../../lib/preview";
|
||||||
import PreviewBaseComponent from "../styling-preview/-preview-base";
|
import PreviewBaseComponent from "../styling-preview/-preview-base";
|
||||||
|
|
||||||
export default class LogoSmall extends PreviewBaseComponent {
|
export default class LogoSmall extends PreviewBaseComponent {
|
||||||
|
@ -34,10 +35,10 @@ export default class LogoSmall extends PreviewBaseComponent {
|
||||||
|
|
||||||
const image = this.image;
|
const image = this.image;
|
||||||
const headerMargin = headerHeight * 0.2;
|
const headerMargin = headerHeight * 0.2;
|
||||||
|
|
||||||
const maxWidth = headerHeight - headerMargin * 2.0;
|
const maxWidth = headerHeight - headerMargin * 2.0;
|
||||||
let imageWidth = image.width;
|
let imageWidth = image.width;
|
||||||
let ratio = 1.0;
|
let ratio = 1.0;
|
||||||
|
|
||||||
if (imageWidth > maxWidth) {
|
if (imageWidth > maxWidth) {
|
||||||
ratio = maxWidth / imageWidth;
|
ratio = maxWidth / imageWidth;
|
||||||
imageWidth = maxWidth;
|
imageWidth = maxWidth;
|
||||||
|
@ -52,38 +53,42 @@ export default class LogoSmall extends PreviewBaseComponent {
|
||||||
);
|
);
|
||||||
|
|
||||||
const afterLogo = headerMargin * 1.7 + imageWidth;
|
const afterLogo = headerMargin * 1.7 + imageWidth;
|
||||||
const fontSize = Math.round(headerHeight * 0.4);
|
const fontSize = Math.round(headerHeight * 0.3);
|
||||||
|
|
||||||
ctx.font = `Bold ${fontSize}px '${headingFont}'`;
|
ctx.font = `Bold ${fontSize}px '${headingFont}'`;
|
||||||
ctx.fillStyle = colors.primary;
|
ctx.fillStyle = colors.primary;
|
||||||
const title = LOREM.substring(0, 27);
|
const title = i18n("wizard.homepage_preview.topic_titles.what_books");
|
||||||
ctx.fillText(
|
ctx.fillText(
|
||||||
title,
|
title,
|
||||||
headerMargin + imageWidth,
|
headerMargin + imageWidth + 10,
|
||||||
headerHeight - fontSize * 1.1
|
headerHeight - fontSize * 1.8
|
||||||
);
|
);
|
||||||
|
|
||||||
const category = this.categories()[0];
|
const category = this.categories()[0];
|
||||||
const badgeSize = height / 13.0;
|
const badgeSize = height / 13.0;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.fillStyle = category.color;
|
ctx.fillStyle = category.color;
|
||||||
ctx.rect(afterLogo, headerHeight * 0.7, badgeSize, badgeSize);
|
ctx.rect(afterLogo + 2, headerHeight * 0.6, badgeSize, badgeSize);
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
|
|
||||||
ctx.font = `Bold ${badgeSize * 1.2}px '${font}'`;
|
ctx.font = `Bold ${badgeSize * 1.2}px '${font}'`;
|
||||||
ctx.fillStyle = colors.primary;
|
ctx.fillStyle = colors.primary;
|
||||||
ctx.fillText(
|
ctx.fillText(
|
||||||
category.displayName,
|
category.name,
|
||||||
afterLogo + badgeSize * 1.5,
|
afterLogo + badgeSize * 1.5,
|
||||||
headerHeight * 0.7 + badgeSize * 0.9
|
headerHeight * 0.6 + badgeSize * 0.9
|
||||||
);
|
);
|
||||||
|
|
||||||
const LINE_HEIGHT = 12;
|
const LINE_HEIGHT = 12;
|
||||||
ctx.font = `${LINE_HEIGHT}px '${font}'`;
|
ctx.font = `${LINE_HEIGHT}px '${font}'`;
|
||||||
const lines = LOREM.split("\n");
|
const opFirstSentenceLines = i18n(
|
||||||
for (let i = 0; i < 10; i++) {
|
"wizard.homepage_preview.topic_ops.what_books"
|
||||||
const line = height * 0.55 + i * (LINE_HEIGHT * 1.5);
|
)
|
||||||
ctx.fillText(lines[i], afterLogo, line);
|
.split(".")[0]
|
||||||
|
.split("\n");
|
||||||
|
for (let i = 0; i < 2; i++) {
|
||||||
|
const line = height * 0.7 + i * (LINE_HEIGHT * 1.5);
|
||||||
|
ctx.fillText(opFirstSentenceLines[i], afterLogo, line);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
<label
|
<label
|
||||||
class="wizard-container__button wizard-container__button-upload
|
class={{concatClass
|
||||||
{{if this.uploading 'disabled'}}"
|
"wizard-container__button wizard-container__button-upload"
|
||||||
|
(if this.uploading "disabled")
|
||||||
|
(if this.hasUpload "has-upload")
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{{#if this.uploading}}
|
{{#if this.uploading}}
|
||||||
{{i18n "wizard.uploading"}}
|
{{i18n "wizard.uploading"}}
|
||||||
|
|
|
@ -27,6 +27,14 @@ export default class Image extends Component {
|
||||||
this.setupUploads();
|
this.setupUploads();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@discourseComputed("uploading", "field.value")
|
||||||
|
hasUpload() {
|
||||||
|
return (
|
||||||
|
!this.uploading &&
|
||||||
|
!this.field.value.includes("discourse-logo-sketch-small.png")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
setupUploads() {
|
setupUploads() {
|
||||||
const id = this.field.id;
|
const id = this.field.id;
|
||||||
this._uppyInstance = new Uppy({
|
this._uppyInstance = new Uppy({
|
||||||
|
|
|
@ -32,37 +32,38 @@ export default class Radio extends Component {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="wizard-container__radio-choices">
|
<div class="wizard-container__radio-choices">
|
||||||
{{#each @field.choices as |c|}}
|
{{#each @field.choices as |choice|}}
|
||||||
<div
|
<div
|
||||||
class={{concatClass
|
class={{concatClass
|
||||||
"wizard-container__radio-choice"
|
"wizard-container__radio-choice"
|
||||||
(if c.selected "--selected")
|
(if choice.selected "--selected")
|
||||||
}}
|
}}
|
||||||
|
data-choice-id={{choice.id}}
|
||||||
>
|
>
|
||||||
<label class="wizard-container__label">
|
<label class="wizard-container__label">
|
||||||
<PluginOutlet
|
<PluginOutlet
|
||||||
@name="wizard-radio"
|
@name="wizard-radio"
|
||||||
@outletArgs={{hash disabled=c.disabled}}
|
@outletArgs={{hash disabled=choice.disabled}}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
value={{c.id}}
|
value={{choice.id}}
|
||||||
class="wizard-container__radio"
|
class="wizard-container__radio"
|
||||||
disabled={{c.disabled}}
|
disabled={{choice.disabled}}
|
||||||
checked={{c.selected}}
|
checked={{choice.selected}}
|
||||||
{{on "change" (withEventValue this.selectionChanged)}}
|
{{on "change" (withEventValue this.selectionChanged)}}
|
||||||
/>
|
/>
|
||||||
<span class="wizard-container__radio-label">
|
<span class="wizard-container__radio-label">
|
||||||
{{#if c.icon}}
|
{{#if choice.icon}}
|
||||||
{{icon c.icon}}
|
{{icon choice.icon}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
<span>{{c.label}}</span>
|
<span>{{choice.label}}</span>
|
||||||
</span>
|
</span>
|
||||||
</PluginOutlet>
|
</PluginOutlet>
|
||||||
|
|
||||||
<PluginOutlet
|
<PluginOutlet
|
||||||
@name="below-wizard-radio"
|
@name="below-wizard-radio"
|
||||||
@outletArgs={{hash disabled=c.disabled}}
|
@outletArgs={{hash disabled=choice.disabled}}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { darkLightDiff, LOREM } from "../../../lib/preview";
|
import { i18n } from "discourse-i18n";
|
||||||
|
import { darkLightDiff } from "../../../lib/preview";
|
||||||
import PreviewBaseComponent from "./-preview-base";
|
import PreviewBaseComponent from "./-preview-base";
|
||||||
|
|
||||||
export default class HomepagePreview extends PreviewBaseComponent {
|
export default class HomepagePreview extends PreviewBaseComponent {
|
||||||
|
@ -25,26 +26,26 @@ export default class HomepagePreview extends PreviewBaseComponent {
|
||||||
|
|
||||||
const homepageStyle = this.getHomepageStyle();
|
const homepageStyle = this.getHomepageStyle();
|
||||||
|
|
||||||
if (homepageStyle === "latest") {
|
if (homepageStyle === "latest" || homepageStyle === "hot") {
|
||||||
this.drawPills(colors, font, height * 0.15);
|
this.drawPills(colors, font, height * 0.15, { homepageStyle });
|
||||||
this.renderLatest(ctx, colors, font, width, height);
|
this.renderNonCategoryHomepage(ctx, colors, font, width, height);
|
||||||
} else if (
|
} else if (
|
||||||
["categories_only", "categories_with_featured_topics"].includes(
|
["categories_only", "categories_with_featured_topics"].includes(
|
||||||
homepageStyle
|
homepageStyle
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
this.drawPills(colors, font, height * 0.15, { categories: true });
|
this.drawPills(colors, font, height * 0.15, { homepageStyle });
|
||||||
this.renderCategories(ctx, colors, font, width, height);
|
this.renderCategories(ctx, colors, font, width, height);
|
||||||
} else if (
|
} else if (
|
||||||
["categories_boxes", "categories_boxes_with_topics"].includes(
|
["categories_boxes", "categories_boxes_with_topics"].includes(
|
||||||
homepageStyle
|
homepageStyle
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
this.drawPills(colors, font, height * 0.15, { categories: true });
|
this.drawPills(colors, font, height * 0.15, { homepageStyle });
|
||||||
const topics = homepageStyle === "categories_boxes_with_topics";
|
const topics = homepageStyle === "categories_boxes_with_topics";
|
||||||
this.renderCategoriesBoxes(ctx, colors, font, width, height, { topics });
|
this.renderCategoriesBoxes(ctx, colors, font, width, height, { topics });
|
||||||
} else {
|
} else {
|
||||||
this.drawPills(colors, font, height * 0.15, { categories: true });
|
this.drawPills(colors, font, height * 0.15, { homepageStyle });
|
||||||
this.renderCategoriesWithTopics(ctx, colors, font, width, height);
|
this.renderCategoriesWithTopics(ctx, colors, font, width, height);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -82,14 +83,10 @@ export default class HomepagePreview extends PreviewBaseComponent {
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
ctx.font = `Bold ${bodyFontSize * 1.3}em '${font}'`;
|
ctx.font = `700 ${bodyFontSize * 1.3}em '${font}'`;
|
||||||
ctx.fillStyle = colors.primary;
|
ctx.fillStyle = colors.primary;
|
||||||
ctx.textAlign = "center";
|
ctx.textAlign = "center";
|
||||||
ctx.fillText(
|
ctx.fillText(category.name, boxStartX + boxWidth / 2, boxStartY + 25);
|
||||||
category.displayName,
|
|
||||||
boxStartX + boxWidth / 2,
|
|
||||||
boxStartY + 25
|
|
||||||
);
|
|
||||||
ctx.textAlign = "left";
|
ctx.textAlign = "left";
|
||||||
|
|
||||||
if (opts.topics) {
|
if (opts.topics) {
|
||||||
|
@ -167,16 +164,16 @@ export default class HomepagePreview extends PreviewBaseComponent {
|
||||||
drawLine(width / 2, y);
|
drawLine(width / 2, y);
|
||||||
|
|
||||||
// Categories
|
// Categories
|
||||||
this.categories().forEach((category) => {
|
this.categories().forEach((category, idx) => {
|
||||||
const textPos = y + categoryHeight * 0.35;
|
const textPos = y + categoryHeight * 0.35;
|
||||||
ctx.font = `Bold ${bodyFontSize * 1.1}em '${font}'`;
|
ctx.font = `700 ${bodyFontSize * 1.1}em '${font}'`;
|
||||||
ctx.fillStyle = colors.primary;
|
ctx.fillStyle = colors.primary;
|
||||||
ctx.fillText(category.displayName, cols[0], textPos);
|
ctx.fillText(category.name, cols[0], textPos);
|
||||||
|
|
||||||
ctx.font = `${bodyFontSize * 0.8}em '${font}'`;
|
ctx.font = `${bodyFontSize * 0.8}em '${font}'`;
|
||||||
ctx.fillStyle = textColor;
|
ctx.fillStyle = textColor;
|
||||||
ctx.fillText(
|
ctx.fillText(
|
||||||
titles[0],
|
titles[idx],
|
||||||
cols[0] - margin * 0.25,
|
cols[0] - margin * 0.25,
|
||||||
textPos + categoryHeight * 0.36
|
textPos + categoryHeight * 0.36
|
||||||
);
|
);
|
||||||
|
@ -263,16 +260,16 @@ export default class HomepagePreview extends PreviewBaseComponent {
|
||||||
const titles = this.getTitles();
|
const titles = this.getTitles();
|
||||||
|
|
||||||
// Categories
|
// Categories
|
||||||
this.categories().forEach((category) => {
|
this.categories().forEach((category, idx) => {
|
||||||
const textPos = y + categoryHeight * 0.35;
|
const textPos = y + categoryHeight * 0.35;
|
||||||
ctx.font = `Bold ${bodyFontSize * 1.1}em '${font}'`;
|
ctx.font = `700 ${bodyFontSize * 1.1}em '${font}'`;
|
||||||
ctx.fillStyle = colors.primary;
|
ctx.fillStyle = colors.primary;
|
||||||
ctx.fillText(category.displayName, cols[0], textPos);
|
ctx.fillText(category.name, cols[0], textPos);
|
||||||
|
|
||||||
ctx.font = `${bodyFontSize * 0.8}em '${font}'`;
|
ctx.font = `${bodyFontSize * 0.8}em '${font}'`;
|
||||||
ctx.fillStyle = textColor;
|
ctx.fillStyle = textColor;
|
||||||
ctx.fillText(
|
ctx.fillText(
|
||||||
titles[0],
|
titles[idx],
|
||||||
cols[0] - margin * 0.25,
|
cols[0] - margin * 0.25,
|
||||||
textPos + categoryHeight * 0.36
|
textPos + categoryHeight * 0.36
|
||||||
);
|
);
|
||||||
|
@ -303,7 +300,7 @@ export default class HomepagePreview extends PreviewBaseComponent {
|
||||||
const category = this.categories()[0];
|
const category = this.categories()[0];
|
||||||
ctx.font = `${bodyFontSize}em '${font}'`;
|
ctx.font = `${bodyFontSize}em '${font}'`;
|
||||||
const textPos = y + topicHeight * 0.45;
|
const textPos = y + topicHeight * 0.45;
|
||||||
ctx.fillStyle = textColor;
|
ctx.fillStyle = colors.primary;
|
||||||
this.scaleImage(
|
this.scaleImage(
|
||||||
this.avatar,
|
this.avatar,
|
||||||
cols[2],
|
cols[2],
|
||||||
|
@ -313,7 +310,7 @@ export default class HomepagePreview extends PreviewBaseComponent {
|
||||||
);
|
);
|
||||||
ctx.fillText(title, cols[3], textPos);
|
ctx.fillText(title, cols[3], textPos);
|
||||||
|
|
||||||
ctx.font = `Bold ${bodyFontSize}em '${font}'`;
|
ctx.font = `700 ${bodyFontSize}em '${font}'`;
|
||||||
ctx.fillText(Math.floor(Math.random() * 90) + 10, cols[4], textPos);
|
ctx.fillText(Math.floor(Math.random() * 90) + 10, cols[4], textPos);
|
||||||
ctx.font = `${bodyFontSize}em '${font}'`;
|
ctx.font = `${bodyFontSize}em '${font}'`;
|
||||||
ctx.fillText(`1h`, cols[4], textPos + topicHeight * 0.4);
|
ctx.fillText(`1h`, cols[4], textPos + topicHeight * 0.4);
|
||||||
|
@ -321,9 +318,9 @@ export default class HomepagePreview extends PreviewBaseComponent {
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.fillStyle = category.color;
|
ctx.fillStyle = category.color;
|
||||||
const badgeSize = topicHeight * 0.1;
|
const badgeSize = topicHeight * 0.1;
|
||||||
ctx.font = `Bold ${bodyFontSize * 0.5}em '${font}'`;
|
ctx.font = `700 ${bodyFontSize * 0.5}em '${font}'`;
|
||||||
ctx.rect(
|
ctx.rect(
|
||||||
cols[3] + margin * 0.5,
|
cols[3] + margin * 0.25,
|
||||||
y + topicHeight * 0.65,
|
y + topicHeight * 0.65,
|
||||||
badgeSize,
|
badgeSize,
|
||||||
badgeSize
|
badgeSize
|
||||||
|
@ -332,8 +329,8 @@ export default class HomepagePreview extends PreviewBaseComponent {
|
||||||
|
|
||||||
ctx.fillStyle = colors.primary;
|
ctx.fillStyle = colors.primary;
|
||||||
ctx.fillText(
|
ctx.fillText(
|
||||||
category.displayName,
|
category.name,
|
||||||
cols[3] + badgeSize * 3,
|
cols[3] + badgeSize * 2,
|
||||||
y + topicHeight * 0.76
|
y + topicHeight * 0.76
|
||||||
);
|
);
|
||||||
y += topicHeight;
|
y += topicHeight;
|
||||||
|
@ -347,16 +344,23 @@ export default class HomepagePreview extends PreviewBaseComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
getTitles() {
|
getTitles() {
|
||||||
return LOREM.split(".")
|
return [
|
||||||
.slice(0, 8)
|
i18n("wizard.homepage_preview.topic_titles.what_books"),
|
||||||
.map((t) => t.substring(0, 40));
|
i18n("wizard.homepage_preview.topic_titles.what_movies"),
|
||||||
|
i18n("wizard.homepage_preview.topic_titles.random_fact"),
|
||||||
|
i18n("wizard.homepage_preview.topic_titles.tv_show"),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
getDescriptions() {
|
getDescriptions() {
|
||||||
return LOREM.split(".");
|
return [
|
||||||
|
i18n("wizard.homepage_preview.category_descriptions.icebreakers"),
|
||||||
|
i18n("wizard.homepage_preview.category_descriptions.news"),
|
||||||
|
i18n("wizard.homepage_preview.category_descriptions.site_feedback"),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
renderLatest(ctx, colors, font, width, height) {
|
renderNonCategoryHomepage(ctx, colors, font, width, height) {
|
||||||
const rowHeight = height / 6.6;
|
const rowHeight = height / 6.6;
|
||||||
// accounts for hard-set color variables in solarized themes
|
// accounts for hard-set color variables in solarized themes
|
||||||
const textColor =
|
const textColor =
|
||||||
|
@ -379,17 +383,33 @@ export default class HomepagePreview extends PreviewBaseComponent {
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
};
|
};
|
||||||
|
|
||||||
const cols = [0.02, 0.66, 0.8, 0.87, 0.93].map((c) => c * width);
|
const cols = [0.02, 0.66, 0.75, 0.83, 0.9].map((c) => c * width);
|
||||||
|
|
||||||
// Headings
|
// Headings
|
||||||
const headingY = height * 0.33;
|
const headingY = height * 0.33;
|
||||||
|
|
||||||
ctx.fillStyle = textColor;
|
ctx.fillStyle = textColor;
|
||||||
ctx.font = `${bodyFontSize * 0.9}em '${font}'`;
|
ctx.font = `${bodyFontSize * 0.9}em '${font}'`;
|
||||||
ctx.fillText("Topic", cols[0], headingY);
|
ctx.fillText(
|
||||||
ctx.fillText("Replies", cols[2], headingY);
|
i18n("wizard.homepage_preview.table_headers.topic"),
|
||||||
ctx.fillText("Views", cols[3], headingY);
|
cols[0],
|
||||||
ctx.fillText("Activity", cols[4], headingY);
|
headingY
|
||||||
|
);
|
||||||
|
ctx.fillText(
|
||||||
|
i18n("wizard.homepage_preview.table_headers.replies"),
|
||||||
|
cols[2],
|
||||||
|
headingY
|
||||||
|
);
|
||||||
|
ctx.fillText(
|
||||||
|
i18n("wizard.homepage_preview.table_headers.views"),
|
||||||
|
cols[3],
|
||||||
|
headingY
|
||||||
|
);
|
||||||
|
ctx.fillText(
|
||||||
|
i18n("wizard.homepage_preview.table_headers.activity"),
|
||||||
|
cols[4],
|
||||||
|
headingY
|
||||||
|
);
|
||||||
|
|
||||||
// Topics
|
// Topics
|
||||||
let y = headingY + rowHeight / 2.6;
|
let y = headingY + rowHeight / 2.6;
|
||||||
|
@ -400,20 +420,21 @@ export default class HomepagePreview extends PreviewBaseComponent {
|
||||||
ctx.lineWidth = 1;
|
ctx.lineWidth = 1;
|
||||||
this.getTitles().forEach((title) => {
|
this.getTitles().forEach((title) => {
|
||||||
const textPos = y + rowHeight * 0.4;
|
const textPos = y + rowHeight * 0.4;
|
||||||
ctx.fillStyle = textColor;
|
ctx.fillStyle = colors.primary;
|
||||||
ctx.fillText(title, cols[0], textPos);
|
ctx.fillText(title, cols[0], textPos);
|
||||||
|
|
||||||
|
// Category badge
|
||||||
const category = this.categories()[0];
|
const category = this.categories()[0];
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.fillStyle = category.color;
|
ctx.fillStyle = category.color;
|
||||||
const badgeSize = rowHeight * 0.15;
|
const badgeSize = rowHeight * 0.15;
|
||||||
ctx.font = `Bold ${bodyFontSize * 0.75}em '${font}'`;
|
ctx.font = `700 ${bodyFontSize * 0.75}em '${font}'`;
|
||||||
ctx.rect(cols[0] + 4, y + rowHeight * 0.6, badgeSize, badgeSize);
|
ctx.rect(cols[0] + 4, y + rowHeight * 0.6, badgeSize, badgeSize);
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
|
|
||||||
ctx.fillStyle = colors.primary;
|
ctx.fillStyle = colors.primary;
|
||||||
ctx.fillText(
|
ctx.fillText(
|
||||||
category.displayName,
|
category.name,
|
||||||
cols[0] + badgeSize * 2,
|
cols[0] + badgeSize * 2,
|
||||||
y + rowHeight * 0.73
|
y + rowHeight * 0.73
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,18 +6,9 @@ import { htmlSafe } from "@ember/template";
|
||||||
import { Promise } from "rsvp";
|
import { Promise } from "rsvp";
|
||||||
import PreloadStore from "discourse/lib/preload-store";
|
import PreloadStore from "discourse/lib/preload-store";
|
||||||
import getUrl from "discourse-common/lib/get-url";
|
import getUrl from "discourse-common/lib/get-url";
|
||||||
|
import { i18n } from "discourse-i18n";
|
||||||
import { darkLightDiff, drawHeader } from "../../../lib/preview";
|
import { darkLightDiff, drawHeader } from "../../../lib/preview";
|
||||||
|
|
||||||
export const LOREM = `
|
|
||||||
Lorem ipsum dolor sit amet,
|
|
||||||
consectetur adipiscing elit.
|
|
||||||
Nullam eget sem non elit
|
|
||||||
tincidunt rhoncus. Fusce
|
|
||||||
velit nisl, porttitor sed
|
|
||||||
nisl ac, consectetur interdum
|
|
||||||
metus. Fusce in consequat
|
|
||||||
augue, vel facilisis felis.`;
|
|
||||||
|
|
||||||
const scaled = {};
|
const scaled = {};
|
||||||
|
|
||||||
function canvasFor(image, w, h) {
|
function canvasFor(image, w, h) {
|
||||||
|
@ -125,7 +116,7 @@ export default class PreviewBase extends Component {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
Promise.all(
|
return Promise.all(
|
||||||
fontFaces.map((fontFace) =>
|
fontFaces.map((fontFace) =>
|
||||||
fontFace.load().then((loadedFont) => {
|
fontFace.load().then((loadedFont) => {
|
||||||
document.fonts.add(loadedFont);
|
document.fonts.add(loadedFont);
|
||||||
|
@ -144,7 +135,7 @@ export default class PreviewBase extends Component {
|
||||||
this.loadingFontVariants = false;
|
this.loadingFontVariants = false;
|
||||||
});
|
});
|
||||||
} else if (this.loadedFonts.has(font.id)) {
|
} else if (this.loadedFonts.has(font.id)) {
|
||||||
this.triggerRepaint();
|
return Promise.resolve(this.triggerRepaint());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,9 +162,15 @@ export default class PreviewBase extends Component {
|
||||||
|
|
||||||
reload() {
|
reload() {
|
||||||
Promise.all([this.loadFonts(), this.loadImages()]).then(() => {
|
Promise.all([this.loadFonts(), this.loadImages()]).then(() => {
|
||||||
|
// NOTE: This must be done otherwise the "bold" variant of the body font
|
||||||
|
// will not be loaded for some reason before rendering the canvas.
|
||||||
|
//
|
||||||
|
// The header font does not suffer from this issue.
|
||||||
|
this.loadFontVariants(this.wizard.font).then(() => {
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
this.triggerRepaint();
|
this.triggerRepaint();
|
||||||
});
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
triggerRepaint() {
|
triggerRepaint() {
|
||||||
|
@ -219,9 +216,18 @@ export default class PreviewBase extends Component {
|
||||||
|
|
||||||
categories() {
|
categories() {
|
||||||
return [
|
return [
|
||||||
{ name: "consecteteur", color: "#652D90" },
|
{
|
||||||
{ name: "ultrices", color: "#3AB54A" },
|
name: i18n("wizard.homepage_preview.category_names.icebreakers"),
|
||||||
{ name: "placerat", color: "#25AAE2" },
|
color: "#652D90",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n("wizard.homepage_preview.category_names.news"),
|
||||||
|
color: "#3AB54A",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n("wizard.homepage_preview.category_names.site_feedback"),
|
||||||
|
color: "#25AAE2",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -248,17 +254,20 @@ export default class PreviewBase extends Component {
|
||||||
ctx.drawImage(scaled[key], x, y, w, h);
|
ctx.drawImage(scaled[key], x, y, w, h);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get headerHeight() {
|
||||||
|
return this.height * 0.15;
|
||||||
|
}
|
||||||
|
|
||||||
drawFullHeader(colors, font, logo) {
|
drawFullHeader(colors, font, logo) {
|
||||||
const { ctx } = this;
|
const { ctx } = this;
|
||||||
|
|
||||||
const headerHeight = this.height * 0.15;
|
drawHeader(ctx, colors, this.width, this.headerHeight);
|
||||||
drawHeader(ctx, colors, this.width, headerHeight);
|
|
||||||
|
|
||||||
const avatarSize = this.height * 0.1;
|
const avatarSize = this.height * 0.1;
|
||||||
const headerMargin = headerHeight * 0.2;
|
const headerMargin = this.headerHeight * 0.2;
|
||||||
|
|
||||||
if (logo) {
|
if (logo) {
|
||||||
const logoHeight = headerHeight - headerMargin * 2;
|
const logoHeight = this.headerHeight - headerMargin * 2;
|
||||||
|
|
||||||
const ratio = logoHeight / logo.height;
|
const ratio = logoHeight / logo.height;
|
||||||
this.scaleImage(
|
this.scaleImage(
|
||||||
|
@ -280,23 +289,25 @@ export default class PreviewBase extends Component {
|
||||||
avatarSize,
|
avatarSize,
|
||||||
avatarSize
|
avatarSize
|
||||||
);
|
);
|
||||||
|
|
||||||
// accounts for hard-set color variables in solarized themes
|
// accounts for hard-set color variables in solarized themes
|
||||||
ctx.fillStyle =
|
ctx.fillStyle =
|
||||||
colors.primary_low_mid ||
|
colors.primary_low_mid ||
|
||||||
darkLightDiff(colors.primary, colors.secondary, 45, 55);
|
darkLightDiff(colors.primary, colors.secondary, 45, 55);
|
||||||
|
|
||||||
const pathScale = headerHeight / 1200;
|
const pathScale = this.headerHeight / 1200;
|
||||||
// search icon SVG path
|
|
||||||
const searchIcon = new Path2D(
|
const searchIcon = new Path2D(
|
||||||
"M505 442.7L405.3 343c-4.5-4.5-10.6-7-17-7H372c27.6-35.3 44-79.7 44-128C416 93.1 322.9 0 208 0S0 93.1 0 208s93.1 208 208 208c48.3 0 92.7-16.4 128-44v16.3c0 6.4 2.5 12.5 7 17l99.7 99.7c9.4 9.4 24.6 9.4 33.9 0l28.3-28.3c9.4-9.4 9.4-24.6.1-34zM208 336c-70.7 0-128-57.2-128-128 0-70.7 57.2-128 128-128 70.7 0 128 57.2 128 128 0 70.7-57.2 128-128 128z"
|
"M505 442.7L405.3 343c-4.5-4.5-10.6-7-17-7H372c27.6-35.3 44-79.7 44-128C416 93.1 322.9 0 208 0S0 93.1 0 208s93.1 208 208 208c48.3 0 92.7-16.4 128-44v16.3c0 6.4 2.5 12.5 7 17l99.7 99.7c9.4 9.4 24.6 9.4 33.9 0l28.3-28.3c9.4-9.4 9.4-24.6.1-34zM208 336c-70.7 0-128-57.2-128-128 0-70.7 57.2-128 128-128 70.7 0 128 57.2 128 128 0 70.7-57.2 128-128 128z"
|
||||||
);
|
);
|
||||||
// hamburger icon
|
|
||||||
const hamburgerIcon = new Path2D(
|
const hamburgerIcon = new Path2D(
|
||||||
"M16 132h416c8.837 0 16-7.163 16-16V76c0-8.837-7.163-16-16-16H16C7.163 60 0 67.163 0 76v40c0 8.837 7.163 16 16 16zm0 160h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16zm0 160h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16z"
|
"M16 132h416c8.837 0 16-7.163 16-16V76c0-8.837-7.163-16-16-16H16C7.163 60 0 67.163 0 76v40c0 8.837 7.163 16 16 16zm0 160h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16zm0 160h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16z"
|
||||||
);
|
);
|
||||||
|
const chatIcon = new Path2D(
|
||||||
|
"M512 240c0 114.9-114.6 208-256 208c-37.1 0-72.3-6.4-104.1-17.9c-11.9 8.7-31.3 20.6-54.3 30.6C73.6 471.1 44.7 480 16 480c-6.5 0-12.3-3.9-14.8-9.9c-2.5-6-1.1-12.8 3.4-17.4c0 0 0 0 0 0s0 0 0 0s0 0 0 0c0 0 0 0 0 0l.3-.3c.3-.3 .7-.7 1.3-1.4c1.1-1.2 2.8-3.1 4.9-5.7c4.1-5 9.6-12.4 15.2-21.6c10-16.6 19.5-38.4 21.4-62.9C17.7 326.8 0 285.1 0 240C0 125.1 114.6 32 256 32s256 93.1 256 208z"
|
||||||
|
);
|
||||||
ctx.save(); // Save the previous state for translation and scale
|
ctx.save(); // Save the previous state for translation and scale
|
||||||
ctx.translate(
|
ctx.translate(
|
||||||
this.width - avatarSize * 3 - headerMargin * 0.5,
|
this.width - avatarSize * 2 - headerMargin * 0.5,
|
||||||
avatarSize / 2
|
avatarSize / 2
|
||||||
);
|
);
|
||||||
// need to scale paths otherwise they're too large
|
// need to scale paths otherwise they're too large
|
||||||
|
@ -305,10 +316,15 @@ export default class PreviewBase extends Component {
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.translate(
|
ctx.translate(
|
||||||
this.width - avatarSize * 2 - headerMargin * 0.5,
|
this.width - avatarSize * 3 - headerMargin * 0.5,
|
||||||
avatarSize / 2
|
avatarSize / 2
|
||||||
);
|
);
|
||||||
ctx.scale(pathScale, pathScale);
|
ctx.scale(pathScale, pathScale);
|
||||||
|
ctx.fill(chatIcon);
|
||||||
|
ctx.restore();
|
||||||
|
ctx.save();
|
||||||
|
ctx.translate(headerMargin * 1.75, avatarSize / 2);
|
||||||
|
ctx.scale(pathScale, pathScale);
|
||||||
ctx.fill(hamburgerIcon);
|
ctx.fill(hamburgerIcon);
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
@ -318,77 +334,109 @@ export default class PreviewBase extends Component {
|
||||||
|
|
||||||
const { ctx } = this;
|
const { ctx } = this;
|
||||||
|
|
||||||
const categoriesSize = headerHeight * 2;
|
const badgeHeight = headerHeight * 2 * 0.25;
|
||||||
const badgeHeight = categoriesSize * 0.25;
|
|
||||||
const headerMargin = headerHeight * 0.2;
|
const headerMargin = headerHeight * 0.2;
|
||||||
|
const fontSize = Math.round(badgeHeight * 0.5);
|
||||||
|
ctx.font = `${fontSize}px '${font}'`;
|
||||||
|
|
||||||
|
const allCategoriesText = i18n(
|
||||||
|
"wizard.homepage_preview.nav_buttons.all_categories"
|
||||||
|
);
|
||||||
|
const categoriesWidth = ctx.measureText(allCategoriesText).width;
|
||||||
|
const categoriesBoxWidth = categoriesWidth + headerMargin * 2;
|
||||||
|
|
||||||
|
// Box around "all categories >"
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.strokeStyle = colors.primary;
|
ctx.strokeStyle = colors.primary;
|
||||||
ctx.lineWidth = 0.5;
|
ctx.lineWidth = 0.5;
|
||||||
ctx.rect(
|
ctx.rect(
|
||||||
headerMargin,
|
headerMargin,
|
||||||
headerHeight + headerMargin,
|
headerHeight + headerMargin,
|
||||||
categoriesSize,
|
categoriesBoxWidth,
|
||||||
badgeHeight
|
badgeHeight
|
||||||
);
|
);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
const fontSize = Math.round(badgeHeight * 0.5);
|
|
||||||
|
|
||||||
ctx.font = `${fontSize}px '${font}'`;
|
|
||||||
ctx.fillStyle = colors.primary;
|
ctx.fillStyle = colors.primary;
|
||||||
ctx.fillText(
|
ctx.fillText(
|
||||||
"all categories",
|
allCategoriesText,
|
||||||
headerMargin * 1.5,
|
headerMargin * 1.5,
|
||||||
headerHeight + headerMargin * 1.4 + fontSize
|
headerHeight + headerMargin * 1.4 + fontSize
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Caret (>) at the end of "all categories" box
|
||||||
const pathScale = badgeHeight / 1000;
|
const pathScale = badgeHeight / 1000;
|
||||||
// caret icon
|
|
||||||
const caretIcon = new Path2D(
|
const caretIcon = new Path2D(
|
||||||
"M0 384.662V127.338c0-17.818 21.543-26.741 34.142-14.142l128.662 128.662c7.81 7.81 7.81 20.474 0 28.284L34.142 398.804C21.543 411.404 0 402.48 0 384.662z"
|
"M0 384.662V127.338c0-17.818 21.543-26.741 34.142-14.142l128.662 128.662c7.81 7.81 7.81 20.474 0 28.284L34.142 398.804C21.543 411.404 0 402.48 0 384.662z"
|
||||||
);
|
);
|
||||||
|
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.translate(
|
ctx.translate(
|
||||||
categoriesSize - headerMargin / 4,
|
categoriesBoxWidth,
|
||||||
headerHeight + headerMargin + badgeHeight / 4
|
headerHeight + headerMargin + badgeHeight / 4
|
||||||
);
|
);
|
||||||
ctx.scale(pathScale, pathScale);
|
ctx.scale(pathScale, pathScale);
|
||||||
ctx.fill(caretIcon);
|
ctx.fill(caretIcon);
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
|
|
||||||
const text = opts.categories ? "Categories" : "Latest";
|
const categoryHomepage =
|
||||||
|
opts.homepageStyle !== "hot" && opts.homepageStyle !== "latest";
|
||||||
|
|
||||||
|
// First top menu item
|
||||||
|
let otherHomepageText;
|
||||||
|
switch (opts.homepageStyle) {
|
||||||
|
case "hot":
|
||||||
|
otherHomepageText = i18n("wizard.homepage_preview.nav_buttons.hot");
|
||||||
|
break;
|
||||||
|
case "latest":
|
||||||
|
otherHomepageText = i18n("wizard.homepage_preview.nav_buttons.latest");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstTopMenuItemText = categoryHomepage
|
||||||
|
? i18n("wizard.homepage_preview.nav_buttons.categories")
|
||||||
|
: otherHomepageText;
|
||||||
|
|
||||||
|
const newText = i18n("wizard.homepage_preview.nav_buttons.new");
|
||||||
|
const unreadText = i18n("wizard.homepage_preview.nav_buttons.unread");
|
||||||
|
const topText = i18n("wizard.homepage_preview.nav_buttons.top");
|
||||||
|
|
||||||
const activeWidth = categoriesSize * (opts.categories ? 0.8 : 0.55);
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.fillStyle = colors.tertiary;
|
ctx.fillStyle = colors.tertiary;
|
||||||
ctx.rect(
|
ctx.rect(
|
||||||
headerMargin * 2 + categoriesSize,
|
categoriesBoxWidth + headerMargin * 2,
|
||||||
headerHeight + headerMargin,
|
headerHeight + headerMargin,
|
||||||
activeWidth,
|
ctx.measureText(firstTopMenuItemText).width + headerMargin * 2,
|
||||||
badgeHeight
|
badgeHeight
|
||||||
);
|
);
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
|
|
||||||
ctx.font = `${fontSize}px '${font}'`;
|
ctx.font = `${fontSize}px '${font}'`;
|
||||||
ctx.fillStyle = colors.secondary;
|
ctx.fillStyle = colors.secondary;
|
||||||
let x = headerMargin * 3.0 + categoriesSize;
|
const pillButtonTextY = headerHeight + headerMargin * 1.4 + fontSize;
|
||||||
|
const firstTopMenuItemX = headerMargin * 3.0 + categoriesBoxWidth;
|
||||||
ctx.fillText(
|
ctx.fillText(
|
||||||
text,
|
firstTopMenuItemText,
|
||||||
x - headerMargin * 0.1,
|
firstTopMenuItemX,
|
||||||
headerHeight + headerMargin * 1.5 + fontSize
|
pillButtonTextY,
|
||||||
|
ctx.measureText(firstTopMenuItemText).width
|
||||||
);
|
);
|
||||||
|
|
||||||
ctx.fillStyle = colors.primary;
|
ctx.fillStyle = colors.primary;
|
||||||
x += categoriesSize * (opts.categories ? 0.8 : 0.6);
|
|
||||||
ctx.fillText("New", x, headerHeight + headerMargin * 1.5 + fontSize);
|
|
||||||
|
|
||||||
x += categoriesSize * 0.4;
|
const newTextX =
|
||||||
ctx.fillText("Unread", x, headerHeight + headerMargin * 1.5 + fontSize);
|
firstTopMenuItemX +
|
||||||
|
ctx.measureText(firstTopMenuItemText).width +
|
||||||
|
headerMargin * 2.0;
|
||||||
|
ctx.fillText(newText, newTextX, pillButtonTextY);
|
||||||
|
|
||||||
x += categoriesSize * 0.6;
|
const unreadTextX =
|
||||||
ctx.fillText("Top", x, headerHeight + headerMargin * 1.5 + fontSize);
|
newTextX + ctx.measureText(newText).width + headerMargin * 2.0;
|
||||||
|
ctx.fillText(unreadText, unreadTextX, pillButtonTextY);
|
||||||
|
|
||||||
|
const topTextX =
|
||||||
|
unreadTextX + ctx.measureText(unreadText).width + headerMargin * 2.0;
|
||||||
|
ctx.fillText(topText, topTextX, pillButtonTextY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,19 +2,17 @@ import { action } from "@ember/object";
|
||||||
import { observes } from "@ember-decorators/object";
|
import { observes } from "@ember-decorators/object";
|
||||||
import { bind } from "discourse-common/utils/decorators";
|
import { bind } from "discourse-common/utils/decorators";
|
||||||
import { i18n } from "discourse-i18n";
|
import { i18n } from "discourse-i18n";
|
||||||
import { chooseDarker, darkLightDiff } from "../../../lib/preview";
|
import {
|
||||||
|
chooseDarker,
|
||||||
|
darkLightDiff,
|
||||||
|
resizeTextLinesToFitRect,
|
||||||
|
} from "../../../lib/preview";
|
||||||
import HomepagePreview from "./-homepage-preview";
|
import HomepagePreview from "./-homepage-preview";
|
||||||
import PreviewBaseComponent from "./-preview-base";
|
import PreviewBaseComponent from "./-preview-base";
|
||||||
|
|
||||||
const LOREM = `
|
|
||||||
Lorem ipsum dolor sit amet, consectetur adipiscing.
|
|
||||||
Nullam eget sem non elit tincidunt rhoncus. Fusce
|
|
||||||
velit nisl, porttitor sed nisl ac, consectetur interdum
|
|
||||||
metus. Fusce in consequat augue, vel facilisis felis.`;
|
|
||||||
|
|
||||||
export default class Index extends PreviewBaseComponent {
|
export default class Index extends PreviewBaseComponent {
|
||||||
width = 628;
|
width = 630;
|
||||||
height = 322;
|
height = 380;
|
||||||
logo = null;
|
logo = null;
|
||||||
avatar = null;
|
avatar = null;
|
||||||
previewTopic = true;
|
previewTopic = true;
|
||||||
|
@ -117,79 +115,94 @@ export default class Index extends PreviewBaseComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
paint({ ctx, colors, font, headingFont, width, height }) {
|
paint({ ctx, colors, font, headingFont, width, height }) {
|
||||||
const headerHeight = height * 0.3;
|
|
||||||
|
|
||||||
this.drawFullHeader(colors, headingFont, this.logo);
|
this.drawFullHeader(colors, headingFont, this.logo);
|
||||||
|
|
||||||
const margin = 20;
|
const margin = 20;
|
||||||
const avatarSize = height * 0.15;
|
const avatarSize = height * 0.1 + 5;
|
||||||
const lineHeight = height / 14;
|
const lineHeight = height / 14;
|
||||||
|
const leftHandTextGutter = margin + avatarSize + margin;
|
||||||
|
const timelineX = width * 0.86;
|
||||||
|
|
||||||
// Draw a fake topic
|
// Draw a fake topic
|
||||||
this.scaleImage(
|
this.scaleImage(
|
||||||
this.avatar,
|
this.avatar,
|
||||||
margin,
|
margin,
|
||||||
headerHeight + height * 0.09,
|
this.headerHeight + height * 0.22,
|
||||||
avatarSize,
|
avatarSize,
|
||||||
avatarSize
|
avatarSize
|
||||||
);
|
);
|
||||||
|
|
||||||
const titleFontSize = headerHeight / 55;
|
const titleFontSize = this.headerHeight / 30;
|
||||||
|
|
||||||
|
// Topic title
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.fillStyle = colors.primary;
|
ctx.fillStyle = colors.primary;
|
||||||
ctx.font = `bold ${titleFontSize}em '${headingFont}'`;
|
ctx.font = `700 ${titleFontSize}em '${headingFont}'`;
|
||||||
ctx.fillText(i18n("wizard.previews.topic_title"), margin, height * 0.3);
|
ctx.fillText(i18n("wizard.previews.topic_title"), margin, height * 0.3);
|
||||||
|
|
||||||
const bodyFontSize = height / 330.0;
|
// Topic OP text
|
||||||
|
const bodyFontSize = 1;
|
||||||
ctx.font = `${bodyFontSize}em '${font}'`;
|
ctx.font = `${bodyFontSize}em '${font}'`;
|
||||||
|
|
||||||
let line = 0;
|
let verticalLinePos = 0;
|
||||||
const lines = LOREM.split("\n");
|
const topicOp = i18n("wizard.homepage_preview.topic_ops.what_books");
|
||||||
for (let i = 0; i < 5; i++) {
|
const topicOpLines = topicOp.split("\n");
|
||||||
line = height * 0.35 + i * lineHeight;
|
|
||||||
ctx.fillText(lines[i], margin + avatarSize + margin, line);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Share Button
|
resizeTextLinesToFitRect(
|
||||||
const shareButtonWidth = i18n("wizard.previews.share_button").length * 11;
|
topicOpLines,
|
||||||
|
timelineX - leftHandTextGutter,
|
||||||
|
ctx,
|
||||||
|
bodyFontSize,
|
||||||
|
font,
|
||||||
|
(textLine, idx) => {
|
||||||
|
verticalLinePos = height * 0.4 + idx * lineHeight;
|
||||||
|
ctx.fillText(textLine, leftHandTextGutter, verticalLinePos);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
ctx.font = `${bodyFontSize}em '${font}'`;
|
||||||
|
|
||||||
|
// Share button
|
||||||
|
const shareButtonWidth =
|
||||||
|
Math.round(ctx.measureText(i18n("wizard.previews.share_button")).width) +
|
||||||
|
margin;
|
||||||
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.rect(margin, line + lineHeight, shareButtonWidth, height * 0.1);
|
ctx.rect(margin, verticalLinePos, shareButtonWidth, height * 0.1);
|
||||||
// accounts for hard-set color variables in solarized themes
|
// accounts for hard-set color variables in solarized themes
|
||||||
ctx.fillStyle =
|
ctx.fillStyle =
|
||||||
colors.primary_low ||
|
colors.primary_low ||
|
||||||
darkLightDiff(colors.primary, colors.secondary, 90, 65);
|
darkLightDiff(colors.primary, colors.secondary, 90, 65);
|
||||||
|
ctx.fill();
|
||||||
ctx.fillStyle = chooseDarker(colors.primary, colors.secondary);
|
ctx.fillStyle = chooseDarker(colors.primary, colors.secondary);
|
||||||
ctx.font = `${bodyFontSize}em '${font}'`;
|
|
||||||
ctx.fillText(
|
ctx.fillText(
|
||||||
i18n("wizard.previews.share_button"),
|
i18n("wizard.previews.share_button"),
|
||||||
margin + 10,
|
margin + 10,
|
||||||
line + lineHeight * 1.9
|
verticalLinePos + lineHeight * 0.9
|
||||||
);
|
);
|
||||||
|
|
||||||
// Reply Button
|
// Reply button
|
||||||
const replyButtonWidth = i18n("wizard.previews.reply_button").length * 11;
|
const replyButtonWidth =
|
||||||
|
Math.round(ctx.measureText(i18n("wizard.previews.reply_button")).width) +
|
||||||
|
margin;
|
||||||
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.rect(
|
ctx.rect(
|
||||||
shareButtonWidth + margin + 10,
|
shareButtonWidth + margin + 10,
|
||||||
line + lineHeight,
|
verticalLinePos,
|
||||||
replyButtonWidth,
|
replyButtonWidth,
|
||||||
height * 0.1
|
height * 0.1
|
||||||
);
|
);
|
||||||
ctx.fillStyle = colors.tertiary;
|
ctx.fillStyle = colors.tertiary;
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
ctx.fillStyle = colors.secondary;
|
ctx.fillStyle = colors.secondary;
|
||||||
ctx.font = `${bodyFontSize}em '${font}'`;
|
|
||||||
ctx.fillText(
|
ctx.fillText(
|
||||||
i18n("wizard.previews.reply_button"),
|
i18n("wizard.previews.reply_button"),
|
||||||
shareButtonWidth + margin + 20,
|
shareButtonWidth + margin * 2,
|
||||||
line + lineHeight * 1.9
|
verticalLinePos + lineHeight * 0.9
|
||||||
);
|
);
|
||||||
|
|
||||||
// Draw Timeline
|
// Draw timeline
|
||||||
const timelineX = width * 0.86;
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.strokeStyle = colors.tertiary;
|
ctx.strokeStyle = colors.tertiary;
|
||||||
ctx.lineWidth = 0.5;
|
ctx.lineWidth = 0.5;
|
||||||
|
@ -197,17 +210,30 @@ export default class Index extends PreviewBaseComponent {
|
||||||
ctx.lineTo(timelineX, height * 0.7);
|
ctx.lineTo(timelineX, height * 0.7);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
// Timeline
|
// Timeline handle
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.strokeStyle = colors.tertiary;
|
ctx.strokeStyle = colors.tertiary;
|
||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = 3;
|
||||||
ctx.moveTo(timelineX, height * 0.3);
|
ctx.moveTo(timelineX, height * 0.3 + 10);
|
||||||
ctx.lineTo(timelineX, height * 0.4);
|
ctx.lineTo(timelineX, height * 0.4);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
ctx.font = `Bold ${bodyFontSize}em ${font}`;
|
// Timeline post count
|
||||||
|
const postCountY = height * 0.3 + margin + 10;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.font = `700 ${bodyFontSize}em '${font}'`;
|
||||||
ctx.fillStyle = colors.primary;
|
ctx.fillStyle = colors.primary;
|
||||||
ctx.fillText("1 / 20", timelineX + margin, height * 0.3 + margin * 1.5);
|
ctx.fillText("1 / 20", timelineX + margin / 2, postCountY);
|
||||||
|
|
||||||
|
// Timeline post date
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.font = `${bodyFontSize * 0.9}em '${font}'`;
|
||||||
|
ctx.fillStyle = darkLightDiff(colors.primary, colors.secondary, 70, 65);
|
||||||
|
ctx.fillText(
|
||||||
|
"Nov 22",
|
||||||
|
timelineX + margin / 2,
|
||||||
|
postCountY + lineHeight * 0.75
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
|
|
@ -1,15 +1,5 @@
|
||||||
/*eslint no-bitwise:0 */
|
/*eslint no-bitwise:0 */
|
||||||
|
|
||||||
export const LOREM = `
|
|
||||||
Lorem ipsum dolor sit amet,
|
|
||||||
consectetur adipiscing elit.
|
|
||||||
Nullam eget sem non elit
|
|
||||||
tincidunt rhoncus. Fusce
|
|
||||||
velit nisl, porttitor sed
|
|
||||||
nisl ac, consectetur interdum
|
|
||||||
metus. Fusce in consequat
|
|
||||||
augue, vel facilisis felis.`;
|
|
||||||
|
|
||||||
export function parseColor(color) {
|
export function parseColor(color) {
|
||||||
const m = color.match(/^#([0-9a-f]{6})$/i);
|
const m = color.match(/^#([0-9a-f]{6})$/i);
|
||||||
if (m) {
|
if (m) {
|
||||||
|
@ -148,3 +138,26 @@ export function drawHeader(ctx, colors, width, headerHeight) {
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resizeTextLinesToFitRect(
|
||||||
|
textLines,
|
||||||
|
rectWidth,
|
||||||
|
ctx,
|
||||||
|
fontSize,
|
||||||
|
font,
|
||||||
|
renderCallback
|
||||||
|
) {
|
||||||
|
const maxLengthLine = textLines.reduce((a, b) =>
|
||||||
|
a.length > b.length ? a : b
|
||||||
|
);
|
||||||
|
|
||||||
|
let fontSizeDecreaseMultiplier = 1;
|
||||||
|
while (ctx.measureText(maxLengthLine).width > rectWidth) {
|
||||||
|
fontSizeDecreaseMultiplier -= 0.1;
|
||||||
|
ctx.font = `${fontSize * fontSizeDecreaseMultiplier}em '${font}'`;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < textLines.length; i++) {
|
||||||
|
renderCallback(textLines[i], i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -115,6 +115,9 @@ function rowHelper(row) {
|
||||||
el() {
|
el() {
|
||||||
return row;
|
return row;
|
||||||
},
|
},
|
||||||
|
hasClass(className) {
|
||||||
|
return row.classList.contains(className);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,255 @@
|
||||||
|
import { render } from "@ember/test-helpers";
|
||||||
|
import { module, test } from "qunit";
|
||||||
|
import Dropdown from "discourse/static/wizard/components/fields/dropdown";
|
||||||
|
import { Choice, Field } from "discourse/static/wizard/models/wizard";
|
||||||
|
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||||
|
import selectKit from "discourse/tests/helpers/select-kit-helper";
|
||||||
|
|
||||||
|
function buildFontChoices() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "arial",
|
||||||
|
label: "Arial",
|
||||||
|
classNames: "body-font-arial",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "helvetica",
|
||||||
|
label: "Helvetica",
|
||||||
|
classNames: "body-font-helvetica",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "lato",
|
||||||
|
label: "Lato",
|
||||||
|
classNames: "body-font-lato",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "montserrat",
|
||||||
|
label: "Montserrat",
|
||||||
|
classNames: "body-font-montserrat",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "noto_sans",
|
||||||
|
label: "NotoSans",
|
||||||
|
classNames: "body-font-noto-sans",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "roboto",
|
||||||
|
label: "Roboto",
|
||||||
|
classNames: "body-font-roboto",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ubuntu",
|
||||||
|
label: "Ubuntu",
|
||||||
|
classNames: "body-font-ubuntu",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
module(
|
||||||
|
"Integration | Component | Wizard | Fields | Dropdown",
|
||||||
|
function (hooks) {
|
||||||
|
setupRenderingTest(hooks);
|
||||||
|
|
||||||
|
test("color_scheme field sets colors data on each field choice to render palettes in dropdown", async function (assert) {
|
||||||
|
const lightColors = [
|
||||||
|
{
|
||||||
|
name: "primary",
|
||||||
|
hex: "222222",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "secondary",
|
||||||
|
hex: "ffffff",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tertiary",
|
||||||
|
hex: "0088cc",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "quaternary",
|
||||||
|
hex: "e45735",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "header_background",
|
||||||
|
hex: "ffffff",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "header_primary",
|
||||||
|
hex: "333333",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "highlight",
|
||||||
|
hex: "ffff4d",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "selected",
|
||||||
|
hex: "d1f0ff",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "hover",
|
||||||
|
hex: "f2f2f2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "danger",
|
||||||
|
hex: "c80001",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "success",
|
||||||
|
hex: "009900",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "love",
|
||||||
|
hex: "fa6c8d",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const darkColors = [
|
||||||
|
{
|
||||||
|
name: "primary",
|
||||||
|
hex: "dddddd",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "secondary",
|
||||||
|
hex: "222222",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tertiary",
|
||||||
|
hex: "099dd7",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "quaternary",
|
||||||
|
hex: "c14924",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "header_background",
|
||||||
|
hex: "111111",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "header_primary",
|
||||||
|
hex: "dddddd",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "highlight",
|
||||||
|
hex: "a87137",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "selected",
|
||||||
|
hex: "052e3d",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "hover",
|
||||||
|
hex: "313131",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "danger",
|
||||||
|
hex: "e45735",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "success",
|
||||||
|
hex: "1ca551",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "love",
|
||||||
|
hex: "fa6c8d",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const field = new Field({
|
||||||
|
type: "dropdown",
|
||||||
|
id: "color_scheme",
|
||||||
|
label: "Color palette",
|
||||||
|
choices: [
|
||||||
|
new Choice({
|
||||||
|
id: "light",
|
||||||
|
label: "Light",
|
||||||
|
data: { colors: lightColors },
|
||||||
|
}),
|
||||||
|
new Choice({
|
||||||
|
id: "dark",
|
||||||
|
label: "Dark",
|
||||||
|
data: { colors: darkColors },
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await render(<template><Dropdown @field={{field}} /></template>);
|
||||||
|
const colorPalettesSelector = selectKit(
|
||||||
|
".wizard-container__dropdown.color-palettes"
|
||||||
|
);
|
||||||
|
await colorPalettesSelector.expand();
|
||||||
|
|
||||||
|
lightColors
|
||||||
|
.reject((colorDef) => colorDef.name === "secondary")
|
||||||
|
.forEach((colorDef) => {
|
||||||
|
assert
|
||||||
|
.dom(
|
||||||
|
`.palettes .palette[style*='background-color:#${colorDef.hex}']`,
|
||||||
|
colorPalettesSelector.rowByValue("light").el()
|
||||||
|
)
|
||||||
|
.exists();
|
||||||
|
});
|
||||||
|
|
||||||
|
darkColors
|
||||||
|
.reject((colorDef) => colorDef.name === "secondary")
|
||||||
|
.forEach((colorDef) => {
|
||||||
|
assert
|
||||||
|
.dom(
|
||||||
|
`.palettes .palette[style*='background-color:#${colorDef.hex}']`,
|
||||||
|
colorPalettesSelector.rowByValue("dark").el()
|
||||||
|
)
|
||||||
|
.exists();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("body_font sets body-font-X classNames on each field choice", async function (assert) {
|
||||||
|
const fontChoices = buildFontChoices();
|
||||||
|
|
||||||
|
const field = new Field({
|
||||||
|
type: "dropdown",
|
||||||
|
id: "body_font",
|
||||||
|
label: "Body font",
|
||||||
|
choices: fontChoices.map((choice) => new Choice(choice)),
|
||||||
|
});
|
||||||
|
|
||||||
|
await render(<template><Dropdown @field={{field}} /></template>);
|
||||||
|
const fontSelector = selectKit(
|
||||||
|
".wizard-container__dropdown.font-selector"
|
||||||
|
);
|
||||||
|
await fontSelector.expand();
|
||||||
|
|
||||||
|
fontChoices.forEach((choice) => {
|
||||||
|
assert.true(
|
||||||
|
fontSelector
|
||||||
|
.rowByValue(choice.id)
|
||||||
|
.hasClass(`body-font-${choice.id.replace("_", "-")}`),
|
||||||
|
`has body-font-${choice.id} CSS class`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("heading_font sets heading-font-x classNames on each field choice", async function (assert) {
|
||||||
|
const fontChoices = buildFontChoices();
|
||||||
|
|
||||||
|
const field = new Field({
|
||||||
|
type: "dropdown",
|
||||||
|
id: "heading_font",
|
||||||
|
label: "heading font",
|
||||||
|
choices: fontChoices.map((choice) => new Choice(choice)),
|
||||||
|
});
|
||||||
|
|
||||||
|
await render(<template><Dropdown @field={{field}} /></template>);
|
||||||
|
const fontSelector = selectKit(
|
||||||
|
".wizard-container__dropdown.font-selector"
|
||||||
|
);
|
||||||
|
await fontSelector.expand();
|
||||||
|
|
||||||
|
fontChoices.forEach((choice) => {
|
||||||
|
assert.true(
|
||||||
|
fontSelector
|
||||||
|
.rowByValue(choice.id)
|
||||||
|
.hasClass(`heading-font-${choice.id.replace("_", "-")}`),
|
||||||
|
`has heading-font-${choice.id} CSS class`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { classNames } from "@ember-decorators/component";
|
||||||
|
import ComboBoxComponent from "select-kit/components/combo-box";
|
||||||
|
import { pluginApiIdentifiers, selectKitOptions } from "./select-kit";
|
||||||
|
|
||||||
|
@classNames("font-selector")
|
||||||
|
@pluginApiIdentifiers(["font-selector"])
|
||||||
|
@selectKitOptions({
|
||||||
|
selectedNameComponent: "selected-font",
|
||||||
|
})
|
||||||
|
export default class FontSelector extends ComboBoxComponent {
|
||||||
|
modifyComponentForRow() {
|
||||||
|
return "font-selector/font-selector-row";
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { classNames } from "@ember-decorators/component";
|
||||||
|
import SelectKitRowComponent from "select-kit/components/select-kit/select-kit-row";
|
||||||
|
|
||||||
|
@classNames("font-selector-row")
|
||||||
|
export default class FontSelectorRow extends SelectKitRowComponent {}
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { classNames } from "@ember-decorators/component";
|
||||||
|
import ComboBoxComponent from "select-kit/components/combo-box";
|
||||||
|
import { pluginApiIdentifiers } from "./select-kit";
|
||||||
|
|
||||||
|
@classNames("homepage-style-selector")
|
||||||
|
@pluginApiIdentifiers(["homepage-style-selector"])
|
||||||
|
export default class HomepageStyleSelector extends ComboBoxComponent {
|
||||||
|
modifyComponentForRow() {
|
||||||
|
return "homepage-style-selector/homepage-style-selector-row";
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
<div class="texts">
|
||||||
|
<span class="name">{{html-safe this.label}}</span>
|
||||||
|
{{#if this.item.description}}
|
||||||
|
<span class="desc">{{html-safe this.item.description}}</span>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { classNames } from "@ember-decorators/component";
|
||||||
|
import SelectKitRowComponent from "select-kit/components/select-kit/select-kit-row";
|
||||||
|
|
||||||
|
@classNames("homepage-style-selector-row")
|
||||||
|
export default class HomepageStyleSelectorRow extends SelectKitRowComponent {}
|
|
@ -0,0 +1,10 @@
|
||||||
|
import concatClass from "discourse/helpers/concat-class";
|
||||||
|
import SelectedNameComponent from "select-kit/components/selected-name";
|
||||||
|
|
||||||
|
export default class SelectedFont extends SelectedNameComponent {
|
||||||
|
<template>
|
||||||
|
<span class={{concatClass "name" this.item.classNames}}>
|
||||||
|
{{this.label}}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
}
|
|
@ -1,3 +1,6 @@
|
||||||
|
$blob-bg: absolute-image-url("/branded-background.svg");
|
||||||
|
$blob-mobile-bg: absolute-image-url("/branded-background-mobile.svg");
|
||||||
|
|
||||||
@keyframes bump {
|
@keyframes bump {
|
||||||
0% {
|
0% {
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
|
@ -13,7 +16,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
body.wizard {
|
body.wizard {
|
||||||
background-color: var(--primary-50);
|
background-color: var(--secondary);
|
||||||
|
background-image: $blob-bg;
|
||||||
|
background-size: 110vw 110vh; // crops better than cover at various viewport sizes
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: bottom;
|
||||||
|
|
||||||
color: var(--primary-very-high);
|
color: var(--primary-very-high);
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||||
Oxygen-Sans, Ubuntu, Cantarell, Arial, sans-serif;
|
Oxygen-Sans, Ubuntu, Cantarell, Arial, sans-serif;
|
||||||
|
@ -22,12 +30,18 @@ body.wizard {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@include breakpoint("mobile-extra-large") {
|
||||||
|
background: $blob-mobile-bg;
|
||||||
|
background-size: cover;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: bottom;
|
||||||
|
}
|
||||||
|
|
||||||
#wizard-main {
|
#wizard-main {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding-top: 1.5em;
|
padding-top: 1.5em;
|
||||||
background-image: absolute-image-url("/bubbles-bg.png");
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,6 +151,12 @@ body.wizard {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__step.styling .wizard-container__field.styling-preview-field {
|
||||||
|
label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&__field {
|
&__field {
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
}
|
}
|
||||||
|
@ -165,12 +185,18 @@ body.wizard {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__field.dropdown-field.dropdown-homepage-style {
|
||||||
|
.wizard-container__dropdown {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&__step-form {
|
&__step-form {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__sidebar {
|
&__sidebar {
|
||||||
width: 170px;
|
width: 230px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
margin-right: 1em;
|
margin-right: 1em;
|
||||||
|
|
||||||
|
@ -307,7 +333,6 @@ body.wizard {
|
||||||
|
|
||||||
&__step-text {
|
&__step-text {
|
||||||
display: inline;
|
display: inline;
|
||||||
margin-right: 0.25em;
|
|
||||||
@media only screen and (max-width: 568px) {
|
@media only screen and (max-width: 568px) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
@ -539,6 +564,7 @@ body.wizard {
|
||||||
|
|
||||||
&__dropdown {
|
&__dropdown {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 38px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__dropdown .select-kit-header:not(.btn) {
|
&__dropdown .select-kit-header:not(.btn) {
|
||||||
|
@ -553,6 +579,19 @@ body.wizard {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__dropdown.homepage-style-selector {
|
||||||
|
.select-kit-row {
|
||||||
|
.name {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desc {
|
||||||
|
display: block;
|
||||||
|
margin-top: 0.1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&__field.checkbox-field .wizard-container__label {
|
&__field.checkbox-field .wizard-container__label {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class ColorScheme < ActiveRecord::Base
|
class ColorScheme < ActiveRecord::Base
|
||||||
CUSTOM_SCHEMES = {
|
BUILT_IN_SCHEMES = {
|
||||||
Dark: {
|
Dark: {
|
||||||
"primary" => "dddddd",
|
"primary" => "dddddd",
|
||||||
"secondary" => "222222",
|
"secondary" => "222222",
|
||||||
|
@ -286,7 +286,7 @@ class ColorScheme < ActiveRecord::Base
|
||||||
|
|
||||||
list = [{ id: LIGHT_THEME_ID, colors: base_with_hash }]
|
list = [{ id: LIGHT_THEME_ID, colors: base_with_hash }]
|
||||||
|
|
||||||
CUSTOM_SCHEMES.each do |k, v|
|
BUILT_IN_SCHEMES.each do |k, v|
|
||||||
colors = []
|
colors = []
|
||||||
v.each { |name, color| colors << { name: name, hex: "#{color}" } }
|
v.each { |name, color| colors << { name: name, hex: "#{color}" } }
|
||||||
list.push(id: k.to_s, colors: colors)
|
list.push(id: k.to_s, colors: colors)
|
||||||
|
@ -385,7 +385,7 @@ class ColorScheme < ActiveRecord::Base
|
||||||
new_color_scheme.user_selectable = true
|
new_color_scheme.user_selectable = true
|
||||||
|
|
||||||
colors =
|
colors =
|
||||||
CUSTOM_SCHEMES[params[:base_scheme_id].to_sym]&.map do |name, hex|
|
BUILT_IN_SCHEMES[params[:base_scheme_id].to_sym]&.map do |name, hex|
|
||||||
{ name: name, hex: hex }
|
{ name: name, hex: hex }
|
||||||
end if params[:base_scheme_id]
|
end if params[:base_scheme_id]
|
||||||
colors ||= base.colors_hashes
|
colors ||= base.colors_hashes
|
||||||
|
@ -439,7 +439,7 @@ class ColorScheme < ActiveRecord::Base
|
||||||
|
|
||||||
def base_colors
|
def base_colors
|
||||||
colors = nil
|
colors = nil
|
||||||
colors = CUSTOM_SCHEMES[base_scheme_id.to_sym] if base_scheme_id && base_scheme_id != "Light"
|
colors = BUILT_IN_SCHEMES[base_scheme_id.to_sym] if base_scheme_id && base_scheme_id != "Light"
|
||||||
colors || ColorScheme.base_colors
|
colors || ColorScheme.base_colors
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -72,7 +72,7 @@ class SiteSetting < ActiveRecord::Base
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.top_menu_items
|
def self.top_menu_items
|
||||||
top_menu.split("|").map { |menu_item| TopMenuItem.new(menu_item) }
|
top_menu_map.map { |menu_item| TopMenuItem.new(menu_item) }
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.homepage
|
def self.homepage
|
||||||
|
|
|
@ -372,6 +372,10 @@ class Theme < ActiveRecord::Base
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.find_default
|
||||||
|
find_by(id: SiteSetting.default_theme_id)
|
||||||
|
end
|
||||||
|
|
||||||
def self.lookup_field(theme_id, target, field, skip_transformation: false, csp_nonce: nil)
|
def self.lookup_field(theme_id, target, field, skip_transformation: false, csp_nonce: nil)
|
||||||
return "" if theme_id.blank?
|
return "" if theme_id.blank?
|
||||||
|
|
||||||
|
|
|
@ -7408,8 +7408,42 @@ en:
|
||||||
regular: "Regular User"
|
regular: "Regular User"
|
||||||
|
|
||||||
previews:
|
previews:
|
||||||
topic_title: "A discussion topic heading"
|
topic_title: "What books are you reading?"
|
||||||
share_button: "Share"
|
share_button: "Share"
|
||||||
reply_button: "Reply"
|
reply_button: "Reply"
|
||||||
topic_preview: "Topic preview"
|
topic_preview: "Topic preview"
|
||||||
homepage_preview: "Homepage preview"
|
homepage_preview: "Homepage preview"
|
||||||
|
|
||||||
|
homepage_preview:
|
||||||
|
nav_buttons:
|
||||||
|
all_categories: "all categories"
|
||||||
|
new: "New"
|
||||||
|
unread: "Unread"
|
||||||
|
top: "Top"
|
||||||
|
latest: "Latest"
|
||||||
|
hot: "Hot"
|
||||||
|
categories: "Categories"
|
||||||
|
topic_titles:
|
||||||
|
what_books: "What books are you reading?"
|
||||||
|
what_movies: "What movies have you seen recently?"
|
||||||
|
random_fact: "Random fact of the day"
|
||||||
|
tv_show: "Recommend a TV show"
|
||||||
|
topic_ops:
|
||||||
|
what_books: |
|
||||||
|
We all love to read, let's use this topic to share our
|
||||||
|
current or recent reads. I'm a fantasy fan and I've been
|
||||||
|
re-reading The Lord of the Rings for the 100th time.
|
||||||
|
What about you?
|
||||||
|
category_descriptions:
|
||||||
|
icebreakers: "Get to know your fellow community members with fun questions."
|
||||||
|
news: "Discuss the latest news and events."
|
||||||
|
site_feedback: "Share your thoughts on the community and suggest improvements."
|
||||||
|
category_names:
|
||||||
|
icebreakers: "Icebreakers"
|
||||||
|
news: "News"
|
||||||
|
site_feedback: "Site Feedback"
|
||||||
|
table_headers:
|
||||||
|
topic: "Topic"
|
||||||
|
replies: "Replies"
|
||||||
|
views: "Views"
|
||||||
|
activity: "Activity"
|
||||||
|
|
|
@ -6264,23 +6264,17 @@ en:
|
||||||
label: "Homepage style"
|
label: "Homepage style"
|
||||||
choices:
|
choices:
|
||||||
latest:
|
latest:
|
||||||
label: "Latest Topics"
|
label: "Latest"
|
||||||
categories_only:
|
description: "Displays the most recently active topics in all categories, helping members stay up-to-date with discussions they care about"
|
||||||
label: "Categories Only"
|
hot:
|
||||||
categories_with_featured_topics:
|
label: "Hot"
|
||||||
label: "Categories with Featured Topics"
|
description: "Surfaces trending topics by blending recent and overall popularity, showcases what members are talking about in your community right now"
|
||||||
categories_and_latest_topics:
|
categories_and_latest_topics:
|
||||||
label: "Categories and Latest Topics"
|
label: "Categories with latest topics"
|
||||||
categories_and_latest_topics_created_date:
|
description: "Combines the recently active topics across all categories with a list of categories, their description, and total topics"
|
||||||
label: "Categories and Latest Topics (sort by topic created date)"
|
|
||||||
categories_and_top_topics:
|
|
||||||
label: "Categories and Top Topics"
|
|
||||||
categories_boxes:
|
categories_boxes:
|
||||||
label: "Categories Boxes"
|
label: "Category boxes"
|
||||||
categories_boxes_with_topics:
|
description: "Displays the categories and their description in a grid, ideal for members to see an overview of the sub-communities of your site"
|
||||||
label: "Categories Boxes with Topics"
|
|
||||||
subcategories_with_featured_topics:
|
|
||||||
label: "Subcategories with Featured Topics"
|
|
||||||
|
|
||||||
branding:
|
branding:
|
||||||
title: "Site logo"
|
title: "Site logo"
|
||||||
|
|
|
@ -86,7 +86,7 @@ module Stylesheet
|
||||||
.body-font-#{font[:key].tr("_", "-")} {
|
.body-font-#{font[:key].tr("_", "-")} {
|
||||||
font-family: #{font[:stack]};
|
font-family: #{font[:stack]};
|
||||||
}
|
}
|
||||||
.heading-font-#{font[:key].tr("_", "-")} h2 {
|
.heading-font-#{font[:key].tr("_", "-")} {
|
||||||
font-family: #{font[:stack]};
|
font-family: #{font[:stack]};
|
||||||
}
|
}
|
||||||
CSS
|
CSS
|
||||||
|
@ -222,7 +222,7 @@ module Stylesheet
|
||||||
)
|
)
|
||||||
contents << <<~CSS
|
contents << <<~CSS
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: #{font[:name]};
|
font-family: '#{font[:name]}';
|
||||||
src: #{src};
|
src: #{src};
|
||||||
font-weight: #{variant[:weight]};
|
font-weight: #{variant[:weight]};
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,20 @@ class Wizard
|
||||||
def build
|
def build
|
||||||
return @wizard unless SiteSetting.wizard_enabled? && @wizard.user.try(:staff?)
|
return @wizard unless SiteSetting.wizard_enabled? && @wizard.user.try(:staff?)
|
||||||
|
|
||||||
|
append_introduction_step
|
||||||
|
append_privacy_step
|
||||||
|
append_styling_step
|
||||||
|
append_ready_step
|
||||||
|
append_branding_step
|
||||||
|
append_corporate_step
|
||||||
|
|
||||||
|
DiscourseEvent.trigger(:build_wizard, @wizard)
|
||||||
|
@wizard
|
||||||
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
def append_introduction_step
|
||||||
@wizard.append_step("introduction") do |step|
|
@wizard.append_step("introduction") do |step|
|
||||||
step.emoji = "wave"
|
step.emoji = "wave"
|
||||||
step.description_vars = { base_path: Discourse.base_path }
|
step.description_vars = { base_path: Discourse.base_path }
|
||||||
|
@ -56,7 +70,9 @@ class Wizard
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def append_privacy_step
|
||||||
@wizard.append_step("privacy") do |step|
|
@wizard.append_step("privacy") do |step|
|
||||||
step.emoji = "hugs"
|
step.emoji = "hugs"
|
||||||
|
|
||||||
|
@ -93,12 +109,16 @@ class Wizard
|
||||||
updater.update_setting(:must_approve_users, updater.fields[:must_approve_users] == "yes")
|
updater.update_setting(:must_approve_users, updater.fields[:must_approve_users] == "yes")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def append_ready_step
|
||||||
@wizard.append_step("ready") do |step|
|
@wizard.append_step("ready") do |step|
|
||||||
# no form on this page, just info.
|
# no form on this page, just info.
|
||||||
step.emoji = "rocket"
|
step.emoji = "rocket"
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def append_branding_step
|
||||||
@wizard.append_step("branding") do |step|
|
@wizard.append_step("branding") do |step|
|
||||||
step.emoji = "framed_picture"
|
step.emoji = "framed_picture"
|
||||||
step.add_field(id: "logo", type: "image", value: SiteSetting.site_logo_url)
|
step.add_field(id: "logo", type: "image", value: SiteSetting.site_logo_url)
|
||||||
|
@ -112,10 +132,12 @@ class Wizard
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def append_styling_step
|
||||||
@wizard.append_step("styling") do |step|
|
@wizard.append_step("styling") do |step|
|
||||||
step.emoji = "art"
|
step.emoji = "art"
|
||||||
default_theme = Theme.find_by(id: SiteSetting.default_theme_id)
|
default_theme = Theme.find_default
|
||||||
default_theme_override = SiteSetting.exists?(name: "default_theme_id")
|
default_theme_override = SiteSetting.exists?(name: "default_theme_id")
|
||||||
|
|
||||||
base_scheme = default_theme&.color_scheme&.base_scheme_id
|
base_scheme = default_theme&.color_scheme&.base_scheme_id
|
||||||
|
@ -159,17 +181,20 @@ class Wizard
|
||||||
show_in_sidebar: true,
|
show_in_sidebar: true,
|
||||||
)
|
)
|
||||||
|
|
||||||
DiscourseFonts.fonts.each do |font|
|
DiscourseFonts
|
||||||
|
.fonts
|
||||||
|
.sort_by { |f| f[:name] }
|
||||||
|
.each do |font|
|
||||||
body_font.add_choice(font[:key], label: font[:name])
|
body_font.add_choice(font[:key], label: font[:name])
|
||||||
heading_font.add_choice(font[:key], label: font[:name])
|
heading_font.add_choice(font[:key], label: font[:name])
|
||||||
end
|
end
|
||||||
|
|
||||||
current =
|
current =
|
||||||
(
|
(
|
||||||
if SiteSetting.top_menu.starts_with?("categories")
|
if SiteSetting.homepage == "categories"
|
||||||
SiteSetting.desktop_category_page_style
|
SiteSetting.desktop_category_page_style
|
||||||
else
|
else
|
||||||
"latest"
|
SiteSetting.homepage
|
||||||
end
|
end
|
||||||
)
|
)
|
||||||
style =
|
style =
|
||||||
|
@ -181,7 +206,11 @@ class Wizard
|
||||||
show_in_sidebar: true,
|
show_in_sidebar: true,
|
||||||
)
|
)
|
||||||
style.add_choice("latest")
|
style.add_choice("latest")
|
||||||
CategoryPageStyle.values.each { |page| style.add_choice(page[:value]) }
|
style.add_choice("hot")
|
||||||
|
|
||||||
|
# Subset of CategoryPageStyle, we don't want to show all the options here.
|
||||||
|
style.add_choice("categories_and_latest_topics")
|
||||||
|
style.add_choice("categories_boxes")
|
||||||
|
|
||||||
step.add_field(id: "styling_preview", type: "styling-preview")
|
step.add_field(id: "styling_preview", type: "styling-preview")
|
||||||
|
|
||||||
|
@ -189,10 +218,11 @@ class Wizard
|
||||||
updater.update_setting(:base_font, updater.fields[:body_font])
|
updater.update_setting(:base_font, updater.fields[:body_font])
|
||||||
updater.update_setting(:heading_font, updater.fields[:heading_font])
|
updater.update_setting(:heading_font, updater.fields[:heading_font])
|
||||||
|
|
||||||
top_menu = SiteSetting.top_menu.split("|")
|
top_menu = SiteSetting.top_menu_map
|
||||||
if updater.fields[:homepage_style] == "latest" && top_menu[0] != "latest"
|
if %w[latest hot].include?(updater.fields[:homepage_style]) &&
|
||||||
top_menu.delete("latest")
|
top_menu.first != updater.fields[:homepage_style]
|
||||||
top_menu.insert(0, "latest")
|
top_menu.delete(updater.fields[:homepage_style])
|
||||||
|
top_menu.insert(0, updater.fields[:homepage_style])
|
||||||
elsif updater.fields[:homepage_style] != "latest"
|
elsif updater.fields[:homepage_style] != "latest"
|
||||||
top_menu.delete("categories")
|
top_menu.delete("categories")
|
||||||
top_menu.insert(0, "categories")
|
top_menu.insert(0, "categories")
|
||||||
|
@ -228,7 +258,9 @@ class Wizard
|
||||||
updater.refresh_required = true
|
updater.refresh_required = true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def append_corporate_step
|
||||||
@wizard.append_step("corporate") do |step|
|
@wizard.append_step("corporate") do |step|
|
||||||
step.emoji = "briefcase"
|
step.emoji = "briefcase"
|
||||||
step.description_vars = { base_path: Discourse.base_path }
|
step.description_vars = { base_path: Discourse.base_path }
|
||||||
|
@ -256,13 +288,8 @@ class Wizard
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
DiscourseEvent.trigger(:build_wizard, @wizard)
|
|
||||||
@wizard
|
|
||||||
end
|
end
|
||||||
|
|
||||||
protected
|
|
||||||
|
|
||||||
def replace_setting_value(updater, raw, field_name)
|
def replace_setting_value(updater, raw, field_name)
|
||||||
old_value = SiteSetting.get(field_name)
|
old_value = SiteSetting.get(field_name)
|
||||||
old_value = field_name if old_value.blank?
|
old_value = field_name if old_value.blank?
|
||||||
|
|
|
@ -8,6 +8,8 @@ Fabricator(:topic) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Fabricator(:topic_with_op, from: :topic) { after_create { |topic| Fabricate(:post, topic: topic) } }
|
||||||
|
|
||||||
Fabricator(:deleted_topic, from: :topic) { deleted_at { 1.minute.ago } }
|
Fabricator(:deleted_topic, from: :topic) { deleted_at { 1.minute.ago } }
|
||||||
|
|
||||||
Fabricator(:closed_topic, from: :topic) { closed true }
|
Fabricator(:closed_topic, from: :topic) { closed true }
|
||||||
|
|
|
@ -92,12 +92,12 @@ RSpec.describe Wizard::Builder do
|
||||||
|
|
||||||
describe "styling" do
|
describe "styling" do
|
||||||
let(:styling_step) { wizard.steps.find { |s| s.id == "styling" } }
|
let(:styling_step) { wizard.steps.find { |s| s.id == "styling" } }
|
||||||
let(:font_field) { styling_step.fields[1] }
|
let(:font_field) { styling_step.fields.find { |f| f.id == "body_font" } }
|
||||||
fab!(:theme)
|
fab!(:theme)
|
||||||
let(:colors_field) { styling_step.fields.first }
|
let(:colors_field) { styling_step.fields.first }
|
||||||
|
|
||||||
it "has the full list of available fonts" do
|
it "has the full list of available fonts in alphabetical order" do
|
||||||
expect(font_field.choices.size).to eq(DiscourseFonts.fonts.size)
|
expect(font_field.choices.map(&:label)).to eq(DiscourseFonts.fonts.map { |f| f[:name] }.sort)
|
||||||
end
|
end
|
||||||
|
|
||||||
context "with colors" do
|
context "with colors" do
|
||||||
|
|
|
@ -146,7 +146,7 @@ RSpec.describe ColorScheme do
|
||||||
|
|
||||||
it "falls back to default scheme if base scheme does not have color" do
|
it "falls back to default scheme if base scheme does not have color" do
|
||||||
custom_scheme_id = "BaseSchemeWithNoHighlightColor"
|
custom_scheme_id = "BaseSchemeWithNoHighlightColor"
|
||||||
ColorScheme::CUSTOM_SCHEMES[custom_scheme_id.to_sym] = { "secondary" => "123123" }
|
ColorScheme::BUILT_IN_SCHEMES[custom_scheme_id.to_sym] = { "secondary" => "123123" }
|
||||||
|
|
||||||
color_scheme = ColorScheme.new(base_scheme_id: custom_scheme_id)
|
color_scheme = ColorScheme.new(base_scheme_id: custom_scheme_id)
|
||||||
color_scheme.color_scheme_colors << ColorSchemeColor.new(name: "primary", hex: "121212")
|
color_scheme.color_scheme_colors << ColorSchemeColor.new(name: "primary", hex: "121212")
|
||||||
|
@ -156,7 +156,7 @@ RSpec.describe ColorScheme do
|
||||||
expect(resolved["secondary"]).to eq("123123") # From custom scheme
|
expect(resolved["secondary"]).to eq("123123") # From custom scheme
|
||||||
expect(resolved["tertiary"]).to eq("0088cc") # From `foundation/colors.scss`
|
expect(resolved["tertiary"]).to eq("0088cc") # From `foundation/colors.scss`
|
||||||
ensure
|
ensure
|
||||||
ColorScheme::CUSTOM_SCHEMES.delete(custom_scheme_id.to_sym)
|
ColorScheme::BUILT_IN_SCHEMES.delete(custom_scheme_id.to_sym)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "calculates 'hover' and 'selected' from existing db colors in dark mode" do
|
it "calculates 'hover' and 'selected' from existing db colors in dark mode" do
|
||||||
|
|
|
@ -3,17 +3,148 @@
|
||||||
module PageObjects
|
module PageObjects
|
||||||
module Pages
|
module Pages
|
||||||
class Wizard < PageObjects::Pages::Base
|
class Wizard < PageObjects::Pages::Base
|
||||||
|
attr_reader :introduction_step,
|
||||||
|
:privacy_step,
|
||||||
|
:ready_step,
|
||||||
|
:branding_step,
|
||||||
|
:styling_step,
|
||||||
|
:corporate_step
|
||||||
|
|
||||||
|
def initialize
|
||||||
|
@introduction_step = PageObjects::Pages::Wizard::IntroductionStep.new(self)
|
||||||
|
@privacy_step = PageObjects::Pages::Wizard::PrivacyStep.new(self)
|
||||||
|
@ready_step = PageObjects::Pages::Wizard::ReadyStep.new(self)
|
||||||
|
@branding_step = PageObjects::Pages::Wizard::BrandingStep.new(self)
|
||||||
|
@styling_step = PageObjects::Pages::Wizard::StylingStep.new(self)
|
||||||
|
@corporate_step = PageObjects::Pages::Wizard::CorporateStep.new(self)
|
||||||
|
end
|
||||||
|
|
||||||
|
def go_to_step(step_id)
|
||||||
|
visit("/wizard/steps/#{step_id}")
|
||||||
|
end
|
||||||
|
|
||||||
|
def on_step?(step_id)
|
||||||
|
has_css?(".wizard-container__step.#{step_id}")
|
||||||
|
end
|
||||||
|
|
||||||
def click_jump_in
|
def click_jump_in
|
||||||
find(".jump-in").click
|
find(".wizard-container__button.jump-in").click
|
||||||
|
end
|
||||||
|
|
||||||
|
def click_configure_more
|
||||||
|
find(".wizard-container__button.configure-more").click
|
||||||
end
|
end
|
||||||
|
|
||||||
def go_to_next_step
|
def go_to_next_step
|
||||||
find(".wizard-container__button.next").click
|
find(".wizard-container__button.next").click
|
||||||
end
|
end
|
||||||
|
|
||||||
def select_access_option(label)
|
def find_field(field_type, field_id)
|
||||||
find(".wizard-container__radio-choice", text: label).click
|
find(".wizard-container__field.#{field_type}-field.#{field_type}-#{field_id}")
|
||||||
|
end
|
||||||
|
|
||||||
|
def fill_field(field_type, field_id, value)
|
||||||
|
find_field(field_type, field_id).fill_in(with: value)
|
||||||
|
end
|
||||||
|
|
||||||
|
def has_field_with_value?(field_type, field_id, value)
|
||||||
|
find_field(field_type, field_id).find("input").value == value
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
class PageObjects::Pages::Wizard::StepBase < PageObjects::Pages::Base
|
||||||
|
attr_reader :wizard
|
||||||
|
|
||||||
|
def initialize(wizard)
|
||||||
|
@wizard = wizard
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class PageObjects::Pages::Wizard::IntroductionStep < PageObjects::Pages::Wizard::StepBase
|
||||||
|
end
|
||||||
|
|
||||||
|
class PageObjects::Pages::Wizard::PrivacyStep < PageObjects::Pages::Wizard::StepBase
|
||||||
|
def choice_selector(choice_id)
|
||||||
|
".wizard-container__radio-choice[data-choice-id='#{choice_id}']"
|
||||||
|
end
|
||||||
|
|
||||||
|
def select_access_option(section, choice_id)
|
||||||
|
wizard.find_field("radio", section).find(choice_selector(choice_id)).click
|
||||||
|
end
|
||||||
|
|
||||||
|
def has_selected_choice?(section, choice_id)
|
||||||
|
wizard.find_field("radio", section).has_css?(choice_selector(choice_id) + ".--selected")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class PageObjects::Pages::Wizard::ReadyStep < PageObjects::Pages::Wizard::StepBase
|
||||||
|
end
|
||||||
|
|
||||||
|
class PageObjects::Pages::Wizard::BrandingStep < PageObjects::Pages::Wizard::StepBase
|
||||||
|
def click_upload_button(field_id)
|
||||||
|
wizard.find_field("image", field_id).find(".wizard-container__button-upload").click
|
||||||
|
end
|
||||||
|
|
||||||
|
def has_upload?(field_id)
|
||||||
|
wizard.find_field("image", field_id).has_css?(".wizard-container__button-upload.has-upload")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class PageObjects::Pages::Wizard::StylingStep < PageObjects::Pages::Wizard::StepBase
|
||||||
|
def select_color_palette_option(palette)
|
||||||
|
select_kit =
|
||||||
|
PageObjects::Components::SelectKit.new(".dropdown-color-scheme .wizard-container__dropdown")
|
||||||
|
select_kit.expand
|
||||||
|
select_kit.select_row_by_value(palette)
|
||||||
|
end
|
||||||
|
|
||||||
|
def select_body_font_option(font)
|
||||||
|
select_kit =
|
||||||
|
PageObjects::Components::SelectKit.new(".dropdown-body-font .wizard-container__dropdown")
|
||||||
|
select_kit.expand
|
||||||
|
select_kit.select_row_by_value(font)
|
||||||
|
end
|
||||||
|
|
||||||
|
def select_heading_font_option(font)
|
||||||
|
select_kit =
|
||||||
|
PageObjects::Components::SelectKit.new(".dropdown-heading-font .wizard-container__dropdown")
|
||||||
|
select_kit.expand
|
||||||
|
select_kit.select_row_by_value(font)
|
||||||
|
end
|
||||||
|
|
||||||
|
def select_homepage_style_option(homepage)
|
||||||
|
select_kit =
|
||||||
|
PageObjects::Components::SelectKit.new(".dropdown-homepage-style .wizard-container__dropdown")
|
||||||
|
select_kit.expand
|
||||||
|
select_kit.select_row_by_value(homepage)
|
||||||
|
end
|
||||||
|
|
||||||
|
def has_selected_color_palette?(palette)
|
||||||
|
select_kit =
|
||||||
|
PageObjects::Components::SelectKit.new(".dropdown-color-scheme .wizard-container__dropdown")
|
||||||
|
select_kit.has_selected_value?(palette)
|
||||||
|
end
|
||||||
|
|
||||||
|
def has_selected_body_font?(font)
|
||||||
|
select_kit =
|
||||||
|
PageObjects::Components::SelectKit.new(".dropdown-body-font .wizard-container__dropdown")
|
||||||
|
select_kit.has_selected_value?(font)
|
||||||
|
end
|
||||||
|
|
||||||
|
def has_selected_heading_font?(font)
|
||||||
|
select_kit =
|
||||||
|
PageObjects::Components::SelectKit.new(".dropdown-heading-font .wizard-container__dropdown")
|
||||||
|
select_kit.has_selected_value?(font)
|
||||||
|
end
|
||||||
|
|
||||||
|
def has_selected_homepage_style?(hompage)
|
||||||
|
select_kit =
|
||||||
|
PageObjects::Components::SelectKit.new(".dropdown-homepage-style .wizard-container__dropdown")
|
||||||
|
select_kit.has_selected_value?(hompage)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class PageObjects::Pages::Wizard::CorporateStep < PageObjects::Pages::Wizard::StepBase
|
||||||
|
end
|
||||||
|
|
|
@ -2,55 +2,153 @@
|
||||||
|
|
||||||
describe "Wizard", type: :system do
|
describe "Wizard", type: :system do
|
||||||
fab!(:admin)
|
fab!(:admin)
|
||||||
fab!(:topic) { Fabricate(:topic, title: "admin guide with 15 chars") }
|
|
||||||
fab!(:post) { Fabricate(:post, topic: topic) }
|
|
||||||
|
|
||||||
let(:wizard_page) { PageObjects::Pages::Wizard.new }
|
let(:wizard_page) { PageObjects::Pages::Wizard.new }
|
||||||
|
|
||||||
before { sign_in(admin) }
|
before { sign_in(admin) }
|
||||||
|
|
||||||
|
it "successfully goes through every step of the wizard" do
|
||||||
|
visit("/wizard")
|
||||||
|
expect(wizard_page).to be_on_step("introduction")
|
||||||
|
wizard_page.fill_field("text", "title", "My Test Site")
|
||||||
|
wizard_page.go_to_next_step
|
||||||
|
expect(wizard_page).to be_on_step("privacy")
|
||||||
|
wizard_page.go_to_next_step
|
||||||
|
expect(wizard_page).to be_on_step("styling")
|
||||||
|
wizard_page.go_to_next_step
|
||||||
|
expect(wizard_page).to be_on_step("ready")
|
||||||
|
wizard_page.click_configure_more
|
||||||
|
expect(wizard_page).to be_on_step("branding")
|
||||||
|
wizard_page.go_to_next_step
|
||||||
|
expect(wizard_page).to be_on_step("corporate")
|
||||||
|
wizard_page.click_jump_in
|
||||||
|
expect(page).to have_current_path("/latest")
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "Wizard Step: Privacy" do
|
||||||
it "lets user configure member access" do
|
it "lets user configure member access" do
|
||||||
visit("/wizard/steps/privacy")
|
wizard_page.go_to_step("privacy")
|
||||||
|
expect(SiteSetting.login_required).to eq(false)
|
||||||
|
expect(SiteSetting.invite_only).to eq(false)
|
||||||
|
expect(SiteSetting.must_approve_users).to eq(false)
|
||||||
|
|
||||||
expect(page).to have_css(
|
expect(wizard_page.privacy_step).to have_selected_choice("login-required", "public")
|
||||||
".wizard-container__radio-choice.--selected",
|
expect(wizard_page.privacy_step).to have_selected_choice("invite-only", "sign_up")
|
||||||
text: I18n.t("wizard.step.privacy.fields.login_required.choices.public.label"),
|
expect(wizard_page.privacy_step).to have_selected_choice("must-approve-users", "no")
|
||||||
)
|
|
||||||
|
|
||||||
wizard_page.select_access_option("Private")
|
wizard_page.privacy_step.select_access_option("login-required", "private")
|
||||||
|
wizard_page.privacy_step.select_access_option("invite-only", "invite_only")
|
||||||
expect(page).to have_css(
|
wizard_page.privacy_step.select_access_option("must-approve-users", "yes")
|
||||||
".wizard-container__radio-choice.--selected",
|
|
||||||
text: I18n.t("wizard.step.privacy.fields.login_required.choices.private.label"),
|
|
||||||
)
|
|
||||||
|
|
||||||
wizard_page.go_to_next_step
|
wizard_page.go_to_next_step
|
||||||
|
|
||||||
expect(page).to have_current_path("/wizard/steps/ready")
|
expect(wizard_page).to be_on_step("styling")
|
||||||
expect(SiteSetting.login_required).to eq(true)
|
expect(SiteSetting.login_required).to eq(true)
|
||||||
|
expect(SiteSetting.invite_only).to eq(true)
|
||||||
|
expect(SiteSetting.must_approve_users).to eq(true)
|
||||||
|
|
||||||
visit("/wizard/steps/privacy")
|
wizard_page.go_to_step("privacy")
|
||||||
|
|
||||||
expect(page).to have_css(
|
expect(wizard_page.privacy_step).to have_selected_choice("login-required", "private")
|
||||||
".wizard-container__radio-choice.--selected",
|
expect(wizard_page.privacy_step).to have_selected_choice("invite-only", "invite_only")
|
||||||
text: I18n.t("wizard.step.privacy.fields.login_required.choices.private.label"),
|
expect(wizard_page.privacy_step).to have_selected_choice("must-approve-users", "yes")
|
||||||
)
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it "redirects to latest when wizard is completed" do
|
describe "Wizard Step: Branding" do
|
||||||
visit("/wizard/steps/ready")
|
let(:file_path_1) { file_from_fixtures("logo.png", "images").path }
|
||||||
|
let(:file_path_2) { file_from_fixtures("logo.jpg", "images").path }
|
||||||
|
|
||||||
|
it "lets user configure logos" do
|
||||||
|
wizard_page.go_to_step("branding")
|
||||||
|
expect(wizard_page).to be_on_step("branding")
|
||||||
|
attach_file(file_path_1) { wizard_page.branding_step.click_upload_button("logo") }
|
||||||
|
expect(wizard_page.branding_step).to have_upload("logo")
|
||||||
|
attach_file(file_path_2) { wizard_page.branding_step.click_upload_button("logo-small") }
|
||||||
|
expect(wizard_page.branding_step).to have_upload("logo-small")
|
||||||
|
wizard_page.go_to_next_step
|
||||||
|
expect(wizard_page).to be_on_step("corporate")
|
||||||
|
|
||||||
|
expect(SiteSetting.logo).to eq(Upload.find_by(original_filename: File.basename(file_path_1)))
|
||||||
|
expect(SiteSetting.logo_small).to eq(
|
||||||
|
Upload.find_by(original_filename: File.basename(file_path_2)),
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "Wizard Step: Styling" do
|
||||||
|
it "lets user configure styling including fonts and colors" do
|
||||||
|
wizard_page.go_to_step("styling")
|
||||||
|
expect(wizard_page).to be_on_step("styling")
|
||||||
|
|
||||||
|
wizard_page.styling_step.select_color_palette_option("Dark")
|
||||||
|
wizard_page.styling_step.select_body_font_option("lato")
|
||||||
|
wizard_page.styling_step.select_heading_font_option("merriweather")
|
||||||
|
wizard_page.styling_step.select_homepage_style_option("hot")
|
||||||
|
|
||||||
|
wizard_page.go_to_next_step
|
||||||
|
expect(wizard_page).to be_on_step("ready")
|
||||||
|
|
||||||
|
expect(Theme.find_default.color_scheme_id).to eq(
|
||||||
|
ColorScheme.find_by(base_scheme_id: "Dark", via_wizard: true).id,
|
||||||
|
)
|
||||||
|
expect(SiteSetting.base_font).to eq("lato")
|
||||||
|
expect(SiteSetting.heading_font).to eq("merriweather")
|
||||||
|
expect(SiteSetting.homepage).to eq("hot")
|
||||||
|
|
||||||
|
wizard_page.go_to_step("styling")
|
||||||
|
|
||||||
|
expect(wizard_page.styling_step).to have_selected_color_palette("Dark")
|
||||||
|
expect(wizard_page.styling_step).to have_selected_body_font("lato")
|
||||||
|
expect(wizard_page.styling_step).to have_selected_heading_font("merriweather")
|
||||||
|
expect(wizard_page.styling_step).to have_selected_homepage_style("hot")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "Wizard Step: Ready" do
|
||||||
|
it "redirects to latest" do
|
||||||
|
wizard_page.go_to_step("ready")
|
||||||
wizard_page.click_jump_in
|
wizard_page.click_jump_in
|
||||||
|
|
||||||
expect(page).to have_current_path("/latest")
|
expect(page).to have_current_path("/latest")
|
||||||
end
|
end
|
||||||
|
|
||||||
it "redirects to admin guide when wizard is completed and bootstrap mode is enabled" do
|
it "redirects to admin guide when bootstrap mode is enabled" do
|
||||||
|
topic = Fabricate(:topic_with_op, title: "Admin Getting Started Guide")
|
||||||
SiteSetting.bootstrap_mode_enabled = true
|
SiteSetting.bootstrap_mode_enabled = true
|
||||||
SiteSetting.admin_quick_start_topic_id = topic.id
|
SiteSetting.admin_quick_start_topic_id = topic.id
|
||||||
|
|
||||||
visit("/wizard/steps/ready")
|
wizard_page.go_to_step("ready")
|
||||||
wizard_page.click_jump_in
|
wizard_page.click_jump_in
|
||||||
|
|
||||||
expect(page).to have_current_path("/t/admin-guide-with-15-chars/#{topic.id}")
|
expect(page).to have_current_path(topic.url)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "Wizard Step: Corporate" do
|
||||||
|
it "lets user configure corporate including governing law and city for disputes" do
|
||||||
|
wizard_page.go_to_step("corporate")
|
||||||
|
expect(wizard_page).to be_on_step("corporate")
|
||||||
|
wizard_page.fill_field("text", "company-name", "ACME")
|
||||||
|
wizard_page.fill_field("text", "governing-law", "California")
|
||||||
|
wizard_page.fill_field("text", "contact-url", "https://ac.me")
|
||||||
|
wizard_page.fill_field("text", "city-for-disputes", "San Francisco")
|
||||||
|
wizard_page.fill_field("text", "contact-email", "coyote@ac.me")
|
||||||
|
wizard_page.click_jump_in
|
||||||
|
expect(page).to have_current_path("/latest")
|
||||||
|
|
||||||
|
expect(SiteSetting.company_name).to eq("ACME")
|
||||||
|
expect(SiteSetting.governing_law).to eq("California")
|
||||||
|
expect(SiteSetting.city_for_disputes).to eq("San Francisco")
|
||||||
|
expect(SiteSetting.contact_url).to eq("https://ac.me")
|
||||||
|
expect(SiteSetting.contact_email).to eq("coyote@ac.me")
|
||||||
|
|
||||||
|
wizard_page.go_to_step("corporate")
|
||||||
|
expect(wizard_page).to have_field_with_value("text", "company-name", "ACME")
|
||||||
|
expect(wizard_page).to have_field_with_value("text", "governing-law", "California")
|
||||||
|
expect(wizard_page).to have_field_with_value("text", "contact-url", "https://ac.me")
|
||||||
|
expect(wizard_page).to have_field_with_value("text", "city-for-disputes", "San Francisco")
|
||||||
|
expect(wizard_page).to have_field_with_value("text", "contact-email", "coyote@ac.me")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue
Block a user