diff --git a/javascripts/discourse/components/sticky-sidebar.hbs b/javascripts/discourse/components/sticky-sidebar.hbs new file mode 100644 index 0000000..b8105a5 --- /dev/null +++ b/javascripts/discourse/components/sticky-sidebar.hbs @@ -0,0 +1,17 @@ + \ No newline at end of file diff --git a/javascripts/discourse/components/sticky-sidebar.js b/javascripts/discourse/components/sticky-sidebar.js new file mode 100644 index 0000000..d9e4241 --- /dev/null +++ b/javascripts/discourse/components/sticky-sidebar.js @@ -0,0 +1,78 @@ +import Component from "@glimmer/component"; +import { tracked } from "@glimmer/tracking"; +import { action } from "@ember/object"; + +export default class StickySidebar extends Component { + @tracked top = 0; + @tracked bottom = 0; + @tracked position = "relative"; + + offset = 0; + prevScrollTop = 0; + yOrigin = 0; + mode = "unset"; + + @action + onScroll() { + const scrollY = window.scrollY; + const scrollingUp = scrollY < this.prevScrollTop; + const scrollingDown = scrollY > this.prevScrollTop; + const element = this.element; + if (!this.yOrigin) { + // save the initial vertical position + this.yOrigin = getYOrigin(this.element); + } + const stickyTop = this.yOrigin; + + if (scrollingUp) { + if (isTopInView(element, this.yOrigin)) { + this.mode = "top"; + this.position = "fixed"; + this.top = stickyTop; + this.bottom = "unset"; + } + if (this.mode === "bottom") { + this.mode = "between"; + const top = element.getBoundingClientRect().top; + this.position = "relative"; + this.top = top + scrollY - this.yOrigin; + } + } else if (scrollingDown) { + if (isBottomInView(element, this.yOrigin)) { + this.mode = "bottom"; + this.position = "fixed"; + this.bottom = 0; + this.top = "unset"; + } + if (this.mode === "top") { + this.mode = "between"; + const top = element.getBoundingClientRect().top; + this.position = "relative"; + this.top = top + scrollY - this.yOrigin; + } + } + this.prevScrollTop = scrollY; + } + + @action + didInsert(element) { + this.element = element; + this.offset = this.element.offsetTop; + } +} + +function isTopInView(element, yOffset) { + const rect = element.getBoundingClientRect(); + return rect.top >= yOffset && rect.top <= window.innerHeight; +} + +function isBottomInView(element) { + const rect = element.getBoundingClientRect(); + return rect.bottom >= 0 && rect.bottom <= window.innerHeight; +} + +function getYOrigin(el) { + const rect = el.getBoundingClientRect(); + const scrollTop = window.scrollY || window.pageYOffset; + return rect.top + scrollTop; +} diff --git a/javascripts/discourse/connectors/before-main-outlet/blocks.gjs b/javascripts/discourse/connectors/before-main-outlet/blocks.gjs index 2d7bffe..cc2a653 100644 --- a/javascripts/discourse/connectors/before-main-outlet/blocks.gjs +++ b/javascripts/discourse/connectors/before-main-outlet/blocks.gjs @@ -7,6 +7,7 @@ import BlockProfile from "../../components/blocks/profile"; import BlockTime from "../../components/blocks/time"; import BlockTopContributors from "../../components/blocks/top-contributors"; import BlockTopTopics from "../../components/blocks/top-topics"; +import StickySidebarComponent from "../../components/sticky-sidebar"; export default class BlocksComponent extends Component { @service currentUser; @@ -39,24 +40,26 @@ export default class BlocksComponent extends Component { } diff --git a/javascripts/discourse/connectors/discovery-navigation-bar-above/navigation.gjs b/javascripts/discourse/connectors/discovery-navigation-bar-above/navigation.gjs index de43d88..adc3bbd 100644 --- a/javascripts/discourse/connectors/discovery-navigation-bar-above/navigation.gjs +++ b/javascripts/discourse/connectors/discovery-navigation-bar-above/navigation.gjs @@ -1,34 +1,186 @@ import Component from "@glimmer/component"; -// import { tracked } from "@glimmer/tracking"; -// import { concat, get } from "@ember/helper"; -// import { action } from "@ember/object"; +import { tracked } from "@glimmer/tracking"; +import { on } from "@ember/modifier"; +import { action } from "@ember/object"; import { service } from "@ember/service"; -// import { htmlSafe } from "@ember/template"; -// import { eq, or } from "truth-helpers"; -// import avatar from "discourse/helpers/avatar"; -// import categoryLink from "discourse/helpers/category-link"; -// import concatClass from "discourse/helpers/concat-class"; -// import number from "discourse/helpers/number"; -// import replaceEmoji from "discourse/helpers/replace-emoji"; -// import { ajax } from "discourse/lib/ajax"; -// import Category from "discourse/models/category"; +import { capitalize } from "@ember/string"; +import { eq } from "truth-helpers"; import i18n from "discourse-common/helpers/i18n"; export default class Breadcrumbs extends Component { @service router; @service site; + @tracked routeType; + + @action + updateRouteType() { + if (this.router?.currentRoute?.parent?.name === "discovery") { + switch (this.router?.currentRoute?.localName) { + case "latest": + case "hot": + case "top": + case "new": + case "unread": + this.routeType = "home"; + break; + case "category": + case "latestCategory": + case "hotCategory": + case "topCategory": + case "newCategory": + case "unreadCategory": + this.routeType = "category"; + break; + case "categories": + this.routeType = "categories"; + break; + default: + this.routeType = null; + break; + } + } else { + this.routeType = null; + } + } + + get filterType() { + if (this.router?.currentRoute?.localName === "categories") { + return "categories"; + } + return this.router?.currentRoute?.attributes?.filterType || ""; + } + get isHomepage() { - return this.router.currentRouteName === "discovery.latest"; + this.updateRouteType(); + return this.routeType === "home"; + } + + get isCategoryView() { + this.updateRouteType(); + return this.routeType === "category"; + } + + get isCategoryList() { + this.updateRouteType(); + return this.routeType === "categories"; + } + + get categoryName() { + return this.router?.currentRoute?.attributes?.category?.name || "Category"; + } + + get categoryBadge() { + const defaultBadge = settings.default_category_badge || "📁"; + + const badge = settings.category_icons?.find( + (category) => + category.id[0] === this.router?.currentRoute?.attributes?.category?.id + )?.emoji; + + if (!badge) { + return defaultBadge; + } + + try { + // check for valid emoji + const regex = /\p{Emoji}/u; + if (regex.test(badge)) { + return badge; + } + return defaultBadge; + } catch (e) { + // \p{Emoji} not supported -> skip validation + return badge; + } + } + + @action + home() { + this.router.transitionTo("/"); } } +class TopicFilter extends Component { + @service router; + @service site; + + @tracked filterOptions; + + constructor() { + super(...arguments); + + this.filterOptions = + this.site.siteSettings?.top_menu?.split("|").map((item) => { + return { name: item, localization: `js.filters.${item}.title` }; + }) || []; + } + + @action + filterTopics(event) { + const routeType = this.args.routeType; + const category = this.router?.currentRoute?.attributes?.category; + + if (routeType === "category" && event.target.value !== "categories") { + this.router.transitionTo( + `/c/${category.slug}/${category.id}/l/${event.target.value}` + ); + } else { + this.router.transitionTo(`/${event.target.value}`); + } + } + + +} diff --git a/javascripts/discourse/connectors/topic-title/back.hbs b/javascripts/discourse/connectors/topic-title/back.hbs new file mode 100644 index 0000000..67827d7 --- /dev/null +++ b/javascripts/discourse/connectors/topic-title/back.hbs @@ -0,0 +1,4 @@ +{{#unless this.model.isPrivateMessage}} + + +{{/unless}} \ No newline at end of file diff --git a/javascripts/discourse/modifiers/scroll.js b/javascripts/discourse/modifiers/scroll.js new file mode 100644 index 0000000..09a8c22 --- /dev/null +++ b/javascripts/discourse/modifiers/scroll.js @@ -0,0 +1,11 @@ +import { modifier } from 'ember-modifier'; + +export default modifier((element, [callback]) => { + const handleScroll = () => callback(); + + window.addEventListener('scroll', handleScroll); + + return () => { + window.removeEventListener('scroll', handleScroll); + }; +}); diff --git a/scss/components/navigation.scss b/scss/components/navigation.scss index cfa571a..de83a1e 100644 --- a/scss/components/navigation.scss +++ b/scss/components/navigation.scss @@ -326,6 +326,7 @@ height: 4.5rem; border-bottom: var(--border-outer); + padding: 0 1rem; &__title { @include headline-small; @@ -335,12 +336,34 @@ align-items: center; margin: 0; - padding: 0 1rem; - - &::before { - @include i; + .badge { + &[data-badge-type="icon"]{ + @include i; + } + &[data-badge-type="emoji"]{ + width: 1.25rem; + line-height: 1; + font-size: 1.25rem; + } + &[data-clickable="true"]{ + cursor: pointer; + } + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; padding: 0 0.375rem; } } + &__select { + width: auto; + max-width: 180px; + + margin-left: auto; + margin-bottom: 0; + + border: none; + padding: 0 3px; + } } diff --git a/scss/discourse/topic.scss b/scss/discourse/topic.scss index 657715b..a836068 100644 --- a/scss/discourse/topic.scss +++ b/scss/discourse/topic.scss @@ -1,4 +1,5 @@ #topic-title { + display: flex; border-bottom: var(--border-outer); padding: 1rem; margin: 0; @@ -9,6 +10,18 @@ color: var(--neutral-10); } } + .topic-title-outlet { + padding-top: 8px; + order: -1; + .topic-back-button { + &:before { + @include i; + padding-left: 12px; + padding-right: 16px; + content: "arrow_back"; + } + } + } } .more-topics__container .nav { @@ -187,4 +200,4 @@ } } } -} +} \ No newline at end of file diff --git a/scss/layout.scss b/scss/layout.scss index 63b3e05..99dfa63 100644 --- a/scss/layout.scss +++ b/scss/layout.scss @@ -91,6 +91,10 @@ body { padding-left: 0; } + .sticky-sidebar { + display: unset; + } + .blocks { grid-area: right; } diff --git a/settings.yml b/settings.yml index 004ee1d..ddafa38 100644 --- a/settings.yml +++ b/settings.yml @@ -122,3 +122,21 @@ blocks: default: 1x1 period: type: null +category_badges: + type: objects + default: [] + schema: + name: category_badge + properties: + category: + type: categories + required: true + badge: + type: string + description: a single emoji to use as a badge (copy and paste from https://emojipedia.org/twitter) + max_length: 1 + required: true +default_category_icon: + default: 📁 + description: a single emoji to use as a default category icon (copy and paste from https://emojipedia.org/twitter) + max: 1 \ No newline at end of file