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/pkg/web/react_test.go b/app/pkg/web/react_test.go index b6921cba8..4d60c7204 100644 --- a/app/pkg/web/react_test.go +++ b/app/pkg/web/react_test.go @@ -52,7 +52,8 @@ func TestReactRenderer_RenderEmptyHomeHTML(t *testing.T) { }, }) Expect(html).ContainsSubstring(`
DEV
`) - Expect(html).ContainsSubstring(``) + Expect(html).ContainsSubstring(` )} - - Sign in - + )} 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 2a77afeac..feafdd1bf 100644 --- a/public/components/ShowPostResponse.tsx +++ b/public/components/ShowPostResponse.tsx @@ -1,17 +1,17 @@ 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" | "normal" +type Size = "micro" | "small" | "xsmall" | "normal" interface PostResponseProps { status: string @@ -20,30 +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 } = 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 && ( + + )} +
+
) } @@ -94,6 +105,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 40d7926a7..b0f977b77 100644 --- a/public/components/VoteCounter.scss +++ b/public/components/VoteCounter.scss @@ -2,58 +2,129 @@ .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-primary-base); display: flex; flex-direction: column; align-items: center; - border-radius: 8px; - 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; + justify-content: center; + gap: spacing(2); + border-radius: 16px; + transition: all 0.2s ease; + background-color: var(--colors-blue-50); + border: 2px solid var(--colors-blue-100); + + @include dark-mode { + background-color: var(--colors-blue-900); + border-color: var(--colors-blue-800); + color: var(--colors-blue-300); } &:hover { - background-color: var(--colors-gray-50); + background-color: var(--colors-blue-100); + border-color: var(--colors-primary-base); color: var(--colors-primary-base); - svg { + + @include dark-mode { + background-color: var(--colors-blue-800); + border-color: var(--colors-blue-500); + color: var(--colors-blue-200); + } + + .c-vote-counter__icon { color: var(--colors-primary-base); - transform: scale(1.1); + transform: translateY(-1px); + + @include dark-mode { + color: var(--colors-blue-200); + } } } &--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); + background-color: var(--colors-blue-100); color: var(--colors-primary-base); - border-color: var(--colors-primary-light); + border-color: var(--colors-primary-base); + + @include dark-mode { + background-color: var(--colors-blue-800); + color: var(--colors-blue-200); + border-color: var(--colors-blue-500); + } - svg { + .c-vote-counter__icon { color: var(--colors-primary-base); + + @include dark-mode { + color: var(--colors-blue-200); + } + } + + .c-vote-counter__count { + font-weight: get("font.weight.bold"); } &:hover { - background-color: var(--colors-blue-100); + background-color: var(--colors-blue-200); + border-color: var(--colors-primary-base); + + @include dark-mode { + background-color: var(--colors-blue-700); + border-color: var(--colors-blue-400); + } } } &--disabled { - justify-content: center; - padding: 0; + padding: spacing(3) 0; + 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-primary-base); + transition: all 0.2s ease; + flex-shrink: 0; + + @include dark-mode { + color: var(--colors-blue-300); + } + } + + &__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..7f346675f 100644 --- a/public/components/VoteCounter.tsx +++ b/public/components/VoteCounter.tsx @@ -5,14 +5,16 @@ 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 + 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,23 +40,23 @@ 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 = ( ) const disabled = ( ) diff --git a/public/components/common/Avatar.scss b/public/components/common/Avatar.scss index a725df538..c00ef9682 100644 --- a/public/components/common/Avatar.scss +++ b/public/components/common/Avatar.scss @@ -5,5 +5,8 @@ vertical-align: middle; display: inline-block; border: 2px solid var(--colors-white); + @include dark-mode { + border: 2px solid var(--colors-gray-200); + } box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1); } 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/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/components/common/form/CommentEditor.scss b/public/components/common/form/CommentEditor.scss index ae33d3b62..3cb547d71 100644 --- a/public/components/common/form/CommentEditor.scss +++ b/public/components/common/form/CommentEditor.scss @@ -75,6 +75,10 @@ overflow: hidden; background-color: var(--colors-white); + @include dark-mode { + background-color: var(--colors-gray-100); + } + &.m-error { border-color: var(--colors-red-600); } 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. +

+ +