From 17e9f455896776a48f145a1d4f7c0a4fdb45fd89 Mon Sep 17 00:00:00 2001 From: Matt Roberts Date: Tue, 2 Dec 2025 17:03:01 +0000 Subject: [PATCH 01/17] Home page design refresh (again) --- public/assets/images/chevron-up.svg | 3 + public/assets/images/heroicons-filter.svg | 2 +- public/assets/styles/utility/display.scss | 8 +++ public/assets/styles/variables.scss | 8 ++- public/assets/styles/variables/_text.scss | 1 + public/components/Header.tsx | 17 ++++-- public/components/VoteCounter.scss | 59 ++++++++++++------- public/components/VoteCounter.tsx | 10 ++-- public/pages/Home/Home.page.scss | 59 +++++++++++++++---- public/pages/Home/Home.page.tsx | 27 ++++++--- public/pages/Home/components/ListPosts.tsx | 21 +++---- public/pages/Home/components/PostFilter.scss | 25 ++++++++ public/pages/Home/components/PostFilter.tsx | 2 +- .../pages/Home/components/PostsContainer.scss | 50 +++++++++++++++- .../pages/Home/components/PostsContainer.tsx | 30 +++++----- public/pages/Home/components/PostsSort.tsx | 2 +- 16 files changed, 243 insertions(+), 81 deletions(-) create mode 100644 public/assets/images/chevron-up.svg diff --git a/public/assets/images/chevron-up.svg b/public/assets/images/chevron-up.svg new file mode 100644 index 000000000..0ef067896 --- /dev/null +++ b/public/assets/images/chevron-up.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/images/heroicons-filter.svg b/public/assets/images/heroicons-filter.svg index 949a0acaf..ffa3684c1 100644 --- a/public/assets/images/heroicons-filter.svg +++ b/public/assets/images/heroicons-filter.svg @@ -1,3 +1,3 @@ - + diff --git a/public/assets/styles/utility/display.scss b/public/assets/styles/utility/display.scss index dd6c23b30..c8084139b 100644 --- a/public/assets/styles/utility/display.scss +++ b/public/assets/styles/utility/display.scss @@ -67,6 +67,14 @@ } } +@include media("xl2") { + .container { + width: 1320px; + margin-left: auto; + margin-right: auto; + } +} + .box { border-radius: get("border.radius.large"); background-color: var(--colors-white); diff --git a/public/assets/styles/variables.scss b/public/assets/styles/variables.scss index a2ffecf70..38b566e85 100644 --- a/public/assets/styles/variables.scss +++ b/public/assets/styles/variables.scss @@ -30,17 +30,19 @@ $all: ( small: 6px, medium: 8px, large: 12px, + xlarge: 20px, full: 999px, ), ), ); // Queries -$medias: "sm", "md", "lg", "xl"; +$medias: "sm", "md", "lg", "xl", "xl2"; $sm-width: 576px; $md-width: 768px; $lg-width: 992px; $xl-width: 1200px; +$xl2-width: 1400px; @mixin media($media) { @if $media == "sm" { @@ -59,6 +61,10 @@ $xl-width: 1200px; @media only screen and (min-width: #{$xl-width}) { @content; } + } @else if $media == "xl2" { + @media only screen and (min-width: #{$xl2-width}) { + @content; + } } } diff --git a/public/assets/styles/variables/_text.scss b/public/assets/styles/variables/_text.scss index 015668900..0ef975686 100644 --- a/public/assets/styles/variables/_text.scss +++ b/public/assets/styles/variables/_text.scss @@ -22,5 +22,6 @@ $font: ( medium: 500, semibold: 600, bold: 700, + xbold: 800, ), ); diff --git a/public/components/Header.tsx b/public/components/Header.tsx index e841b5a04..64b36af7e 100644 --- a/public/components/Header.tsx +++ b/public/components/Header.tsx @@ -1,10 +1,11 @@ import React, { useState } from "react" -import { SignInModal, RSSModal, TenantLogo, NotificationIndicator, UserMenu, ThemeSwitcher, Icon } from "@fider/components" +import { SignInModal, RSSModal, TenantLogo, NotificationIndicator, UserMenu, ThemeSwitcher, Icon, Button } from "@fider/components" import { useFider } from "@fider/hooks" import { HStack } from "./layout" import { Trans } from "@lingui/react/macro" import { i18n } from "@lingui/core" import IconRss from "@fider/assets/images/heroicons-rss.svg" +import IconArrowLeft from "@fider/assets/images/heroicons-arrowleft.svg" interface HeaderProps { hasInert?: boolean @@ -15,8 +16,7 @@ export const Header = (props: HeaderProps) => { const [isSignInModalOpen, setIsSignInModalOpen] = useState(false) const [isRSSModalOpen, setIsRSSModalOpen] = useState(false) - const showSignInModal = (e: React.MouseEvent) => { - e.preventDefault() + const handleSignInClick = () => { setIsSignInModalOpen(true) } @@ -60,9 +60,14 @@ export const Header = (props: HeaderProps) => { )} - - Sign in - + )} diff --git a/public/components/VoteCounter.scss b/public/components/VoteCounter.scss index 40d7926a7..37a8be575 100644 --- a/public/components/VoteCounter.scss +++ b/public/components/VoteCounter.scss @@ -2,58 +2,73 @@ .c-vote-counter { &__button { - font-size: get("font.size.lg"); - width: sizing(12); - height: sizing(12); + width: sizing(14); + height: sizing(14); cursor: pointer; text-align: center; margin: 0 auto; - padding: 3px 0 8px 0; - color: var(--colors-gray-600); + padding: spacing(3) 0; + color: var(--colors-gray-500); display: flex; flex-direction: column; align-items: center; - border-radius: 8px; + justify-content: center; + gap: spacing(2); + border-radius: 12px; transition: all 0.15s ease; - background-color: transparent; - border: 1px solid transparent; - - svg { - color: var(--colors-gray-600); - margin-bottom: -2px; - transition: all 0.15s ease; - } + background-color: var(--colors-gray-50); + border: 1px solid var(--colors-gray-200); &:hover { - background-color: var(--colors-gray-50); + background-color: var(--colors-white); + border-color: var(--colors-gray-300); color: var(--colors-primary-base); - svg { + + .c-vote-counter__icon { color: var(--colors-primary-base); - transform: scale(1.1); + transform: translateY(-1px); } } &--voted { - justify-content: center; - padding: 0; + padding: spacing(3) 0; + gap: spacing(2); font-weight: get("font.weight.bold"); background-color: var(--colors-blue-50); color: var(--colors-primary-base); border-color: var(--colors-primary-light); - svg { + .c-vote-counter__icon { color: var(--colors-primary-base); } + .c-vote-counter__count { + font-weight: get("font.weight.bold"); + } + &:hover { background-color: var(--colors-blue-100); } } &--disabled { - justify-content: center; - padding: 0; + padding: spacing(3) 0; + gap: spacing(2); @include disabled(); } } + + &__icon { + width: 18px; + height: 18px; + color: var(--colors-gray-500); + transition: all 0.15s ease; + flex-shrink: 0; + } + + &__count { + font-size: get("font.size.sm"); + font-weight: get("font.weight.semibold"); + line-height: 1; + } } diff --git a/public/components/VoteCounter.tsx b/public/components/VoteCounter.tsx index 5604e153b..28f83bd1b 100644 --- a/public/components/VoteCounter.tsx +++ b/public/components/VoteCounter.tsx @@ -5,7 +5,7 @@ import { Post, PostStatus } from "@fider/models" import { actions, classSet } from "@fider/services" import { Icon, SignInModal } from "@fider/components" import { useFider } from "@fider/hooks" -import FaCaretUp from "@fider/assets/images/fa-caretup.svg" +import ChevronUp from "@fider/assets/images/chevron-up.svg" export interface VoteCounterProps { post: Post @@ -46,15 +46,15 @@ export const VoteCounter = (props: VoteCounterProps) => { const vote = ( ) const disabled = ( ) diff --git a/public/pages/Home/Home.page.scss b/public/pages/Home/Home.page.scss index ee5910943..c7b8d1ca1 100644 --- a/public/pages/Home/Home.page.scss +++ b/public/pages/Home/Home.page.scss @@ -5,6 +5,9 @@ grid-template-columns: 1fr; column-gap: 0; row-gap: spacing(8); + background-color: var(--colors-gray-100); + padding-top: spacing(12); + padding-bottom: spacing(12); @include media("lg") { grid-template-columns: 1fr 1fr 1fr; @@ -14,27 +17,63 @@ .p-home { &__welcome-col { - > :first-child { - background-color: var(--colors-white); - border-radius: get("border.radius.large"); - border: 1px solid var(--colors-gray-200); + @include media("lg") { + padding-right: spacing(4); + } + } + + &__welcome-title { + font-size: 32px; + font-weight: get("font.weight.xbold"); + line-height: 1.2; + color: var(--colors-gray-900); + letter-spacing: -0.5px; - @include media("lg") { - } + @include media("lg") { + font-size: 36px; } } + &__welcome-body { + // font-size: get("font.size.lg"); + line-height: 1.6; + // color: var(--colors-gray-700); + } + &__posts-col { + grid-column: span 2 / span 2; + } + + &__add-idea-btn { + width: 100%; + margin-bottom: spacing(6); + padding: spacing(5); background-color: var(--colors-white); - border-radius: get("border.radius.large"); border: 1px solid var(--colors-gray-200); + border-radius: get("border.radius.xlarge"); + box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1); + cursor: pointer; + transition: box-shadow 0.2s ease-in-out; + font-size: get("font.size.lg"); + font-weight: get("font.weight.medium"); + color: var(--colors-gray-700); + text-align: left; - @include media("lg") { + &:hover { + box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); + } + + &:focus { + outline: 2px solid var(--colors-primary-base); + outline-offset: 2px; } } - &__posts-col { - grid-column: span 2 / span 2; + &__add-idea-icon { + width: 40px; + height: 40px; + color: var(--colors-primary-base); + flex-shrink: 0; } } } diff --git a/public/pages/Home/Home.page.tsx b/public/pages/Home/Home.page.tsx index f7a30b6af..a64c82cd4 100644 --- a/public/pages/Home/Home.page.tsx +++ b/public/pages/Home/Home.page.tsx @@ -1,12 +1,13 @@ import "./Home.page.scss" import NoDataIllustration from "@fider/assets/images/undraw-no-data.svg" +import IconPlusCircle from "@fider/assets/images/heroicons-pluscircle.svg" import React, { useEffect, useState } from "react" import { Post, Tag, PostStatus } from "@fider/models" -import { Markdown, Hint, PoweredByFider, Icon, Header, Button } from "@fider/components" +import { Markdown, Hint, PoweredByFider, Icon, Header } from "@fider/components" import { PostsContainer } from "./components/PostsContainer" import { useFider } from "@fider/hooks" -import { VStack } from "@fider/components/layout" +import { VStack, HStack } from "@fider/components/layout" import { ShareFeedback } from "./components/ShareFeedback" import { i18n } from "@lingui/core" import { Trans } from "@lingui/react/macro" @@ -95,17 +96,25 @@ What can we do better? This is the place for you to vote, discuss and share idea
- - - + +
+

+ Help us build the best feedback platform. +

+ +
-
setIsShareFeedbackOpen(true)}> +
-
+
+ {isLonely() ? : }
diff --git a/public/pages/Home/components/ListPosts.tsx b/public/pages/Home/components/ListPosts.tsx index 341d5ba87..a6d3d04d6 100644 --- a/public/pages/Home/components/ListPosts.tsx +++ b/public/pages/Home/components/ListPosts.tsx @@ -13,23 +13,24 @@ interface ListPostsProps { const ListPostItem = (props: { post: Post; user?: CurrentUser; tags: Tag[] }) => { return ( - +
- + {props.post.status !== "open" && ( -
+
)} - - + + {props.post.title} {props.post.commentsCount > 0 && ( - - {props.post.commentsCount} + + {props.post.commentsCount} + )} @@ -48,7 +49,7 @@ const ListPostItem = (props: { post: Post; user?: CurrentUser; tags: Tag[] }) => const MinimalListPostItem = (props: { post: Post; tags: Tag[] }) => { return ( - + {props.post.title} @@ -85,11 +86,11 @@ export const ListPosts = (props: ListPostsProps) => { ))} ) : ( - + <> {props.posts.map((post) => ( post.tags.indexOf(tag.slug) >= 0)} /> ))} - + )} ) diff --git a/public/pages/Home/components/PostFilter.scss b/public/pages/Home/components/PostFilter.scss index 4e4afb897..6895e6a53 100644 --- a/public/pages/Home/components/PostFilter.scss +++ b/public/pages/Home/components/PostFilter.scss @@ -4,3 +4,28 @@ margin: spacing(2); width: calc(100% - spacing(4)); } + +.c-post-filter-btn, +.c-post-sort-btn { + cursor: pointer; + transition: all 0.2s ease-in-out; + box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + + &:hover { + background-color: var(--colors-white); + border-color: var(--colors-gray-300); + box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + } +} + +// Override parent dropdown handle focus to apply to children +.c-dropdown__handle:focus:not(.no-focus) { + border-color: transparent !important; + box-shadow: none !important; + + .c-post-filter-btn, + .c-post-sort-btn { + border-color: var(--colors-primary-base) !important; + box-shadow: 0 0 0 3px rgba(45, 87, 237, 0.1), 0 1px 2px 0 rgb(0 0 0 / 0.05) !important; + } +} diff --git a/public/pages/Home/components/PostFilter.tsx b/public/pages/Home/components/PostFilter.tsx index 0e739ab98..96d818a35 100644 --- a/public/pages/Home/components/PostFilter.tsx +++ b/public/pages/Home/components/PostFilter.tsx @@ -151,7 +151,7 @@ export const PostFilter = (props: PostFilterProps) => { setQuery("")} renderHandle={ - + {i18n._({ id: "home.filter.label", message: "Filter" })} {filterCount > 0 &&
{filterCount}
} diff --git a/public/pages/Home/components/PostsContainer.scss b/public/pages/Home/components/PostsContainer.scss index 0f3245417..f5a2e9536 100644 --- a/public/pages/Home/components/PostsContainer.scss +++ b/public/pages/Home/components/PostsContainer.scss @@ -5,7 +5,7 @@ display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; row-gap: spacing(4); - margin-bottom: spacing(5); + margin-bottom: spacing(6); } &__filter-col { @@ -35,4 +35,52 @@ grid-column: 1 / -1; } } + + &__list { + display: flex; + flex-direction: column; + gap: spacing(4); + } + + &__post { + background-color: var(--colors-white); + border-radius: get("border.radius.xlarge"); + padding: spacing(6) spacing(7); + box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1); + transition: box-shadow 0.2s ease-in-out; + + &:hover { + box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); + } + } + + &__post-title { + font-size: get("font.size.lg"); + font-weight: get("font.weight.bold"); + color: var(--colors-gray-900); + line-height: 1.4; + } + + &__post-comments { + color: var(--colors-gray-500); + font-size: get("font.size.sm"); + align-items: center; + margin-left: spacing(4); + } + + &__postdescription { + color: var(--colors-gray-800); + font-size: get("font.size.base"); + line-height: 1.6; + } + + &__post-minimal { + // Minimal styling for compact post lists (e.g., similar posts) + padding: spacing(2) 0; + border-bottom: 1px solid var(--colors-gray-200); + + &:last-child { + border-bottom: none; + } + } } diff --git a/public/pages/Home/components/PostsContainer.tsx b/public/pages/Home/components/PostsContainer.tsx index 7f0c0b0ab..8329e8b98 100644 --- a/public/pages/Home/components/PostsContainer.tsx +++ b/public/pages/Home/components/PostsContainer.tsx @@ -144,7 +144,7 @@ export class PostsContainer extends React.Component -
) } diff --git a/public/pages/Home/components/PostsSort.tsx b/public/pages/Home/components/PostsSort.tsx index fc1fcae33..c285b6e37 100644 --- a/public/pages/Home/components/PostsSort.tsx +++ b/public/pages/Home/components/PostsSort.tsx @@ -26,7 +26,7 @@ export const PostsSort: React.FC = ({ value = "trending", onChan +
{i18n._({ id: "home.postsort.label", message: "Sort by:" })} {selectedItem.label}
} From f972ff1830ee0b1d0a114d277628f27c2730c260 Mon Sep 17 00:00:00 2001 From: Matt Roberts Date: Wed, 3 Dec 2025 16:03:47 +0000 Subject: [PATCH 02/17] More ui for the post details --- public/components/ShowPostResponse.tsx | 14 +- public/components/VoteCounter.scss | 45 ++- public/components/VoteCounter.tsx | 4 +- public/components/common/Avatar.tsx | 4 +- public/pages/ShowPost/ShowPost.page.scss | 92 +++++- public/pages/ShowPost/ShowPost.page.tsx | 289 +++++++++++------- .../ShowPost/components/ActionButton.scss | 37 +++ .../ShowPost/components/ActionButton.tsx | 26 ++ .../ShowPost/components/CommentInput.tsx | 18 +- .../ShowPost/components/ShowComment.scss | 21 +- .../pages/ShowPost/components/ShowComment.tsx | 15 +- .../ShowPost/components/StayUpdatedCard.tsx | 60 ++++ 12 files changed, 455 insertions(+), 170 deletions(-) create mode 100644 public/pages/ShowPost/components/ActionButton.scss create mode 100644 public/pages/ShowPost/components/ActionButton.tsx create mode 100644 public/pages/ShowPost/components/StayUpdatedCard.tsx diff --git a/public/components/ShowPostResponse.tsx b/public/components/ShowPostResponse.tsx index 2a77afeac..0ae864df3 100644 --- a/public/components/ShowPostResponse.tsx +++ b/public/components/ShowPostResponse.tsx @@ -11,7 +11,7 @@ import { HStack, VStack } from "./layout" import { timeSince } from "@fider/services" import { Trans } from "@lingui/react/macro" -type Size = "micro" | "small" | "normal" +type Size = "micro" | "small" | "xsmall" | "normal" interface PostResponseProps { status: string @@ -28,7 +28,6 @@ export const ResponseDetails = (props: PostResponseProps): JSX.Element | null => return ( -
{timeSince("en", new Date(), props.response.respondedAt, "date")}
{props.response?.text && status !== PostStatus.Duplicate && (
@@ -94,6 +93,17 @@ export const ResponseLozenge = (props: PostResponseProps): JSX.Element | null => return {translatedStatus} } + if (props.size === "xsmall") { + return ( +
+ + + {translatedStatus} + +
+ ) + } + return (
diff --git a/public/components/VoteCounter.scss b/public/components/VoteCounter.scss index 37a8be575..2d531215c 100644 --- a/public/components/VoteCounter.scss +++ b/public/components/VoteCounter.scss @@ -8,20 +8,20 @@ text-align: center; margin: 0 auto; padding: spacing(3) 0; - color: var(--colors-gray-500); + color: var(--colors-primary-base); display: flex; flex-direction: column; align-items: center; justify-content: center; gap: spacing(2); - border-radius: 12px; - transition: all 0.15s ease; - background-color: var(--colors-gray-50); - border: 1px solid var(--colors-gray-200); + border-radius: 16px; + transition: all 0.2s ease; + background-color: var(--colors-blue-50); + border: 2px solid var(--colors-blue-100); &:hover { - background-color: var(--colors-white); - border-color: var(--colors-gray-300); + background-color: var(--colors-blue-100); + border-color: var(--colors-primary-base); color: var(--colors-primary-base); .c-vote-counter__icon { @@ -34,9 +34,9 @@ padding: spacing(3) 0; gap: spacing(2); font-weight: get("font.weight.bold"); - background-color: var(--colors-blue-50); + background-color: var(--colors-blue-100); color: var(--colors-primary-base); - border-color: var(--colors-primary-light); + border-color: var(--colors-primary-base); .c-vote-counter__icon { color: var(--colors-primary-base); @@ -47,7 +47,8 @@ } &:hover { - background-color: var(--colors-blue-100); + background-color: var(--colors-blue-200); + border-color: var(--colors-primary-base); } } @@ -56,13 +57,33 @@ gap: spacing(2); @include disabled(); } + + &--large { + width: sizing(18); + height: sizing(20); + padding: spacing(5) 0; + gap: spacing(2); + border-radius: 20px; + + .c-vote-counter__icon { + width: 28px; + height: 28px; + stroke-width: 2.5; + } + + .c-vote-counter__count { + font-size: 24px; + font-weight: get("font.weight.bold"); + line-height: 1; + } + } } &__icon { width: 18px; height: 18px; - color: var(--colors-gray-500); - transition: all 0.15s ease; + color: var(--colors-primary-base); + transition: all 0.2s ease; flex-shrink: 0; } diff --git a/public/components/VoteCounter.tsx b/public/components/VoteCounter.tsx index 28f83bd1b..7f346675f 100644 --- a/public/components/VoteCounter.tsx +++ b/public/components/VoteCounter.tsx @@ -9,10 +9,12 @@ import ChevronUp from "@fider/assets/images/chevron-up.svg" export interface VoteCounterProps { post: Post + size?: "default" | "large" } export const VoteCounter = (props: VoteCounterProps) => { const fider = useFider() + const { size = "default" } = props const [hasVoted, setHasVoted] = useState(props.post.hasVoted) const [votesCount, setVotesCount] = useState(props.post.votesCount) const [isSignInModalOpen, setIsSignInModalOpen] = useState(false) @@ -38,10 +40,10 @@ export const VoteCounter = (props: VoteCounterProps) => { const isDisabled = status.closed || fider.isReadOnly const className = classSet({ - "border-gray-200 border rounded-md bg-gray-100": true, "c-vote-counter__button": true, "c-vote-counter__button--voted": !status.closed && hasVoted, "c-vote-counter__button--disabled": isDisabled, + "c-vote-counter__button--large": size === "large", }) const vote = ( diff --git a/public/components/common/Avatar.tsx b/public/components/common/Avatar.tsx index 7475b579c..bc98d93bd 100644 --- a/public/components/common/Avatar.tsx +++ b/public/components/common/Avatar.tsx @@ -9,10 +9,10 @@ interface AvatarProps { avatarURL: string name: string } - size?: "small" | "normal" + size?: "small" | "normal" | "large" } export const Avatar = (props: AvatarProps) => { - const size = props.size === "small" ? "h-6 w-6" : "h-8 w-8" + const size = props.size === "small" ? "h-6 w-6" : props.size === "large" ? "h-11 w-11" : "h-8 w-8" return {props.user.name} } diff --git a/public/pages/ShowPost/ShowPost.page.scss b/public/pages/ShowPost/ShowPost.page.scss index 969a00273..77c6a9b62 100644 --- a/public/pages/ShowPost/ShowPost.page.scss +++ b/public/pages/ShowPost/ShowPost.page.scss @@ -1,40 +1,108 @@ @use "~@fider/assets/styles/variables.scss" as *; #p-show-post { + background-color: var(--colors-gray-50); + padding-top: spacing(12); + padding-bottom: spacing(12); + .post-header { flex-grow: 1; } .p-show-post { &__main-col { - // background-color: var(--colors-white); - padding: spacing(4); - border-radius: get("border.radius.large"); - margin-bottom: spacing(4); + display: flex; + flex-direction: column; + gap: spacing(6); + } + + &__post-card { + background-color: var(--colors-white); + border-radius: get("border.radius.xlarge"); + padding: spacing(7); + box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1); + transition: box-shadow 0.2s ease-in-out; + } + + &__vote-counter-large { + flex-shrink: 0; + } + + &__title { + font-size: 32px; + font-weight: get("font.weight.xbold"); + line-height: 1.2; + color: var(--colors-gray-900); + + @include media("lg") { + font-size: 36px; + } + } + + &__meta { + display: flex; + align-items: center; + } + + &__description-section { + margin-top: spacing(7); + } + + &__description { + color: var(--colors-gray-800); + line-height: 1.6; + } + + &__actions { + border-top: 1px solid var(--colors-gray-200); + padding-top: spacing(4); + margin-top: spacing(6); + } + + &__discussion-section { + display: flex; + flex-direction: column; + gap: spacing(6); + } + + &__discussion-header { + margin-bottom: spacing(2); } &__action-col { - > :first-child { - // background-color: var(--colors-white); - padding: spacing(4); - // border-radius: get("border.radius.large"); + display: flex; + flex-direction: column; + gap: spacing(4); + + > * { + background-color: var(--colors-white); + border-radius: get("border.radius.xlarge"); + padding: spacing(6); + box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1); } } } + .c-comment-input-card { + flex-grow: 1; + background-color: var(--colors-white); + border-radius: get("border.radius.xlarge"); + padding: spacing(6); + box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1); + } + @include media("lg") { .p-show-post { display: grid; gap: spacing(6); - grid-template-columns: 2fr 6fr 1fr; + grid-template-columns: 1fr 320px; grid-template-rows: auto; grid-template-areas: - "Action Main" - "Action Main"; + "Main Action" + "Main Action"; &__main-col { grid-area: Main; - overflow: auto; } &__action-col { diff --git a/public/pages/ShowPost/ShowPost.page.tsx b/public/pages/ShowPost/ShowPost.page.tsx index 90cec7f30..785556af2 100644 --- a/public/pages/ShowPost/ShowPost.page.tsx +++ b/public/pages/ShowPost/ShowPost.page.tsx @@ -4,27 +4,42 @@ import React, { useState, useEffect, useCallback } from "react" import { Comment, Post, Tag, Vote, CurrentUser, PostStatus } from "@fider/models" import { actions, cache, clearUrlHash, Failure, Fider, notify, timeAgo } from "@fider/services" -import IconDotsHorizontal from "@fider/assets/images/heroicons-dots-horizontal.svg" import IconDuplicate from "@fider/assets/images/heroicons-duplicate.svg" import { i18n } from "@lingui/core" import IconRSS from "@fider/assets/images/heroicons-rss.svg" import IconPencil from "@fider/assets/images/heroicons-pencil-alt.svg" import IconChat from "@fider/assets/images/heroicons-chat-alt-2.svg" -import { ResponseDetails, Button, UserName, Moment, Markdown, Input, Form, Icon, Header, PoweredByFider, Avatar, Dropdown, RSSModal } from "@fider/components" -import { DiscussionPanel } from "./components/DiscussionPanel" +import { + ResponseDetails, + Button, + UserName, + Moment, + Markdown, + Input, + Form, + Icon, + Header, + PoweredByFider, + Avatar, + RSSModal, + VoteCounter, + ResponseLozenge, +} from "@fider/components" +import { CommentInput } from "./components/CommentInput" +import { ShowComment } from "./components/ShowComment" import CommentEditor from "@fider/components/common/form/CommentEditor" import IconX from "@fider/assets/images/heroicons-x.svg" import IconThumbsUp from "@fider/assets/images/heroicons-thumbsup.svg" import { HStack, VStack } from "@fider/components/layout" import { Trans } from "@lingui/react/macro" -import { FollowButton } from "./components/FollowButton" -import { VoteSection } from "./components/VoteSection" +import { StayUpdatedCard } from "./components/StayUpdatedCard" import { DeletePostModal } from "./components/DeletePostModal" import { ResponseModal } from "./components/ResponseModal" import { VotesPanel } from "./components/VotesPanel" import { TagsPanel } from "@fider/pages/ShowPost/components/TagsPanel" +import { ActionButton } from "./components/ActionButton" import { t } from "@lingui/macro" import { useFider } from "@fider/hooks" import { useAttachments } from "@fider/hooks/useAttachments" @@ -161,143 +176,183 @@ export default function ShowPostPage(props: ShowPostPageProps) {
-
- - - - {!editMode && ( - - - - - - - - )} - - - - {!editMode && ( - - }> - - Copy link - - {Fider.session.tenant.isFeedEnabled && ( - - Comment Feed - - )} - {Fider.session.isAuthenticated && canEditPost(Fider.session.user, props.post) && ( - <> - - Edit - - {Fider.session.user.isCollaborator && ( - - Respond - - )} - - )} - {canDeletePost() && ( - - Delete - - )} - - - )} - + {/* Post Card */} +
+ {/* Top section: Vote counter + Title/Meta */} + + {/* Large Vote Counter */} + {!editMode && ( +
+ +
+ )} -
+ {/* Title and Meta */} + + {/* Title */} {editMode ? (
) : ( - <> -

{props.post.title}

- +

{props.post.title}

)} -
- setShowDeleteModal(false)} showModal={showDeleteModal} post={props.post} /> - {Fider.session.isAuthenticated && Fider.session.user.isCollaborator && ( - setShowResponseModal(false)} showModal={showResponseModal} post={props.post} /> - )} - - {editMode ? ( -
- - - ) : ( + {/* Posted by info with status */} + {!editMode && ( <> - {props.post.description && } - {!props.post.description && ( - - No description provided. - - )} +
+ + + + Posted by + + + + + + +
+ )}
-
- -
+
- - {!editMode ? ( - - - - - ) : ( - - - - + {/* Description - Full width */} + {!editMode ? ( +
+ {props.post.description && } + {!props.post.description && ( + + No description provided. + )} -
- +
+ ) : ( +
+
+ + +
+ )} - - + {/* Tags inline */} +
+ +
+ + {/* Edit Mode Actions */} + {editMode && ( + + + + + )} + + {/* Bottom Action Bar */} + {!editMode && ( +
+ + + Copy link + + + {Fider.session.isAuthenticated && canEditPost(Fider.session.user, props.post) && ( + + Edit + + )} + + {Fider.session.isAuthenticated && Fider.session.user.isCollaborator && ( + + Respond + + )} + + {Fider.session.tenant.isFeedEnabled && ( + + Comment Feed + + )} + + {canDeletePost() && ( + + Delete + + )} + +
+ +
+ )}
-
- + {/* Discussion Section */} +
+ {/* Discussion Header */} +
+

+ Discussion {props.comments.length} +

+
+ + {/* Comment Input at top */} + + + {/* Comments List */} + {props.comments.length > 0 && ( + + {props.comments.map((c) => ( + + ))} + + )}
+ + {/* Right Sidebar */}
+ {/* Stay Updated Card */} + {!editMode && } + + {/* Voters Panel */} +
+ + {/* Modals */} + + setShowDeleteModal(false)} showModal={showDeleteModal} post={props.post} /> + {Fider.session.isAuthenticated && Fider.session.user.isCollaborator && ( + setShowResponseModal(false)} showModal={showResponseModal} post={props.post} /> + )} ) } diff --git a/public/pages/ShowPost/components/ActionButton.scss b/public/pages/ShowPost/components/ActionButton.scss new file mode 100644 index 000000000..6f26fa9c2 --- /dev/null +++ b/public/pages/ShowPost/components/ActionButton.scss @@ -0,0 +1,37 @@ +@use "~@fider/assets/styles/variables.scss" as *; + +.c-action-button { + display: inline-flex; + align-items: center; + padding: spacing(2) spacing(3); + border: none; + background-color: transparent; + border-radius: get("border.radius.medium"); + color: var(--colors-gray-700); + font-size: get("font.size.sm"); + font-weight: get("font.weight.medium"); + cursor: pointer; + transition: background-color 0.2s ease-in-out; + + &:hover { + background-color: var(--colors-gray-100); + } + + &--danger { + color: var(--colors-red-700); + + &:hover { + background-color: var(--colors-red-50); + } + } + + &__icon { + width: 18px; + height: 18px; + flex-shrink: 0; + } + + &__text { + white-space: nowrap; + } +} diff --git a/public/pages/ShowPost/components/ActionButton.tsx b/public/pages/ShowPost/components/ActionButton.tsx new file mode 100644 index 000000000..1475b9123 --- /dev/null +++ b/public/pages/ShowPost/components/ActionButton.tsx @@ -0,0 +1,26 @@ +import "./ActionButton.scss" + +import React from "react" +import { Icon } from "@fider/components" +import { HStack } from "@fider/components/layout" + +interface ActionButtonProps { + icon: SpriteSymbol | string + onClick: () => void + children: React.ReactNode + variant?: "normal" | "danger" +} + +export const ActionButton = (props: ActionButtonProps) => { + const { icon, onClick, children, variant = "normal" } = props + const className = `c-action-button ${variant === "danger" ? "c-action-button--danger" : ""}` + + return ( + + ) +} diff --git a/public/pages/ShowPost/components/CommentInput.tsx b/public/pages/ShowPost/components/CommentInput.tsx index 3549bc945..7d8ae1cb2 100644 --- a/public/pages/ShowPost/components/CommentInput.tsx +++ b/public/pages/ShowPost/components/CommentInput.tsx @@ -1,7 +1,7 @@ import React, { useCallback, useState, useEffect } from "react" import { Post } from "@fider/models" -import { Avatar, UserName, Button, Form } from "@fider/components" +import { Avatar, Button, Form } from "@fider/components" import { SignInModal } from "@fider/components" import { cache, actions, Failure, Fider } from "@fider/services" @@ -76,16 +76,10 @@ export const CommentInput = (props: CommentInputProps) => { return ( <> - - {Fider.session.isAuthenticated && } -
+ + {Fider.session.isAuthenticated && } +
- {Fider.session.isAuthenticated && ( -
- -
- )} - {isClient ? ( <> { {hasContent && ( <> - )} diff --git a/public/pages/ShowPost/components/ShowComment.scss b/public/pages/ShowPost/components/ShowComment.scss index 4cbbfb543..0f199c493 100644 --- a/public/pages/ShowPost/components/ShowComment.scss +++ b/public/pages/ShowPost/components/ShowComment.scss @@ -1,6 +1,21 @@ @use "~@fider/assets/styles/variables.scss" as *; -.comment-area { - // spacing(8) for the avatar + spacing(1) for padding right - width: calc(100% - spacing(9)); +.c-comment { + &__card { + flex-grow: 1; + background-color: var(--colors-white); + border-radius: get("border.radius.xlarge"); + padding: spacing(6); + box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1); + } + + &__content { + flex-grow: 1; + + &--highlighted { + padding: spacing(2); + background-color: var(--colors-yellow-50); + border-radius: get("border.radius.medium"); + } + } } diff --git a/public/pages/ShowPost/components/ShowComment.tsx b/public/pages/ShowPost/components/ShowComment.tsx index fb090f084..60b6af103 100644 --- a/public/pages/ShowPost/components/ShowComment.tsx +++ b/public/pages/ShowPost/components/ShowComment.tsx @@ -158,19 +158,16 @@ export const ShowComment = (props: ShowCommentProps) => { ) const classList = classSet({ - "flex-grow rounded-md p-2 comment-area": true, - "bg-gray-100": !props.highlighted, - "bg-gray-200": props.highlighted, + "c-comment__content": true, + "c-comment__content--highlighted": props.highlighted, }) return (
- - {modal()} -
- -
-
+ {modal()} + + +
diff --git a/public/pages/ShowPost/components/StayUpdatedCard.tsx b/public/pages/ShowPost/components/StayUpdatedCard.tsx new file mode 100644 index 000000000..7614a2bfe --- /dev/null +++ b/public/pages/ShowPost/components/StayUpdatedCard.tsx @@ -0,0 +1,60 @@ +import React, { useState } from "react" +import { Button, Icon } from "@fider/components" +import { actions } from "@fider/services" +import { useFider } from "@fider/hooks" +import IconPlus from "@fider/assets/images/heroicons-plus.svg" +import IconCheck from "@fider/assets/images/heroicons-check.svg" +import { VStack } from "@fider/components/layout" +import { Trans } from "@lingui/macro" +import { Post } from "@fider/models" + +export interface StayUpdatedCardProps { + post: Post + subscribed: boolean +} + +export const StayUpdatedCard = (props: StayUpdatedCardProps) => { + const fider = useFider() + const [subscribed, setSubscribed] = useState(props.subscribed) + + const subscribeOrUnsubscribe = async () => { + const action = subscribed ? actions.unsubscribe : actions.subscribe + + const response = await action(props.post.number) + if (response.ok) { + setSubscribed(!subscribed) + } + } + + if (!fider.session.isAuthenticated) { + return null + } + + const button = subscribed ? ( + + ) : ( + + ) + + return ( + +

+ Stay Updated +

+

+ Get notified when this post receives updates +

+ {button} +
+ ) +} From 62f05c3fd5e8097cf37427f71e6acda778b04e64 Mon Sep 17 00:00:00 2001 From: Matt Roberts Date: Thu, 4 Dec 2025 08:45:37 +0000 Subject: [PATCH 03/17] Mroe post details changes --- public/components/ShowPostResponse.tsx | 46 +++++++++++++++--------- public/pages/ShowPost/ShowPost.page.scss | 19 ++++++++++ 2 files changed, 48 insertions(+), 17 deletions(-) diff --git a/public/components/ShowPostResponse.tsx b/public/components/ShowPostResponse.tsx index 0ae864df3..8f0dd6477 100644 --- a/public/components/ShowPostResponse.tsx +++ b/public/components/ShowPostResponse.tsx @@ -1,15 +1,15 @@ import React from "react" import { PostResponse, PostStatus } from "@fider/models" -import { Icon, Markdown } from "@fider/components" +import { Icon, Markdown, UserName, Moment } from "@fider/components" import HeroIconDuplicate from "@fider/assets/images/heroicons-duplicate.svg" import HeroIconCheck from "@fider/assets/images/heroicons-check-circle.svg" import HeroIconSparkles from "@fider/assets/images/heroicons-sparkles-outline.svg" import HeroIconLightBulb from "@fider/assets/images/heroicons-lightbulb.svg" import HeroIconThumbsUp from "@fider/assets/images/heroicons-thumbsup.svg" import HeroIconThumbsDown from "@fider/assets/images/heroicons-thumbsdown.svg" -import { HStack, VStack } from "./layout" -import { timeSince } from "@fider/services" +import { HStack } from "./layout" import { Trans } from "@lingui/react/macro" +import { useFider } from "@fider/hooks" type Size = "micro" | "small" | "xsmall" | "normal" @@ -20,29 +20,41 @@ interface PostResponseProps { } export const ResponseDetails = (props: PostResponseProps): JSX.Element | null => { + const fider = useFider() const status = PostStatus.Get(props.status) if (!props.response) { return null } + const { bg, border } = getLozengeProps(status) + return ( - -
{timeSince("en", new Date(), props.response.respondedAt, "date")}
- {props.response?.text && status !== PostStatus.Duplicate && ( -
- + +
+
+ + + + +
- )} - {status === PostStatus.Duplicate && props.response.original && ( - - )} - + {props.response?.text && status !== PostStatus.Duplicate && ( +
+ +
+ )} + + {status === PostStatus.Duplicate && props.response.original && ( + + )} +
+
) } diff --git a/public/pages/ShowPost/ShowPost.page.scss b/public/pages/ShowPost/ShowPost.page.scss index 77c6a9b62..174e156cb 100644 --- a/public/pages/ShowPost/ShowPost.page.scss +++ b/public/pages/ShowPost/ShowPost.page.scss @@ -91,6 +91,25 @@ box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1); } + .c-response-details { + &__card { + flex-grow: 1; + border-radius: get("border.radius.xlarge"); + padding: spacing(4); + box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1); + border-width: 2px; + } + + &__header { + margin-bottom: spacing(3); + } + + &__content { + color: var(--colors-gray-800); + line-height: 1.6; + } + } + @include media("lg") { .p-show-post { display: grid; From 06e805fd5197bc9e0370966facba060d8e57e890 Mon Sep 17 00:00:00 2001 From: Matt Roberts Date: Sun, 7 Dec 2025 14:59:21 +0000 Subject: [PATCH 04/17] More UI for the show post page. --- app/services/sqlstore/postgres/comment.go | 2 +- public/assets/images/heroicons-person.svg | 3 + public/components/Reactions.tsx | 2 +- public/components/ShowPostResponse.tsx | 4 +- public/components/common/UserName.scss | 2 +- public/pages/ShowPost/ShowPost.page.scss | 1 - public/pages/ShowPost/ShowPost.page.tsx | 17 +++-- .../pages/ShowPost/components/ShowComment.tsx | 4 +- .../ShowPost/components/StayUpdatedCard.tsx | 4 +- .../pages/ShowPost/components/VotesModal.scss | 21 ++++++ .../pages/ShowPost/components/VotesModal.tsx | 25 ++++--- .../pages/ShowPost/components/VotesPanel.scss | 66 +++++++++++++++++++ .../pages/ShowPost/components/VotesPanel.tsx | 58 +++++++++------- 13 files changed, 162 insertions(+), 47 deletions(-) create mode 100644 public/assets/images/heroicons-person.svg create mode 100644 public/pages/ShowPost/components/VotesModal.scss create mode 100644 public/pages/ShowPost/components/VotesPanel.scss diff --git a/app/services/sqlstore/postgres/comment.go b/app/services/sqlstore/postgres/comment.go index eaaa17d73..ba195065f 100644 --- a/app/services/sqlstore/postgres/comment.go +++ b/app/services/sqlstore/postgres/comment.go @@ -245,7 +245,7 @@ func getCommentsByPost(ctx context.Context, q *query.GetCommentsByPost) error { WHERE p.id = $1 AND p.tenant_id = $2 AND c.deleted_at IS NULL - ORDER BY c.created_at ASC`, q.Post.ID, tenant.ID, userId) + ORDER BY c.created_at DESC`, q.Post.ID, tenant.ID, userId) if err != nil { return errors.Wrap(err, "failed get comments of post with id '%d'", q.Post.ID) } diff --git a/public/assets/images/heroicons-person.svg b/public/assets/images/heroicons-person.svg new file mode 100644 index 000000000..883c7f4dd --- /dev/null +++ b/public/assets/images/heroicons-person.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/components/Reactions.tsx b/public/components/Reactions.tsx index 68b8c26ad..c50a285f1 100644 --- a/public/components/Reactions.tsx +++ b/public/components/Reactions.tsx @@ -75,7 +75,7 @@ export const Reactions: React.FC = ({ emojiSelectorRef, toggleRe "clickable hover:bg-gray-200": fider.session.isAuthenticated && !reaction.includesMe, })} > - {reaction.emoji} {reaction.count} + {reaction.emoji} {reaction.count} ))} diff --git a/public/components/ShowPostResponse.tsx b/public/components/ShowPostResponse.tsx index 8f0dd6477..feafdd1bf 100644 --- a/public/components/ShowPostResponse.tsx +++ b/public/components/ShowPostResponse.tsx @@ -27,11 +27,11 @@ export const ResponseDetails = (props: PostResponseProps): JSX.Element | null => return null } - const { bg, border } = getLozengeProps(status) + const { bg } = getLozengeProps(status) return ( -
+
diff --git a/public/components/common/UserName.scss b/public/components/common/UserName.scss index c76381f9f..bd37021b5 100644 --- a/public/components/common/UserName.scss +++ b/public/components/common/UserName.scss @@ -20,7 +20,7 @@ div { svg { height: 14px; - vertical-align: text-bottom; + vertical-align: middle; margin-inline-start: 2px; align-items: flex-end; } diff --git a/public/pages/ShowPost/ShowPost.page.scss b/public/pages/ShowPost/ShowPost.page.scss index 174e156cb..9bdb219e6 100644 --- a/public/pages/ShowPost/ShowPost.page.scss +++ b/public/pages/ShowPost/ShowPost.page.scss @@ -96,7 +96,6 @@ flex-grow: 1; border-radius: get("border.radius.xlarge"); padding: spacing(4); - box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1); border-width: 2px; } diff --git a/public/pages/ShowPost/ShowPost.page.tsx b/public/pages/ShowPost/ShowPost.page.tsx index 785556af2..46afc53bb 100644 --- a/public/pages/ShowPost/ShowPost.page.tsx +++ b/public/pages/ShowPost/ShowPost.page.tsx @@ -257,7 +257,7 @@ export default function ShowPostPage(props: ShowPostPageProps) { {/* Edit Mode Actions */} {editMode && ( - + - )} - {props.votes.length > 0 && extraVotesCount === 0 && canShowAll && ( - +
+ {visibleVotes.map((vote, i) => ( + + ))} + {remainingCount > 0 ? ( + + ) : ( + + )} +
)} + {props.votes.length === 0 && ( None From d3f4b344a3bf81eb0efeb1ead730b3d4dcefd177 Mon Sep 17 00:00:00 2001 From: Matt Roberts Date: Sun, 7 Dec 2025 15:55:56 +0000 Subject: [PATCH 05/17] Edit the welcome header --- app/actions/tenant.go | 5 +++ app/handlers/admin.go | 1 + app/handlers/post.go | 3 +- app/models/cmd/tenant.go | 1 + app/models/entity/tenant.go | 1 + app/services/sqlstore/postgres/tenant.go | 11 ++++--- .../202512071400_add_welcome_header.sql | 2 ++ public/models/identity.ts | 1 + .../pages/GeneralSettings.page.tsx | 17 +++++++++- public/pages/Home/Home.page.tsx | 32 +++++++++++++++++-- public/pages/ShowPost/ShowPost.page.scss | 2 +- public/services/actions/tenant.ts | 1 + 12 files changed, 67 insertions(+), 10 deletions(-) create mode 100644 migrations/202512071400_add_welcome_header.sql diff --git a/app/actions/tenant.go b/app/actions/tenant.go index 9ecb47c03..c702f1a72 100644 --- a/app/actions/tenant.go +++ b/app/actions/tenant.go @@ -194,6 +194,7 @@ type UpdateTenantSettings struct { Title string `json:"title"` Invitation string `json:"invitation"` WelcomeMessage string `json:"welcomeMessage"` + WelcomeHeader string `json:"welcomeHeader"` Locale string `json:"locale"` CNAME string `json:"cname" format:"lower"` } @@ -242,6 +243,10 @@ func (action *UpdateTenantSettings) Validate(ctx context.Context, user *entity.U result.AddFieldFailure("invitation", "Invitation must have less than 60 characters.") } + if len(action.WelcomeHeader) > 100 { + result.AddFieldFailure("welcomeHeader", "Welcome Header must have less than 100 characters.") + } + if !i18n.IsValidLocale(action.Locale) { result.AddFieldFailure("locale", "Locale is invalid.") } diff --git a/app/handlers/admin.go b/app/handlers/admin.go index 17a4561b7..ec48fe800 100644 --- a/app/handlers/admin.go +++ b/app/handlers/admin.go @@ -56,6 +56,7 @@ func UpdateSettings() web.HandlerFunc { Title: action.Title, Invitation: action.Invitation, WelcomeMessage: action.WelcomeMessage, + WelcomeHeader: action.WelcomeHeader, CNAME: action.CNAME, Locale: action.Locale, }, diff --git a/app/handlers/post.go b/app/handlers/post.go index ced787e70..0cb663431 100644 --- a/app/handlers/post.go +++ b/app/handlers/post.go @@ -61,7 +61,8 @@ func Index() web.HandlerFunc { return c.Page(http.StatusOK, web.Props{ Page: "Home/Home.page", Description: description, - Data: data, + // Header: c.Tenant().WelcomeHeader, + Data: data, }) } } diff --git a/app/models/cmd/tenant.go b/app/models/cmd/tenant.go index 62b0006fc..a4dcbc9a9 100644 --- a/app/models/cmd/tenant.go +++ b/app/models/cmd/tenant.go @@ -30,6 +30,7 @@ type UpdateTenantSettings struct { Title string Invitation string WelcomeMessage string + WelcomeHeader string CNAME string Locale string } diff --git a/app/models/entity/tenant.go b/app/models/entity/tenant.go index 98c92e2a6..69165efc3 100644 --- a/app/models/entity/tenant.go +++ b/app/models/entity/tenant.go @@ -9,6 +9,7 @@ type Tenant struct { Subdomain string `json:"subdomain"` Invitation string `json:"invitation"` WelcomeMessage string `json:"welcomeMessage"` + WelcomeHeader string `json:"welcomeHeader"` CNAME string `json:"cname"` Status enum.TenantStatus `json:"status"` Locale string `json:"locale"` diff --git a/app/services/sqlstore/postgres/tenant.go b/app/services/sqlstore/postgres/tenant.go index 5473ad2c4..2c91c0817 100644 --- a/app/services/sqlstore/postgres/tenant.go +++ b/app/services/sqlstore/postgres/tenant.go @@ -23,6 +23,7 @@ type dbTenant struct { CNAME string `db:"cname"` Invitation string `db:"invitation"` WelcomeMessage string `db:"welcome_message"` + WelcomeHeader string `db:"welcome_header"` Status int `db:"status"` Locale string `db:"locale"` IsPrivate bool `db:"is_private"` @@ -46,6 +47,7 @@ func (t *dbTenant) toModel() *entity.Tenant { CNAME: t.CNAME, Invitation: t.Invitation, WelcomeMessage: t.WelcomeMessage, + WelcomeHeader: t.WelcomeHeader, Status: enum.TenantStatus(t.Status), Locale: t.Locale, IsPrivate: t.IsPrivate, @@ -153,8 +155,8 @@ func updateTenantSettings(ctx context.Context, c *cmd.UpdateTenantSettings) erro c.Logo.BlobKey = "" } - query := "UPDATE tenants SET name = $1, invitation = $2, welcome_message = $3, cname = $4, logo_bkey = $5, locale = $6 WHERE id = $7" - _, err := trx.Execute(query, c.Title, c.Invitation, c.WelcomeMessage, c.CNAME, c.Logo.BlobKey, c.Locale, tenant.ID) + query := "UPDATE tenants SET name = $1, invitation = $2, welcome_message = $3, welcome_header = $4, cname = $5, logo_bkey = $6, locale = $7 WHERE id = $8" + _, err := trx.Execute(query, c.Title, c.Invitation, c.WelcomeMessage, c.WelcomeHeader, c.CNAME, c.Logo.BlobKey, c.Locale, tenant.ID) if err != nil { return errors.Wrap(err, "failed update tenant settings") } @@ -163,6 +165,7 @@ func updateTenantSettings(ctx context.Context, c *cmd.UpdateTenantSettings) erro tenant.Invitation = c.Invitation tenant.CNAME = c.CNAME tenant.WelcomeMessage = c.WelcomeMessage + tenant.WelcomeHeader = c.WelcomeHeader return nil }) @@ -290,7 +293,7 @@ func getFirstTenant(ctx context.Context, q *query.GetFirstTenant) error { tenant := dbTenant{} err := trx.Get(&tenant, ` - SELECT id, name, subdomain, cname, invitation, locale, welcome_message, status, is_private, logo_bkey, custom_css, allowed_schemes, is_email_auth_allowed, is_feed_enabled, prevent_indexing + SELECT id, name, subdomain, cname, invitation, locale, welcome_message, welcome_header, status, is_private, logo_bkey, custom_css, allowed_schemes, is_email_auth_allowed, is_feed_enabled, prevent_indexing FROM tenants ORDER BY id LIMIT 1 `) @@ -309,7 +312,7 @@ func getTenantByDomain(ctx context.Context, q *query.GetTenantByDomain) error { tenant := dbTenant{} err := trx.Get(&tenant, ` - SELECT id, name, subdomain, cname, invitation, locale, welcome_message, status, is_private, logo_bkey, custom_css, allowed_schemes, is_email_auth_allowed, is_feed_enabled, prevent_indexing + SELECT id, name, subdomain, cname, invitation, locale, welcome_message, welcome_header, status, is_private, logo_bkey, custom_css, allowed_schemes, is_email_auth_allowed, is_feed_enabled, prevent_indexing FROM tenants t WHERE subdomain = $1 OR subdomain = $2 OR cname = $3 ORDER BY cname DESC diff --git a/migrations/202512071400_add_welcome_header.sql b/migrations/202512071400_add_welcome_header.sql new file mode 100644 index 000000000..23bce1266 --- /dev/null +++ b/migrations/202512071400_add_welcome_header.sql @@ -0,0 +1,2 @@ +-- Add welcome_header column to tenants table +ALTER TABLE tenants ADD COLUMN welcome_header TEXT NULL DEFAULT ''; diff --git a/public/models/identity.ts b/public/models/identity.ts index 4e724f9e2..33728d220 100644 --- a/public/models/identity.ts +++ b/public/models/identity.ts @@ -6,6 +6,7 @@ export interface Tenant { locale: string invitation: string welcomeMessage: string + welcomeHeader: string status: TenantStatus isPrivate: boolean logoBlobKey: string diff --git a/public/pages/Administration/pages/GeneralSettings.page.tsx b/public/pages/Administration/pages/GeneralSettings.page.tsx index a08a2ed8c..1a44518fa 100644 --- a/public/pages/Administration/pages/GeneralSettings.page.tsx +++ b/public/pages/Administration/pages/GeneralSettings.page.tsx @@ -11,6 +11,7 @@ const GeneralSettingsPage = () => { const fider = useFider() const [title, setTitle] = useState(fider.session.tenant.name) const [welcomeMessage, setWelcomeMessage] = useState(fider.session.tenant.welcomeMessage) + const [welcomeHeader, setWelcomeHeader] = useState(fider.session.tenant.welcomeHeader) const [invitation, setInvitation] = useState(fider.session.tenant.invitation) const [logo, setLogo] = useState(undefined) const [cname, setCNAME] = useState(fider.session.tenant.cname) @@ -18,7 +19,7 @@ const GeneralSettingsPage = () => { const [error, setError] = useState(undefined) const handleSave = async (e: ButtonClickEvent) => { - const result = await actions.updateTenantSettings({ title, cname, welcomeMessage, invitation, logo, locale }) + const result = await actions.updateTenantSettings({ title, cname, welcomeMessage, welcomeHeader, invitation, logo, locale }) if (result.ok) { e.preventEnable() location.href = `/` @@ -48,6 +49,20 @@ const GeneralSettingsPage = () => {

Keep it short and snappy. Your product / service name is usually best.

+ +

+ Large header text shown on the home page. Leave empty to hide. Wrap text with underscores (e.g., _highlighted_) to show it in blue. +

+ +