From 91749c3eb48b79e74498983f7b5bcfce76218a85 Mon Sep 17 00:00:00 2001 From: hobbescodes <87732294+hobbescodes@users.noreply.github.com> Date: Mon, 28 Apr 2025 19:52:36 -0500 Subject: [PATCH 001/103] refactor(recent-feedback): stabilize relative timestamps --- src/components/dashboard/Response/Response.tsx | 9 ++++++++- src/components/layout/Layout/Layout.tsx | 2 ++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/components/dashboard/Response/Response.tsx b/src/components/dashboard/Response/Response.tsx index 7b8f1b31..b9c8cc2d 100644 --- a/src/components/dashboard/Response/Response.tsx +++ b/src/components/dashboard/Response/Response.tsx @@ -17,7 +17,14 @@ interface Props extends FlexProps { * Recent feedback response. */ const Response = ({ feedback, ...rest }: Props) => { - const date = dayjs(feedback?.createdAt).fromNow(); + const startOfDay = dayjs(feedback?.createdAt).utc().startOf("day"); + + const isToday = dayjs.duration(dayjs().utc().diff(startOfDay)).asDays() < 1; + + // NB: `isToday` is used to stabilize the relative time in order to keep in sync with `FeedbackOverview` calculations. + const date = isToday + ? dayjs(feedback?.createdAt).utc().fromNow() + : startOfDay.fromNow(); return ( diff --git a/src/components/layout/Layout/Layout.tsx b/src/components/layout/Layout/Layout.tsx index 91de8520..2e104da7 100644 --- a/src/components/layout/Layout/Layout.tsx +++ b/src/components/layout/Layout/Layout.tsx @@ -2,6 +2,7 @@ import { Center, Flex, Toaster, css, sigil } from "@omnidev/sigil"; import dayjs from "dayjs"; +import duration from "dayjs/plugin/duration"; import relativeTime from "dayjs/plugin/relativeTime"; import utc from "dayjs/plugin/utc"; import { useIsClient } from "usehooks-ts"; @@ -12,6 +13,7 @@ import { toaster } from "lib/util"; import type { PropsWithChildren } from "react"; +dayjs.extend(duration); dayjs.extend(relativeTime); dayjs.extend(utc); From 474663d83b7cbbba4fc98c4922854279ff30b5b5 Mon Sep 17 00:00:00 2001 From: hobbescodes <87732294+hobbescodes@users.noreply.github.com> Date: Mon, 28 Apr 2025 21:54:38 -0500 Subject: [PATCH 002/103] refactor(page): make description optional, update copy --- .../(manage)/settings/page.tsx | 1 - .../organizations/[organizationSlug]/page.tsx | 1 - .../projects/[projectSlug]/settings/page.tsx | 2 -- .../[organizationSlug]/projects/page.tsx | 1 - src/app/organizations/page.tsx | 1 - src/components/layout/Page/Page.tsx | 20 ++++++++++--------- src/lib/config/app.config.ts | 13 +++--------- 7 files changed, 14 insertions(+), 25 deletions(-) diff --git a/src/app/organizations/[organizationSlug]/(manage)/settings/page.tsx b/src/app/organizations/[organizationSlug]/(manage)/settings/page.tsx index 6c9dee9b..ecf35f7c 100644 --- a/src/app/organizations/[organizationSlug]/(manage)/settings/page.tsx +++ b/src/app/organizations/[organizationSlug]/(manage)/settings/page.tsx @@ -84,7 +84,6 @@ const OrganizationSettingsPage = async ({ params }: Props) => { diff --git a/src/app/organizations/[organizationSlug]/page.tsx b/src/app/organizations/[organizationSlug]/page.tsx index 6b9ed95b..e84a25ac 100644 --- a/src/app/organizations/[organizationSlug]/page.tsx +++ b/src/app/organizations/[organizationSlug]/page.tsx @@ -113,7 +113,6 @@ const OrganizationPage = async ({ params }: Props) => { breadcrumbs={breadcrumbs} header={{ title: organization.name!, - description: app.organizationPage.header.description, cta: [ { label: app.organizationPage.header.cta.viewAllProjects.label, diff --git a/src/app/organizations/[organizationSlug]/projects/[projectSlug]/settings/page.tsx b/src/app/organizations/[organizationSlug]/projects/[projectSlug]/settings/page.tsx index f73e46b4..4d42fb2b 100644 --- a/src/app/organizations/[organizationSlug]/projects/[projectSlug]/settings/page.tsx +++ b/src/app/organizations/[organizationSlug]/projects/[projectSlug]/settings/page.tsx @@ -116,8 +116,6 @@ const ProjectSettingsPage = async ({ params }: Props) => { breadcrumbs={breadcrumbs} header={{ title: `${project.name!} Settings`, - description: - "Handle project settings and manage feedback for your project.", }} > { breadcrumbs={breadcrumbs} header={{ title: app.projectsPage.header.title, - description: app.projectsPage.header.description, cta: [ { label: app.projectsPage.header.cta.newProject.label, diff --git a/src/app/organizations/page.tsx b/src/app/organizations/page.tsx index 81238f6e..13ce0cc4 100644 --- a/src/app/organizations/page.tsx +++ b/src/app/organizations/page.tsx @@ -92,7 +92,6 @@ const OrganizationsPage = async ({ searchParams }: Props) => { breadcrumbs={breadcrumbs} header={{ title: app.organizationsPage.header.title, - description: app.organizationsPage.header.description, cta: [ { label: app.organizationsPage.header.cta.newOrganization.label, diff --git a/src/components/layout/Page/Page.tsx b/src/components/layout/Page/Page.tsx index 95a0575f..1c80ae79 100644 --- a/src/components/layout/Page/Page.tsx +++ b/src/components/layout/Page/Page.tsx @@ -15,7 +15,7 @@ interface Props extends StackProps { /** Header section title. */ title: string; /** Header section description. */ - description: string; + description?: string; /** Header section call to action buttons. */ cta?: ActionButton[]; /** Props to pass to the header section. */ @@ -53,14 +53,16 @@ const Page = ({ breadcrumbs, header, children, ...rest }: Props) => ( {header.title} - - {header.description} - + {header.description && ( + + {header.description} + + )} Date: Mon, 28 Apr 2025 23:37:52 -0500 Subject: [PATCH 003/103] chore(copy): update org page copy --- src/lib/config/app.config.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/lib/config/app.config.ts b/src/lib/config/app.config.ts index 64ed7bbc..7d6c6640 100644 --- a/src/lib/config/app.config.ts +++ b/src/lib/config/app.config.ts @@ -377,7 +377,7 @@ const app = { }, projects: { title: "Projects", - description: "Manage feedback collection across your applications", + description: "Manage projects across this organization", emptyState: { message: "No projects found. Would you like to create one?", cta: { @@ -387,7 +387,8 @@ const app = { }, metrics: { title: "Organization Metrics", - description: "Overview of all projects and feedback", + description: + "Overview of all projects and feedback within this organization", data: { totalProjects: { title: "Total Projects", From a0efad7aa0d9590b3687a36c818ac3843fb7a442 Mon Sep 17 00:00:00 2001 From: Brian Cooper Date: Mon, 28 Apr 2025 23:39:51 -0500 Subject: [PATCH 004/103] chore(profile): remove close account CTA --- src/components/profile/Account/Account.tsx | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/components/profile/Account/Account.tsx b/src/components/profile/Account/Account.tsx index 1f8b1a1f..51f22a0e 100644 --- a/src/components/profile/Account/Account.tsx +++ b/src/components/profile/Account/Account.tsx @@ -94,24 +94,6 @@ const Account = ({ user }: Props) => { ); })} - - - - {app.profileAccountPage.cta.deleteAccount.description} - - {app.supportEmail} - - . - - ); }; From 914b9553df7265cec7d4145a412263b9a210322f Mon Sep 17 00:00:00 2001 From: Brian Cooper Date: Mon, 28 Apr 2025 23:41:32 -0500 Subject: [PATCH 005/103] chore(profile): Edit -> Manage --- src/app/profile/[userId]/account/page.tsx | 1 - src/components/profile/Account/Account.tsx | 4 +--- src/lib/config/app.config.ts | 2 +- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/app/profile/[userId]/account/page.tsx b/src/app/profile/[userId]/account/page.tsx index 1d96920c..ec8a36bd 100644 --- a/src/app/profile/[userId]/account/page.tsx +++ b/src/app/profile/[userId]/account/page.tsx @@ -41,7 +41,6 @@ const ProfileAccountPage = async ({ params }: Props) => { description: app.profileAccountPage.description, cta: [ { - // TODO: match identity to say Edit Profile. label: app.profileAccountPage.cta.updateProfile.label, // TODO: get Sigil Icon component working and update accordingly. Context: https://github.com/omnidotdev/backfeed-app/pull/44#discussion_r1897974331 icon: , diff --git a/src/components/profile/Account/Account.tsx b/src/components/profile/Account/Account.tsx index 51f22a0e..6c3728a7 100644 --- a/src/components/profile/Account/Account.tsx +++ b/src/components/profile/Account/Account.tsx @@ -1,10 +1,8 @@ "use client"; -import { Button, Flex, Input, Label, Stack, Text, sigil } from "@omnidev/sigil"; +import { Button, Flex, Input, Label, Stack } from "@omnidev/sigil"; import { useMemo, useState } from "react"; import { IoEyeOffOutline, IoEyeOutline } from "react-icons/io5"; - -import { SectionContainer } from "components/layout"; import { app } from "lib/config"; import type { InputProps } from "@omnidev/sigil"; diff --git a/src/lib/config/app.config.ts b/src/lib/config/app.config.ts index 7d6c6640..6761bb40 100644 --- a/src/lib/config/app.config.ts +++ b/src/lib/config/app.config.ts @@ -286,7 +286,7 @@ const app = { }, cta: { updateProfile: { - label: "Edit Profile", + label: "Manage Profile", }, changePassword: { label: "Change Password", From 10982925f8f24056c2fc164cb19aa469b906e26a Mon Sep 17 00:00:00 2001 From: Brian Cooper Date: Mon, 28 Apr 2025 23:52:35 -0500 Subject: [PATCH 006/103] fix: fix email addresses (WIP) --- src/app/api/invite/route.ts | 2 +- src/app/error.tsx | 6 ++++-- src/lib/config/app.config.ts | 7 ++++++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/app/api/invite/route.ts b/src/app/api/invite/route.ts index eed1ba80..515653c3 100644 --- a/src/app/api/invite/route.ts +++ b/src/app/api/invite/route.ts @@ -28,7 +28,7 @@ export const POST = async (req: NextRequest) => { (await req.json()) as OrganizationInvitation; const { data, error } = await resend.emails.send({ - from: `${app.supportName} <${isDevEnv ? "onboarding@resend.dev" : app.supportEmail}>`, + from: `${app.supportName} <${app.fromEmailAddress}>`, to: isDevEnv ? "delivered@resend.dev" : recipientEmail, subject: `${emailTemplate.subject.value1} ${organizationName} ${emailTemplate.subject.value2} ${app.name}`, react: InviteMemberEmailTemplate({ diff --git a/src/app/error.tsx b/src/app/error.tsx index 30d81569..45c6ec56 100644 --- a/src/app/error.tsx +++ b/src/app/error.tsx @@ -18,8 +18,10 @@ const GlobalErrorPage = () => { {app.globalError.description}{" "} - - {app.supportEmail} + + + {app.supportEmailAddress} + . diff --git a/src/lib/config/app.config.ts b/src/lib/config/app.config.ts index 6761bb40..713ca6fb 100644 --- a/src/lib/config/app.config.ts +++ b/src/lib/config/app.config.ts @@ -1,12 +1,17 @@ // TODO: dedupe as much as possible. +import { isDevEnv } from "lib/config/env.config"; + const app = { name: "Backfeed", description: "Streamlined user feedback 📣", organization: "Omni", productionUrl: "https://backfeed.omni.dev", supportName: "Omni Support", - supportEmail: "team@support.omni.dev", + supportEmailAddress: "support@omni.dev", + fromEmailAddress: isDevEnv + ? "onboarding@resend.dev" + : "team@support.omni.dev", identityUrl: "https://identity.omni.dev", forgotPasswordUrl: "https://identity.omni.dev/forgot-password", docsUrl: "https://docs.omni.dev/backfeed/overview", From 30fc676eb25bcfb96e66c2a62ee9adbc8dd0a451 Mon Sep 17 00:00:00 2001 From: Brian Cooper Date: Mon, 28 Apr 2025 23:57:39 -0500 Subject: [PATCH 007/103] chore(env): extract from/to email addresses to env vars --- .env.development | 5 +++++ .env.production | 3 +++ src/app/api/invite/route.ts | 11 ++++++++--- src/lib/config/app.config.ts | 3 --- src/lib/config/env.config.ts | 4 ++++ 5 files changed, 20 insertions(+), 6 deletions(-) diff --git a/.env.development b/.env.development index c1090846..061e1d29 100644 --- a/.env.development +++ b/.env.development @@ -5,5 +5,10 @@ NEXT_PUBLIC_API_GRAPHQL_URL="http://127.0.0.1:4000/graphql" # TODO switch to base path (https://linear.app/omnidev/issue/OMNI-254/move-apiauth-paths-to-base-path-or-subpath-eg-auth) NEXT_PUBLIC_AUTH_ISSUER="https://localhost:8000/api/auth" +# payment processing # whether to connect to Polar's sandbox environment (https://docs.polar.sh/integrate/sandbox) NEXT_PUBLIC_ENABLE_POLAR_SANDBOX="true" + +# emails +NEXT_PUBLIC_FROM_EMAIL_ADDRESS="onboarding@resend.dev" +NEXT_PUBLIC_TO_EMAIL_ADDRESS="delivered@resend.dev" diff --git a/.env.production b/.env.production index d0c66536..96e0f9f4 100644 --- a/.env.production +++ b/.env.production @@ -3,3 +3,6 @@ NEXT_PUBLIC_API_BASE_URL="https://api.backfeed.omni.dev" NEXT_PUBLIC_API_GRAPHQL_URL="https://api.backfeed.omni.dev/graphql" # TODO switch to base path (https://linear.app/omnidev/issue/OMNI-254/move-apiauth-paths-to-base-path-or-subpath-eg-auth) NEXT_PUBLIC_AUTH_ISSUER="https://identity.omni.dev/api/auth" + +# emails +NEXT_PUBLIC_FROM_EMAIL_ADDRESS="team@support.omni.dev" diff --git a/src/app/api/invite/route.ts b/src/app/api/invite/route.ts index 515653c3..4580ec5c 100644 --- a/src/app/api/invite/route.ts +++ b/src/app/api/invite/route.ts @@ -2,7 +2,12 @@ import { Resend } from "resend"; import { auth } from "auth"; import { InviteMemberEmailTemplate } from "components/organization"; -import { app, isDevEnv } from "lib/config"; +import { + app, + FROM_EMAIL_ADDRESS, + isDevEnv, + TO_EMAIL_ADDRESS, +} from "lib/config"; import type { OrganizationInvitation } from "components/organization"; import type { NextRequest } from "next/server"; @@ -28,8 +33,8 @@ export const POST = async (req: NextRequest) => { (await req.json()) as OrganizationInvitation; const { data, error } = await resend.emails.send({ - from: `${app.supportName} <${app.fromEmailAddress}>`, - to: isDevEnv ? "delivered@resend.dev" : recipientEmail, + from: `${app.supportName} <${FROM_EMAIL_ADDRESS}>`, + to: TO_EMAIL_ADDRESS || recipientEmail, subject: `${emailTemplate.subject.value1} ${organizationName} ${emailTemplate.subject.value2} ${app.name}`, react: InviteMemberEmailTemplate({ inviterUsername, diff --git a/src/lib/config/app.config.ts b/src/lib/config/app.config.ts index 713ca6fb..06bee60c 100644 --- a/src/lib/config/app.config.ts +++ b/src/lib/config/app.config.ts @@ -9,9 +9,6 @@ const app = { productionUrl: "https://backfeed.omni.dev", supportName: "Omni Support", supportEmailAddress: "support@omni.dev", - fromEmailAddress: isDevEnv - ? "onboarding@resend.dev" - : "team@support.omni.dev", identityUrl: "https://identity.omni.dev", forgotPasswordUrl: "https://identity.omni.dev/forgot-password", docsUrl: "https://docs.omni.dev/backfeed/overview", diff --git a/src/lib/config/env.config.ts b/src/lib/config/env.config.ts index f42aaf01..4af96ffd 100644 --- a/src/lib/config/env.config.ts +++ b/src/lib/config/env.config.ts @@ -19,6 +19,10 @@ export const AUTH_CLIENT_SECRET = process.env.AUTH_CLIENT_SECRET; export const ENABLE_POLAR_SANDBOX = process.env.NEXT_PUBLIC_ENABLE_POLAR_SANDBOX === "true"; +// emails +export const FROM_EMAIL_ADDRESS = process.env.NEXT_PUBLIC_FROM_EMAIL_ADDRESS; +export const TO_EMAIL_ADDRESS = process.env.NEXT_PUBLIC_TO_EMAIL_ADDRESS; + // tests // enable mock service worker (https://mswjs.io/docs/integrations/browser#conditionally-enable-mocking), this is wrapped in case mocking requests and responses during development is desired export const ENABLE_MSW = process.env.ENABLE_MSW || isTestEnv; From 2f9be6f171f66a0f4d0eab4fafd37b056308517c Mon Sep 17 00:00:00 2001 From: Brian Cooper Date: Mon, 28 Apr 2025 23:58:28 -0500 Subject: [PATCH 008/103] chore: remove unused imports --- src/app/api/invite/route.ts | 7 +------ src/lib/config/app.config.ts | 2 -- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/app/api/invite/route.ts b/src/app/api/invite/route.ts index 4580ec5c..74b3e3af 100644 --- a/src/app/api/invite/route.ts +++ b/src/app/api/invite/route.ts @@ -2,12 +2,7 @@ import { Resend } from "resend"; import { auth } from "auth"; import { InviteMemberEmailTemplate } from "components/organization"; -import { - app, - FROM_EMAIL_ADDRESS, - isDevEnv, - TO_EMAIL_ADDRESS, -} from "lib/config"; +import { app, FROM_EMAIL_ADDRESS, TO_EMAIL_ADDRESS } from "lib/config"; import type { OrganizationInvitation } from "components/organization"; import type { NextRequest } from "next/server"; diff --git a/src/lib/config/app.config.ts b/src/lib/config/app.config.ts index 06bee60c..d040212d 100644 --- a/src/lib/config/app.config.ts +++ b/src/lib/config/app.config.ts @@ -1,7 +1,5 @@ // TODO: dedupe as much as possible. -import { isDevEnv } from "lib/config/env.config"; - const app = { name: "Backfeed", description: "Streamlined user feedback 📣", From 6dfbc8849c7edab2ff3dcef3e5927b4c23e8c4cc Mon Sep 17 00:00:00 2001 From: Brian Cooper Date: Mon, 28 Apr 2025 23:59:21 -0500 Subject: [PATCH 009/103] chore: rename 'identityUrl' -> 'identityProviderUrl' --- src/app/profile/[userId]/account/page.tsx | 2 +- src/lib/config/app.config.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/profile/[userId]/account/page.tsx b/src/app/profile/[userId]/account/page.tsx index ec8a36bd..c4047b6b 100644 --- a/src/app/profile/[userId]/account/page.tsx +++ b/src/app/profile/[userId]/account/page.tsx @@ -44,7 +44,7 @@ const ProfileAccountPage = async ({ params }: Props) => { label: app.profileAccountPage.cta.updateProfile.label, // TODO: get Sigil Icon component working and update accordingly. Context: https://github.com/omnidotdev/backfeed-app/pull/44#discussion_r1897974331 icon: , - href: app.identityUrl, + href: app.identityProviderUrl, }, ], }} diff --git a/src/lib/config/app.config.ts b/src/lib/config/app.config.ts index d040212d..ec634449 100644 --- a/src/lib/config/app.config.ts +++ b/src/lib/config/app.config.ts @@ -7,7 +7,7 @@ const app = { productionUrl: "https://backfeed.omni.dev", supportName: "Omni Support", supportEmailAddress: "support@omni.dev", - identityUrl: "https://identity.omni.dev", + identityProviderUrl: "https://identity.omni.dev", forgotPasswordUrl: "https://identity.omni.dev/forgot-password", docsUrl: "https://docs.omni.dev/backfeed/overview", breadcrumb: "Home", From 5db3003130ae5f81195e15378957220ab6fd8e28 Mon Sep 17 00:00:00 2001 From: Brian Cooper Date: Tue, 29 Apr 2025 00:05:00 -0500 Subject: [PATCH 010/103] refactor(app-config): nest organization-related strings into `organization` accessor --- src/app/api/invite/route.ts | 2 +- src/app/error.tsx | 4 ++-- src/app/profile/[userId]/account/page.tsx | 2 +- src/components/layout/Footer/Footer.tsx | 2 +- src/lib/config/app.config.ts | 11 ++++++----- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/app/api/invite/route.ts b/src/app/api/invite/route.ts index 74b3e3af..1226e700 100644 --- a/src/app/api/invite/route.ts +++ b/src/app/api/invite/route.ts @@ -28,7 +28,7 @@ export const POST = async (req: NextRequest) => { (await req.json()) as OrganizationInvitation; const { data, error } = await resend.emails.send({ - from: `${app.supportName} <${FROM_EMAIL_ADDRESS}>`, + from: `${app.organization.supportEmailDisplayName} <${FROM_EMAIL_ADDRESS}>`, to: TO_EMAIL_ADDRESS || recipientEmail, subject: `${emailTemplate.subject.value1} ${organizationName} ${emailTemplate.subject.value2} ${app.name}`, react: InviteMemberEmailTemplate({ diff --git a/src/app/error.tsx b/src/app/error.tsx index 45c6ec56..2bc958b9 100644 --- a/src/app/error.tsx +++ b/src/app/error.tsx @@ -18,9 +18,9 @@ const GlobalErrorPage = () => { {app.globalError.description}{" "} - + - {app.supportEmailAddress} + {app.organization.supportEmailAddress} . diff --git a/src/app/profile/[userId]/account/page.tsx b/src/app/profile/[userId]/account/page.tsx index c4047b6b..db3a212e 100644 --- a/src/app/profile/[userId]/account/page.tsx +++ b/src/app/profile/[userId]/account/page.tsx @@ -44,7 +44,7 @@ const ProfileAccountPage = async ({ params }: Props) => { label: app.profileAccountPage.cta.updateProfile.label, // TODO: get Sigil Icon component working and update accordingly. Context: https://github.com/omnidotdev/backfeed-app/pull/44#discussion_r1897974331 icon: , - href: app.identityProviderUrl, + href: app.organization.identityProviderUrl, }, ], }} diff --git a/src/components/layout/Footer/Footer.tsx b/src/components/layout/Footer/Footer.tsx index e347a067..0adce19d 100644 --- a/src/components/layout/Footer/Footer.tsx +++ b/src/components/layout/Footer/Footer.tsx @@ -21,7 +21,7 @@ const Footer = () => ( borderColor: "border.subtle", })} > - © {new Date().getFullYear()} {app.organization} + © {new Date().getFullYear()} {app.organization.name} ); diff --git a/src/lib/config/app.config.ts b/src/lib/config/app.config.ts index ec634449..d159fd03 100644 --- a/src/lib/config/app.config.ts +++ b/src/lib/config/app.config.ts @@ -3,13 +3,14 @@ const app = { name: "Backfeed", description: "Streamlined user feedback 📣", - organization: "Omni", productionUrl: "https://backfeed.omni.dev", - supportName: "Omni Support", - supportEmailAddress: "support@omni.dev", - identityProviderUrl: "https://identity.omni.dev", - forgotPasswordUrl: "https://identity.omni.dev/forgot-password", docsUrl: "https://docs.omni.dev/backfeed/overview", + organization: { + name: "Omni", + supportEmailDisplayName: "Omni Support", + supportEmailAddress: "support@omni.dev", + identityProviderUrl: "https://identity.omni.dev", + }, breadcrumb: "Home", unsavedChanges: { description: "You have unsaved changes.", From 98143c928eedd2e559a9fe4d4b704168238fcb8f Mon Sep 17 00:00:00 2001 From: Brian Cooper Date: Tue, 29 Apr 2025 00:06:29 -0500 Subject: [PATCH 011/103] chore(app-config): add JSDoc --- src/lib/config/app.config.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/lib/config/app.config.ts b/src/lib/config/app.config.ts index d159fd03..44bd7627 100644 --- a/src/lib/config/app.config.ts +++ b/src/lib/config/app.config.ts @@ -1,5 +1,8 @@ // TODO: dedupe as much as possible. +/** + * Application configuration. + */ const app = { name: "Backfeed", description: "Streamlined user feedback 📣", From 121499693dd50ec51b617774639c5ae932f748ea Mon Sep 17 00:00:00 2001 From: Brian Cooper Date: Tue, 29 Apr 2025 00:11:03 -0500 Subject: [PATCH 012/103] feature(recent-feedback): add project name --- src/components/dashboard/Response/Response.tsx | 18 ++++++++++++++---- src/generated/graphql.sdk.ts | 5 ++++- src/generated/graphql.ts | 5 ++++- .../queries/recentFeedback.query.graphql | 3 +++ 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/components/dashboard/Response/Response.tsx b/src/components/dashboard/Response/Response.tsx index b9c8cc2d..9b05ae4c 100644 --- a/src/components/dashboard/Response/Response.tsx +++ b/src/components/dashboard/Response/Response.tsx @@ -1,7 +1,8 @@ "use client"; -import { Flex, Text } from "@omnidev/sigil"; +import { Flex, HStack, Icon, Stack, Text } from "@omnidev/sigil"; import dayjs from "dayjs"; +import { HiOutlineFolder } from "react-icons/hi2"; import { StatusBadge } from "components/core"; @@ -42,9 +43,18 @@ const Response = ({ feedback, ...rest }: Props) => { - - {date} - + + + {date} + + + + + + {feedback.project?.name} + + + ); }; diff --git a/src/generated/graphql.sdk.ts b/src/generated/graphql.sdk.ts index a7f0b45d..3c51e132 100644 --- a/src/generated/graphql.sdk.ts +++ b/src/generated/graphql.sdk.ts @@ -5252,7 +5252,7 @@ export type RecentFeedbackQueryVariables = Exact<{ }>; -export type RecentFeedbackQuery = { __typename?: 'Query', posts?: { __typename?: 'PostConnection', nodes: Array<{ __typename?: 'Post', rowId: string, createdAt?: Date | null, title?: string | null, description?: string | null, status?: { __typename?: 'PostStatus', rowId: string, status: string, color?: string | null } | null, user?: { __typename?: 'User', rowId: string, username?: string | null } | null } | null> } | null }; +export type RecentFeedbackQuery = { __typename?: 'Query', posts?: { __typename?: 'PostConnection', nodes: Array<{ __typename?: 'Post', rowId: string, createdAt?: Date | null, title?: string | null, description?: string | null, project?: { __typename?: 'Project', name: string } | null, status?: { __typename?: 'PostStatus', rowId: string, status: string, color?: string | null } | null, user?: { __typename?: 'User', rowId: string, username?: string | null } | null } | null> } | null }; export type StatusBreakdownQueryVariables = Exact<{ projectId: Scalars['UUID']['input']; @@ -5857,6 +5857,9 @@ export const RecentFeedbackDocument = gql` createdAt title description + project { + name + } status { rowId status diff --git a/src/generated/graphql.ts b/src/generated/graphql.ts index 0489028d..a315c4d0 100644 --- a/src/generated/graphql.ts +++ b/src/generated/graphql.ts @@ -5251,7 +5251,7 @@ export type RecentFeedbackQueryVariables = Exact<{ }>; -export type RecentFeedbackQuery = { __typename?: 'Query', posts?: { __typename?: 'PostConnection', nodes: Array<{ __typename?: 'Post', rowId: string, createdAt?: Date | null, title?: string | null, description?: string | null, status?: { __typename?: 'PostStatus', rowId: string, status: string, color?: string | null } | null, user?: { __typename?: 'User', rowId: string, username?: string | null } | null } | null> } | null }; +export type RecentFeedbackQuery = { __typename?: 'Query', posts?: { __typename?: 'PostConnection', nodes: Array<{ __typename?: 'Post', rowId: string, createdAt?: Date | null, title?: string | null, description?: string | null, project?: { __typename?: 'Project', name: string } | null, status?: { __typename?: 'PostStatus', rowId: string, status: string, color?: string | null } | null, user?: { __typename?: 'User', rowId: string, username?: string | null } | null } | null> } | null }; export type StatusBreakdownQueryVariables = Exact<{ projectId: Scalars['UUID']['input']; @@ -7020,6 +7020,9 @@ export const RecentFeedbackDocument = ` createdAt title description + project { + name + } status { rowId status diff --git a/src/lib/graphql/queries/recentFeedback.query.graphql b/src/lib/graphql/queries/recentFeedback.query.graphql index 47b43aec..3465fb77 100644 --- a/src/lib/graphql/queries/recentFeedback.query.graphql +++ b/src/lib/graphql/queries/recentFeedback.query.graphql @@ -13,6 +13,9 @@ query RecentFeedback($userId: UUID!) { createdAt title description + project { + name + } status { rowId status From 55ebf0875b351760944a670edbce768b27f46a3a Mon Sep 17 00:00:00 2001 From: Brian Cooper Date: Tue, 29 Apr 2025 00:24:14 -0500 Subject: [PATCH 013/103] feature(recent-feedback): add link, change feedback description to title --- .../RecentFeedback/RecentFeedback.tsx | 19 +++++++++++++------ .../dashboard/Response/Response.tsx | 10 +++++----- src/generated/graphql.sdk.ts | 6 +++++- src/generated/graphql.ts | 6 +++++- .../queries/recentFeedback.query.graphql | 4 ++++ 5 files changed, 32 insertions(+), 13 deletions(-) diff --git a/src/components/dashboard/RecentFeedback/RecentFeedback.tsx b/src/components/dashboard/RecentFeedback/RecentFeedback.tsx index a789a509..df8bbf9d 100644 --- a/src/components/dashboard/RecentFeedback/RecentFeedback.tsx +++ b/src/components/dashboard/RecentFeedback/RecentFeedback.tsx @@ -1,6 +1,7 @@ "use client"; import { Flex } from "@omnidev/sigil"; +import Link from "next/link"; import { SkeletonArray } from "components/core"; import { FeedbackSection, Response } from "components/dashboard"; @@ -50,13 +51,19 @@ const RecentFeedback = () => { ) : recentFeedback?.length ? ( recentFeedback?.map((feedback) => ( - } - borderBottomWidth={{ base: "1px", _last: 0 }} - pt={{ base: 3, _first: 0 }} - pb={{ base: 3, _last: 6 }} - /> + href={`/organizations/${feedback?.project?.organization?.slug}/projects/${feedback?.project?.slug}/${feedback?.rowId}`} + > + } + p={2} + _hover={{ + bgColor: "background.muted", + borderRadius: "md", + }} + /> + )) ) : ( { : startOfDay.fromNow(); return ( - - + + {feedback?.user?.username} @@ -39,9 +39,9 @@ const Response = ({ feedback, ...rest }: Props) => { - {feedback?.description} + {feedback?.title} - + @@ -55,7 +55,7 @@ const Response = ({ feedback, ...rest }: Props) => { - + ); }; diff --git a/src/generated/graphql.sdk.ts b/src/generated/graphql.sdk.ts index 3c51e132..d7dae839 100644 --- a/src/generated/graphql.sdk.ts +++ b/src/generated/graphql.sdk.ts @@ -5252,7 +5252,7 @@ export type RecentFeedbackQueryVariables = Exact<{ }>; -export type RecentFeedbackQuery = { __typename?: 'Query', posts?: { __typename?: 'PostConnection', nodes: Array<{ __typename?: 'Post', rowId: string, createdAt?: Date | null, title?: string | null, description?: string | null, project?: { __typename?: 'Project', name: string } | null, status?: { __typename?: 'PostStatus', rowId: string, status: string, color?: string | null } | null, user?: { __typename?: 'User', rowId: string, username?: string | null } | null } | null> } | null }; +export type RecentFeedbackQuery = { __typename?: 'Query', posts?: { __typename?: 'PostConnection', nodes: Array<{ __typename?: 'Post', rowId: string, createdAt?: Date | null, title?: string | null, description?: string | null, project?: { __typename?: 'Project', name: string, slug: string, organization?: { __typename?: 'Organization', slug: string } | null } | null, status?: { __typename?: 'PostStatus', rowId: string, status: string, color?: string | null } | null, user?: { __typename?: 'User', rowId: string, username?: string | null } | null } | null> } | null }; export type StatusBreakdownQueryVariables = Exact<{ projectId: Scalars['UUID']['input']; @@ -5859,6 +5859,10 @@ export const RecentFeedbackDocument = gql` description project { name + slug + organization { + slug + } } status { rowId diff --git a/src/generated/graphql.ts b/src/generated/graphql.ts index a315c4d0..cad399fb 100644 --- a/src/generated/graphql.ts +++ b/src/generated/graphql.ts @@ -5251,7 +5251,7 @@ export type RecentFeedbackQueryVariables = Exact<{ }>; -export type RecentFeedbackQuery = { __typename?: 'Query', posts?: { __typename?: 'PostConnection', nodes: Array<{ __typename?: 'Post', rowId: string, createdAt?: Date | null, title?: string | null, description?: string | null, project?: { __typename?: 'Project', name: string } | null, status?: { __typename?: 'PostStatus', rowId: string, status: string, color?: string | null } | null, user?: { __typename?: 'User', rowId: string, username?: string | null } | null } | null> } | null }; +export type RecentFeedbackQuery = { __typename?: 'Query', posts?: { __typename?: 'PostConnection', nodes: Array<{ __typename?: 'Post', rowId: string, createdAt?: Date | null, title?: string | null, description?: string | null, project?: { __typename?: 'Project', name: string, slug: string, organization?: { __typename?: 'Organization', slug: string } | null } | null, status?: { __typename?: 'PostStatus', rowId: string, status: string, color?: string | null } | null, user?: { __typename?: 'User', rowId: string, username?: string | null } | null } | null> } | null }; export type StatusBreakdownQueryVariables = Exact<{ projectId: Scalars['UUID']['input']; @@ -7022,6 +7022,10 @@ export const RecentFeedbackDocument = ` description project { name + slug + organization { + slug + } } status { rowId diff --git a/src/lib/graphql/queries/recentFeedback.query.graphql b/src/lib/graphql/queries/recentFeedback.query.graphql index 3465fb77..28f37f64 100644 --- a/src/lib/graphql/queries/recentFeedback.query.graphql +++ b/src/lib/graphql/queries/recentFeedback.query.graphql @@ -15,6 +15,10 @@ query RecentFeedback($userId: UUID!) { description project { name + slug + organization { + slug + } } status { rowId From b14508d47e9f1447838833aa5050f2c53b06ba5c Mon Sep 17 00:00:00 2001 From: Brian Cooper Date: Tue, 29 Apr 2025 00:24:33 -0500 Subject: [PATCH 014/103] chore(app-config): normalize copy (remove punctuation) --- src/lib/config/app.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/config/app.config.ts b/src/lib/config/app.config.ts index 44bd7627..f970afd0 100644 --- a/src/lib/config/app.config.ts +++ b/src/lib/config/app.config.ts @@ -143,7 +143,7 @@ const app = { description: "Here's what's happening with your feedback today.", organizations: { title: "Organizations", - description: "Quickly view organizations that you are a member of.", + description: "Quickly view organizations that you are a member of", emptyState: { message: "No organizations found. Would you like to create one?", cta: { From c7c99d7bde9cbf9d2f8ab8eef67b43580b0c7344 Mon Sep 17 00:00:00 2001 From: Brian Cooper Date: Tue, 29 Apr 2025 00:37:29 -0500 Subject: [PATCH 015/103] style(recent-feedback): add information to card --- .../dashboard/Response/Response.tsx | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/components/dashboard/Response/Response.tsx b/src/components/dashboard/Response/Response.tsx index 0075b26f..22b06d7a 100644 --- a/src/components/dashboard/Response/Response.tsx +++ b/src/components/dashboard/Response/Response.tsx @@ -2,7 +2,11 @@ import { Flex, HStack, Icon, Stack, Text } from "@omnidev/sigil"; import dayjs from "dayjs"; -import { HiOutlineFolder } from "react-icons/hi2"; +import { + HiOutlineCalendar, + HiOutlineFolder, + HiOutlineUser, +} from "react-icons/hi2"; import { StatusBadge } from "components/core"; @@ -32,28 +36,30 @@ const Response = ({ feedback, ...rest }: Props) => { - {feedback?.user?.username} + {feedback?.title} - {feedback?.title} + {feedback?.description} - - - {date} - - - - - - {feedback.project?.name} - - + + {[ + { icon: HiOutlineCalendar, text: date }, + { icon: HiOutlineFolder, text: feedback.project?.name }, + { icon: HiOutlineUser, text: feedback.user?.username }, + ].map((item) => ( + + + + {item.text} + + + ))} ); From 0b3888443814af8b746b474e849a0c17b7cf4885 Mon Sep 17 00:00:00 2001 From: Brian Cooper Date: Tue, 29 Apr 2025 01:19:25 -0500 Subject: [PATCH 016/103] fix(recent-feedback): use custom link component --- src/components/dashboard/RecentFeedback/RecentFeedback.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/dashboard/RecentFeedback/RecentFeedback.tsx b/src/components/dashboard/RecentFeedback/RecentFeedback.tsx index df8bbf9d..cde94fe7 100644 --- a/src/components/dashboard/RecentFeedback/RecentFeedback.tsx +++ b/src/components/dashboard/RecentFeedback/RecentFeedback.tsx @@ -1,9 +1,8 @@ "use client"; import { Flex } from "@omnidev/sigil"; -import Link from "next/link"; -import { SkeletonArray } from "components/core"; +import { Link, SkeletonArray } from "components/core"; import { FeedbackSection, Response } from "components/dashboard"; import { EmptyState, ErrorBoundary } from "components/layout"; import { useRecentFeedbackQuery } from "generated/graphql"; From e31976f59beb5c6cdbd0427af4962ee78d08cb24 Mon Sep 17 00:00:00 2001 From: Brian Cooper Date: Tue, 29 Apr 2025 01:19:42 -0500 Subject: [PATCH 017/103] docs(link): update props docs --- src/components/core/Link/Link.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/core/Link/Link.tsx b/src/components/core/Link/Link.tsx index 3729c8b6..b1e0bf0f 100644 --- a/src/components/core/Link/Link.tsx +++ b/src/components/core/Link/Link.tsx @@ -4,7 +4,7 @@ import type { LinkProps } from "next/link"; import type { HTMLAttributes } from "react"; interface Props extends LinkProps, HTMLAttributes { - /** State to determine if the link is disabled. */ + /** Whether the link is disabled. */ disabled?: boolean; } From fbd6233b7fc3bc54dcf5dfffbba33a9d5d5c8f09 Mon Sep 17 00:00:00 2001 From: Brian Cooper Date: Tue, 29 Apr 2025 01:25:53 -0500 Subject: [PATCH 018/103] style(recent-feedback): set last item bottom padding --- src/components/dashboard/RecentFeedback/RecentFeedback.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/dashboard/RecentFeedback/RecentFeedback.tsx b/src/components/dashboard/RecentFeedback/RecentFeedback.tsx index cde94fe7..5d0873f1 100644 --- a/src/components/dashboard/RecentFeedback/RecentFeedback.tsx +++ b/src/components/dashboard/RecentFeedback/RecentFeedback.tsx @@ -1,6 +1,6 @@ "use client"; -import { Flex } from "@omnidev/sigil"; +import { Flex, Stack } from "@omnidev/sigil"; import { Link, SkeletonArray } from "components/core"; import { FeedbackSection, Response } from "components/dashboard"; @@ -45,7 +45,7 @@ const RecentFeedback = () => { w="full" /> ) : ( - + {isLoading ? ( ) : recentFeedback?.length ? ( @@ -57,6 +57,7 @@ const RecentFeedback = () => { } p={2} + _last={{ pb: 6 }} _hover={{ bgColor: "background.muted", borderRadius: "md", @@ -72,7 +73,7 @@ const RecentFeedback = () => { w="full" /> )} - + )} ); From ed5f541eca022fb2546afee3174aaa5aa751212a Mon Sep 17 00:00:00 2001 From: Brian Cooper Date: Tue, 29 Apr 2025 01:26:17 -0500 Subject: [PATCH 019/103] refactor(feedback-section): Flex -> Stack --- .../dashboard/FeedbackSection/FeedbackSection.tsx | 11 +++++------ .../dashboard/RecentFeedback/RecentFeedback.tsx | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/components/dashboard/FeedbackSection/FeedbackSection.tsx b/src/components/dashboard/FeedbackSection/FeedbackSection.tsx index efa04bca..6709f3f0 100644 --- a/src/components/dashboard/FeedbackSection/FeedbackSection.tsx +++ b/src/components/dashboard/FeedbackSection/FeedbackSection.tsx @@ -1,6 +1,6 @@ "use client"; -import { Flex, Text } from "@omnidev/sigil"; +import { Stack, Text } from "@omnidev/sigil"; import type { FlexProps } from "@omnidev/sigil"; @@ -15,9 +15,8 @@ interface Props extends FlexProps { * Feedback section. */ const FeedbackSection = ({ title, children, contentProps, ...rest }: Props) => ( - ( {title} - + {children} - - + + ); export default FeedbackSection; diff --git a/src/components/dashboard/RecentFeedback/RecentFeedback.tsx b/src/components/dashboard/RecentFeedback/RecentFeedback.tsx index 5d0873f1..476f1ca1 100644 --- a/src/components/dashboard/RecentFeedback/RecentFeedback.tsx +++ b/src/components/dashboard/RecentFeedback/RecentFeedback.tsx @@ -1,6 +1,6 @@ "use client"; -import { Flex, Stack } from "@omnidev/sigil"; +import { Stack } from "@omnidev/sigil"; import { Link, SkeletonArray } from "components/core"; import { FeedbackSection, Response } from "components/dashboard"; From fca46a08535a2b8514d59fe45d5c27c51b1d8420 Mon Sep 17 00:00:00 2001 From: Brian Cooper Date: Tue, 29 Apr 2025 01:28:44 -0500 Subject: [PATCH 020/103] style(recent-feedback): set last item bottom margin --- src/components/dashboard/RecentFeedback/RecentFeedback.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/dashboard/RecentFeedback/RecentFeedback.tsx b/src/components/dashboard/RecentFeedback/RecentFeedback.tsx index 476f1ca1..80581c90 100644 --- a/src/components/dashboard/RecentFeedback/RecentFeedback.tsx +++ b/src/components/dashboard/RecentFeedback/RecentFeedback.tsx @@ -57,7 +57,7 @@ const RecentFeedback = () => { } p={2} - _last={{ pb: 6 }} + _last={{ mb: 6 }} _hover={{ bgColor: "background.muted", borderRadius: "md", From 985f867fdcabf16b9a6041ade17f6432dcd6c5fb Mon Sep 17 00:00:00 2001 From: Brian Cooper Date: Tue, 29 Apr 2025 01:32:55 -0500 Subject: [PATCH 021/103] chore(comments): remove min length --- src/components/feedback/CreateComment/CreateComment.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/feedback/CreateComment/CreateComment.tsx b/src/components/feedback/CreateComment/CreateComment.tsx index 9eb53757..9320502b 100644 --- a/src/components/feedback/CreateComment/CreateComment.tsx +++ b/src/components/feedback/CreateComment/CreateComment.tsx @@ -27,7 +27,6 @@ const createCommentSchema = z.object({ message: z .string() .trim() - .min(10, app.feedbackPage.comments.createComment.errors.minLengthMessage) .max( MAX_COMMENT_LENGTH, app.feedbackPage.comments.createComment.errors.maxLengthMessage, From 0a2186b7da93d9672c7dfef4154617161feafafc Mon Sep 17 00:00:00 2001 From: hobbescodes <87732294+hobbescodes@users.noreply.github.com> Date: Tue, 29 Apr 2025 09:06:24 -0500 Subject: [PATCH 022/103] refactor(recent-feedback): update responsive design --- .../FeedbackSection/FeedbackSection.tsx | 4 +-- .../RecentFeedback/RecentFeedback.tsx | 32 +++++++++---------- .../dashboard/Response/Response.tsx | 3 +- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/components/dashboard/FeedbackSection/FeedbackSection.tsx b/src/components/dashboard/FeedbackSection/FeedbackSection.tsx index 6709f3f0..daa9626b 100644 --- a/src/components/dashboard/FeedbackSection/FeedbackSection.tsx +++ b/src/components/dashboard/FeedbackSection/FeedbackSection.tsx @@ -2,13 +2,13 @@ import { Stack, Text } from "@omnidev/sigil"; -import type { FlexProps } from "@omnidev/sigil"; +import type { FlexProps, StackProps } from "@omnidev/sigil"; interface Props extends FlexProps { /** Section title. */ title: string; /** Props to pass to the main content container. */ - contentProps?: FlexProps; + contentProps?: StackProps; } /** diff --git a/src/components/dashboard/RecentFeedback/RecentFeedback.tsx b/src/components/dashboard/RecentFeedback/RecentFeedback.tsx index 80581c90..1dde1e0d 100644 --- a/src/components/dashboard/RecentFeedback/RecentFeedback.tsx +++ b/src/components/dashboard/RecentFeedback/RecentFeedback.tsx @@ -1,6 +1,6 @@ "use client"; -import { Stack } from "@omnidev/sigil"; +import { Flex, Stack } from "@omnidev/sigil"; import { Link, SkeletonArray } from "components/core"; import { FeedbackSection, Response } from "components/dashboard"; @@ -35,7 +35,7 @@ const RecentFeedback = () => { {isError ? ( { ) : recentFeedback?.length ? ( recentFeedback?.map((feedback) => ( - - } - p={2} - _last={{ mb: 6 }} - _hover={{ - bgColor: "background.muted", - borderRadius: "md", - }} - /> - + + + } + p={2} + _hover={{ + bgColor: "background.muted/40", + borderRadius: "md", + }} + /> + + )) ) : ( { - + {/* TODO: discuss possible issues with responsive design (i.e. long project name and/or long username) */} + {[ { icon: HiOutlineCalendar, text: date }, { icon: HiOutlineFolder, text: feedback.project?.name }, From 825b92db46acc7616cc8d90a03fb49cd05e263ee Mon Sep 17 00:00:00 2001 From: hobbescodes <87732294+hobbescodes@users.noreply.github.com> Date: Tue, 29 Apr 2025 09:52:19 -0500 Subject: [PATCH 023/103] refactor(dashboard): include today's feedback in feedback overview --- src/app/page.tsx | 6 +----- .../dashboard/DashboardPage/DashboardPage.tsx | 11 ++--------- .../dashboard/FeedbackOverview/FeedbackOverview.tsx | 7 ++----- src/components/dashboard/Response/Response.tsx | 9 +-------- src/generated/graphql.mock.ts | 2 +- src/generated/graphql.sdk.ts | 5 ++--- src/generated/graphql.ts | 5 ++--- src/lib/graphql/queries/weeklyFeedback.query.graphql | 8 ++------ 8 files changed, 13 insertions(+), 40 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index aa3092c6..63923a66 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -49,8 +49,7 @@ const HomePage = async () => { isMember: true, }; - const oneWeekAgo = dayjs().utc().subtract(7, "days").startOf("day").toDate(); - const startOfToday = dayjs().utc().startOf("day").toDate(); + const oneWeekAgo = dayjs().utc().subtract(6, "days").startOf("day").toDate(); await Promise.all([ queryClient.prefetchQuery({ @@ -81,12 +80,10 @@ const HomePage = async () => { queryKey: useWeeklyFeedbackQuery.getKey({ userId: session.user.rowId!, startDate: oneWeekAgo, - endDate: startOfToday, }), queryFn: useWeeklyFeedbackQuery.fetcher({ userId: session.user.rowId!, startDate: oneWeekAgo, - endDate: startOfToday, }), }), queryClient.prefetchQuery({ @@ -101,7 +98,6 @@ const HomePage = async () => { isBasicTier={isBasicTier} isTeamTier={isTeamTier} oneWeekAgo={oneWeekAgo} - startOfToday={startOfToday} /> {/* dialogs */} diff --git a/src/components/dashboard/DashboardPage/DashboardPage.tsx b/src/components/dashboard/DashboardPage/DashboardPage.tsx index 55c97fa5..f20f39dd 100644 --- a/src/components/dashboard/DashboardPage/DashboardPage.tsx +++ b/src/components/dashboard/DashboardPage/DashboardPage.tsx @@ -30,19 +30,12 @@ interface Props { isTeamTier: boolean; /** Start of day from one week ago. */ oneWeekAgo: Date; - /** Start of today. */ - startOfToday: Date; } /** * Dashboard page. This provides the main layout for the home page when the user is authenticated. */ -const DashboardPage = ({ - isBasicTier, - isTeamTier, - oneWeekAgo, - startOfToday, -}: Props) => { +const DashboardPage = ({ isBasicTier, isTeamTier, oneWeekAgo }: Props) => { const { user, isLoading: isAuthLoading } = useAuth(); const { @@ -122,7 +115,7 @@ const DashboardPage = ({ - + diff --git a/src/components/dashboard/FeedbackOverview/FeedbackOverview.tsx b/src/components/dashboard/FeedbackOverview/FeedbackOverview.tsx index 0ef18c70..1d7e1263 100644 --- a/src/components/dashboard/FeedbackOverview/FeedbackOverview.tsx +++ b/src/components/dashboard/FeedbackOverview/FeedbackOverview.tsx @@ -21,14 +21,12 @@ import { useAuth, useViewportSize } from "lib/hooks"; interface Props { /** Start of day from one week ago. */ oneWeekAgo: Date; - /** Start of today. */ - startOfToday: Date; } /** * Feedback overview section. Displays a bar chart that displays daily feedback volume for the past 7 days. */ -const FeedbackOverview = ({ oneWeekAgo, startOfToday }: Props) => { +const FeedbackOverview = ({ oneWeekAgo }: Props) => { const isLargeViewport = useViewportSize({ minWidth: "64em" }); const getFormattedDate = (diff: number) => @@ -44,7 +42,6 @@ const FeedbackOverview = ({ oneWeekAgo, startOfToday }: Props) => { { userId: user?.rowId!, startDate: oneWeekAgo, - endDate: startOfToday, }, { enabled: !!user?.rowId, @@ -80,7 +77,7 @@ const FeedbackOverview = ({ oneWeekAgo, startOfToday }: Props) => { contentProps={{ align: "center", justify: "center", - p: 4, + p: 2, }} > {!isLoading ? ( diff --git a/src/components/dashboard/Response/Response.tsx b/src/components/dashboard/Response/Response.tsx index 1f33d0d1..925d67f2 100644 --- a/src/components/dashboard/Response/Response.tsx +++ b/src/components/dashboard/Response/Response.tsx @@ -22,14 +22,7 @@ interface Props extends FlexProps { * Recent feedback response. */ const Response = ({ feedback, ...rest }: Props) => { - const startOfDay = dayjs(feedback?.createdAt).utc().startOf("day"); - - const isToday = dayjs.duration(dayjs().utc().diff(startOfDay)).asDays() < 1; - - // NB: `isToday` is used to stabilize the relative time in order to keep in sync with `FeedbackOverview` calculations. - const date = isToday - ? dayjs(feedback?.createdAt).utc().fromNow() - : startOfDay.fromNow(); + const date = dayjs(feedback?.createdAt).utc().fromNow(); return ( diff --git a/src/generated/graphql.mock.ts b/src/generated/graphql.mock.ts index cbcccc2e..1962b83c 100644 --- a/src/generated/graphql.mock.ts +++ b/src/generated/graphql.mock.ts @@ -1022,7 +1022,7 @@ export const mockUserByEmailQuery = (resolver: GraphQLResponseResolver { - * const { userId, startDate, endDate } = variables; + * const { userId, startDate } = variables; * return HttpResponse.json({ * data: { posts } * }) diff --git a/src/generated/graphql.sdk.ts b/src/generated/graphql.sdk.ts index d7dae839..3d7b1593 100644 --- a/src/generated/graphql.sdk.ts +++ b/src/generated/graphql.sdk.ts @@ -5286,7 +5286,6 @@ export type UserByEmailQuery = { __typename?: 'Query', userByEmail?: { __typenam export type WeeklyFeedbackQueryVariables = Exact<{ userId: Scalars['UUID']['input']; startDate: Scalars['Datetime']['input']; - endDate: Scalars['Datetime']['input']; }>; @@ -5916,9 +5915,9 @@ export const UserByEmailDocument = gql` } `; export const WeeklyFeedbackDocument = gql` - query WeeklyFeedback($userId: UUID!, $startDate: Datetime!, $endDate: Datetime!) { + query WeeklyFeedback($userId: UUID!, $startDate: Datetime!) { posts( - filter: {project: {organization: {members: {some: {userId: {equalTo: $userId}}}}}, createdAt: {greaterThanOrEqualTo: $startDate, lessThan: $endDate}} + filter: {project: {organization: {members: {some: {userId: {equalTo: $userId}}}}}, createdAt: {greaterThanOrEqualTo: $startDate}} ) { groupedAggregates(groupBy: [CREATED_AT_TRUNCATED_TO_DAY]) { keys diff --git a/src/generated/graphql.ts b/src/generated/graphql.ts index cad399fb..2e885d59 100644 --- a/src/generated/graphql.ts +++ b/src/generated/graphql.ts @@ -5285,7 +5285,6 @@ export type UserByEmailQuery = { __typename?: 'Query', userByEmail?: { __typenam export type WeeklyFeedbackQueryVariables = Exact<{ userId: Scalars['UUID']['input']; startDate: Scalars['Datetime']['input']; - endDate: Scalars['Datetime']['input']; }>; @@ -7294,9 +7293,9 @@ useInfiniteUserByEmailQuery.getKey = (variables: UserByEmailQueryVariables) => [ useUserByEmailQuery.fetcher = (variables: UserByEmailQueryVariables, options?: RequestInit['headers']) => graphqlFetch(UserByEmailDocument, variables, options); export const WeeklyFeedbackDocument = ` - query WeeklyFeedback($userId: UUID!, $startDate: Datetime!, $endDate: Datetime!) { + query WeeklyFeedback($userId: UUID!, $startDate: Datetime!) { posts( - filter: {project: {organization: {members: {some: {userId: {equalTo: $userId}}}}}, createdAt: {greaterThanOrEqualTo: $startDate, lessThan: $endDate}} + filter: {project: {organization: {members: {some: {userId: {equalTo: $userId}}}}}, createdAt: {greaterThanOrEqualTo: $startDate}} ) { groupedAggregates(groupBy: [CREATED_AT_TRUNCATED_TO_DAY]) { keys diff --git a/src/lib/graphql/queries/weeklyFeedback.query.graphql b/src/lib/graphql/queries/weeklyFeedback.query.graphql index 569b7bba..a609d0d9 100644 --- a/src/lib/graphql/queries/weeklyFeedback.query.graphql +++ b/src/lib/graphql/queries/weeklyFeedback.query.graphql @@ -1,14 +1,10 @@ -query WeeklyFeedback( - $userId: UUID! - $startDate: Datetime! - $endDate: Datetime! -) { +query WeeklyFeedback($userId: UUID!, $startDate: Datetime!) { posts( filter: { project: { organization: { members: { some: { userId: { equalTo: $userId } } } } } - createdAt: { greaterThanOrEqualTo: $startDate, lessThan: $endDate } + createdAt: { greaterThanOrEqualTo: $startDate } } ) { groupedAggregates(groupBy: [CREATED_AT_TRUNCATED_TO_DAY]) { From ad0e24fdbd68d348addf73163d14f6aa3389ad14 Mon Sep 17 00:00:00 2001 From: hobbescodes <87732294+hobbescodes@users.noreply.github.com> Date: Tue, 29 Apr 2025 10:02:09 -0500 Subject: [PATCH 024/103] refactor: remove UTC conversions --- src/app/page.tsx | 5 +---- .../dashboard/FeedbackOverview/FeedbackOverview.tsx | 4 ++-- src/components/dashboard/Response/Response.tsx | 2 +- src/components/layout/Layout/Layout.tsx | 2 -- 4 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index 63923a66..07c738c8 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,6 +1,5 @@ import { HydrationBoundary, dehydrate } from "@tanstack/react-query"; import dayjs from "dayjs"; -import utc from "dayjs/plugin/utc"; import { auth } from "auth"; import { DashboardPage } from "components/dashboard"; @@ -22,8 +21,6 @@ import { getQueryClient } from "lib/util"; import type { OrganizationsQueryVariables } from "generated/graphql"; -dayjs.extend(utc); - export const dynamic = "force-dynamic"; /** @@ -49,7 +46,7 @@ const HomePage = async () => { isMember: true, }; - const oneWeekAgo = dayjs().utc().subtract(6, "days").startOf("day").toDate(); + const oneWeekAgo = dayjs().subtract(6, "days").startOf("day").toDate(); await Promise.all([ queryClient.prefetchQuery({ diff --git a/src/components/dashboard/FeedbackOverview/FeedbackOverview.tsx b/src/components/dashboard/FeedbackOverview/FeedbackOverview.tsx index 1d7e1263..e195ca73 100644 --- a/src/components/dashboard/FeedbackOverview/FeedbackOverview.tsx +++ b/src/components/dashboard/FeedbackOverview/FeedbackOverview.tsx @@ -47,7 +47,7 @@ const FeedbackOverview = ({ oneWeekAgo }: Props) => { enabled: !!user?.rowId, select: (data) => data?.posts?.groupedAggregates?.map((aggregate) => ({ - name: dayjs(aggregate.keys?.[0]).utc().format("ddd"), + name: dayjs(aggregate.keys?.[0]).format("ddd"), total: Number(aggregate.distinctCount?.rowId), })), }, @@ -57,7 +57,7 @@ const FeedbackOverview = ({ oneWeekAgo }: Props) => { weeklyFeedback?.find((item) => item.name === date)?.total ?? 0; const DATA = Array.from({ length: 7 }).map((_, index) => { - const date = getFormattedDate(index + 1); + const date = getFormattedDate(index); return { name: date, diff --git a/src/components/dashboard/Response/Response.tsx b/src/components/dashboard/Response/Response.tsx index 925d67f2..f2ad8cca 100644 --- a/src/components/dashboard/Response/Response.tsx +++ b/src/components/dashboard/Response/Response.tsx @@ -22,7 +22,7 @@ interface Props extends FlexProps { * Recent feedback response. */ const Response = ({ feedback, ...rest }: Props) => { - const date = dayjs(feedback?.createdAt).utc().fromNow(); + const date = dayjs(feedback?.createdAt).fromNow(); return ( diff --git a/src/components/layout/Layout/Layout.tsx b/src/components/layout/Layout/Layout.tsx index 2e104da7..4d451893 100644 --- a/src/components/layout/Layout/Layout.tsx +++ b/src/components/layout/Layout/Layout.tsx @@ -4,7 +4,6 @@ import { Center, Flex, Toaster, css, sigil } from "@omnidev/sigil"; import dayjs from "dayjs"; import duration from "dayjs/plugin/duration"; import relativeTime from "dayjs/plugin/relativeTime"; -import utc from "dayjs/plugin/utc"; import { useIsClient } from "usehooks-ts"; import { Footer, Header } from "components/layout"; @@ -15,7 +14,6 @@ import type { PropsWithChildren } from "react"; dayjs.extend(duration); dayjs.extend(relativeTime); -dayjs.extend(utc); /** * Core application layout. From aa73d820e301b202ccefeb634ff0ceba4c0855d7 Mon Sep 17 00:00:00 2001 From: hobbescodes <87732294+hobbescodes@users.noreply.github.com> Date: Tue, 29 Apr 2025 10:06:15 -0500 Subject: [PATCH 025/103] Revert "refactor: remove UTC conversions" This reverts commit ad0e24fdbd68d348addf73163d14f6aa3389ad14. --- src/app/page.tsx | 5 ++++- .../dashboard/FeedbackOverview/FeedbackOverview.tsx | 4 ++-- src/components/dashboard/Response/Response.tsx | 2 +- src/components/layout/Layout/Layout.tsx | 2 ++ 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index 07c738c8..63923a66 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,5 +1,6 @@ import { HydrationBoundary, dehydrate } from "@tanstack/react-query"; import dayjs from "dayjs"; +import utc from "dayjs/plugin/utc"; import { auth } from "auth"; import { DashboardPage } from "components/dashboard"; @@ -21,6 +22,8 @@ import { getQueryClient } from "lib/util"; import type { OrganizationsQueryVariables } from "generated/graphql"; +dayjs.extend(utc); + export const dynamic = "force-dynamic"; /** @@ -46,7 +49,7 @@ const HomePage = async () => { isMember: true, }; - const oneWeekAgo = dayjs().subtract(6, "days").startOf("day").toDate(); + const oneWeekAgo = dayjs().utc().subtract(6, "days").startOf("day").toDate(); await Promise.all([ queryClient.prefetchQuery({ diff --git a/src/components/dashboard/FeedbackOverview/FeedbackOverview.tsx b/src/components/dashboard/FeedbackOverview/FeedbackOverview.tsx index e195ca73..1d7e1263 100644 --- a/src/components/dashboard/FeedbackOverview/FeedbackOverview.tsx +++ b/src/components/dashboard/FeedbackOverview/FeedbackOverview.tsx @@ -47,7 +47,7 @@ const FeedbackOverview = ({ oneWeekAgo }: Props) => { enabled: !!user?.rowId, select: (data) => data?.posts?.groupedAggregates?.map((aggregate) => ({ - name: dayjs(aggregate.keys?.[0]).format("ddd"), + name: dayjs(aggregate.keys?.[0]).utc().format("ddd"), total: Number(aggregate.distinctCount?.rowId), })), }, @@ -57,7 +57,7 @@ const FeedbackOverview = ({ oneWeekAgo }: Props) => { weeklyFeedback?.find((item) => item.name === date)?.total ?? 0; const DATA = Array.from({ length: 7 }).map((_, index) => { - const date = getFormattedDate(index); + const date = getFormattedDate(index + 1); return { name: date, diff --git a/src/components/dashboard/Response/Response.tsx b/src/components/dashboard/Response/Response.tsx index f2ad8cca..925d67f2 100644 --- a/src/components/dashboard/Response/Response.tsx +++ b/src/components/dashboard/Response/Response.tsx @@ -22,7 +22,7 @@ interface Props extends FlexProps { * Recent feedback response. */ const Response = ({ feedback, ...rest }: Props) => { - const date = dayjs(feedback?.createdAt).fromNow(); + const date = dayjs(feedback?.createdAt).utc().fromNow(); return ( diff --git a/src/components/layout/Layout/Layout.tsx b/src/components/layout/Layout/Layout.tsx index 4d451893..2e104da7 100644 --- a/src/components/layout/Layout/Layout.tsx +++ b/src/components/layout/Layout/Layout.tsx @@ -4,6 +4,7 @@ import { Center, Flex, Toaster, css, sigil } from "@omnidev/sigil"; import dayjs from "dayjs"; import duration from "dayjs/plugin/duration"; import relativeTime from "dayjs/plugin/relativeTime"; +import utc from "dayjs/plugin/utc"; import { useIsClient } from "usehooks-ts"; import { Footer, Header } from "components/layout"; @@ -14,6 +15,7 @@ import type { PropsWithChildren } from "react"; dayjs.extend(duration); dayjs.extend(relativeTime); +dayjs.extend(utc); /** * Core application layout. From e0f4c4b05e55d9dfe952c79ffd814d9f9a23cf63 Mon Sep 17 00:00:00 2001 From: hobbescodes <87732294+hobbescodes@users.noreply.github.com> Date: Tue, 29 Apr 2025 10:07:05 -0500 Subject: [PATCH 026/103] chore: remove duration plugin --- src/components/layout/Layout/Layout.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/layout/Layout/Layout.tsx b/src/components/layout/Layout/Layout.tsx index 2e104da7..91de8520 100644 --- a/src/components/layout/Layout/Layout.tsx +++ b/src/components/layout/Layout/Layout.tsx @@ -2,7 +2,6 @@ import { Center, Flex, Toaster, css, sigil } from "@omnidev/sigil"; import dayjs from "dayjs"; -import duration from "dayjs/plugin/duration"; import relativeTime from "dayjs/plugin/relativeTime"; import utc from "dayjs/plugin/utc"; import { useIsClient } from "usehooks-ts"; @@ -13,7 +12,6 @@ import { toaster } from "lib/util"; import type { PropsWithChildren } from "react"; -dayjs.extend(duration); dayjs.extend(relativeTime); dayjs.extend(utc); From ea8a7fd9d1bf377db20d899672f79f69807396a0 Mon Sep 17 00:00:00 2001 From: Beau Hawkinson <72956780+Twonarly1@users.noreply.github.com> Date: Tue, 29 Apr 2025 13:52:48 -0500 Subject: [PATCH 027/103] fix: update manage profile href for both dev and prod environments --- src/app/profile/[userId]/account/page.tsx | 5 +++-- src/lib/config/app.config.ts | 1 - 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/profile/[userId]/account/page.tsx b/src/app/profile/[userId]/account/page.tsx index db3a212e..ae082e59 100644 --- a/src/app/profile/[userId]/account/page.tsx +++ b/src/app/profile/[userId]/account/page.tsx @@ -4,7 +4,7 @@ import { FaRegEdit } from "react-icons/fa"; import { auth } from "auth"; import { Page } from "components/layout"; import { Account } from "components/profile"; -import { app } from "lib/config"; +import { app, AUTH_ISSUER } from "lib/config"; import { getSdk } from "lib/graphql"; export const metadata = { @@ -44,7 +44,8 @@ const ProfileAccountPage = async ({ params }: Props) => { label: app.profileAccountPage.cta.updateProfile.label, // TODO: get Sigil Icon component working and update accordingly. Context: https://github.com/omnidotdev/backfeed-app/pull/44#discussion_r1897974331 icon: , - href: app.organization.identityProviderUrl, + // TODO remove this split once `NEXT_PUBLIC_AUTH_ISSUER` set to base URL (https://linear.app/omnidev/issue/OMNI-254/move-apiauth-paths-to-base-path-or-subpath-eg-auth) + href: AUTH_ISSUER!.split("/api")[0], }, ], }} diff --git a/src/lib/config/app.config.ts b/src/lib/config/app.config.ts index f970afd0..00e3601f 100644 --- a/src/lib/config/app.config.ts +++ b/src/lib/config/app.config.ts @@ -12,7 +12,6 @@ const app = { name: "Omni", supportEmailDisplayName: "Omni Support", supportEmailAddress: "support@omni.dev", - identityProviderUrl: "https://identity.omni.dev", }, breadcrumb: "Home", unsavedChanges: { From a4af5421550154c8ccd7b9da25a218dec46187bb Mon Sep 17 00:00:00 2001 From: hobbescodes <87732294+hobbescodes@users.noreply.github.com> Date: Tue, 29 Apr 2025 17:25:42 -0500 Subject: [PATCH 028/103] refactor: move view all organizations outside of pinned orgs scope --- .../core/CallToAction/CallToAction.tsx | 2 +- .../dashboard/DashboardPage/DashboardPage.tsx | 6 +++- .../PinnedOrganizations.tsx | 29 +------------------ 3 files changed, 7 insertions(+), 30 deletions(-) diff --git a/src/components/core/CallToAction/CallToAction.tsx b/src/components/core/CallToAction/CallToAction.tsx index 8b535b77..7c81d7c1 100644 --- a/src/components/core/CallToAction/CallToAction.tsx +++ b/src/components/core/CallToAction/CallToAction.tsx @@ -13,7 +13,7 @@ export interface ActionButton extends ButtonProps { /** Button label. */ label: string; /** Button icon. */ - icon: ReactNode; + icon?: ReactNode; /** URL path for navigation. */ href?: string; diff --git a/src/components/dashboard/DashboardPage/DashboardPage.tsx b/src/components/dashboard/DashboardPage/DashboardPage.tsx index f20f39dd..8130a797 100644 --- a/src/components/dashboard/DashboardPage/DashboardPage.tsx +++ b/src/components/dashboard/DashboardPage/DashboardPage.tsx @@ -88,12 +88,16 @@ const DashboardPage = ({ isBasicTier, isTeamTier, oneWeekAgo }: Props) => { title: `${app.dashboardPage.welcomeMessage}, ${user?.username}!`, description: app.dashboardPage.description, cta: [ + { + label: app.dashboardPage.cta.viewOrganizations.label, + variant: "outline", + disabled: !numberOfOrganizations, + }, { label: app.dashboardPage.cta.newOrganization.label, // TODO: get Sigil Icon component working and update accordingly. Context: https://github.com/omnidotdev/backfeed-app/pull/44#discussion_r1897974331 icon: , dialogType: DialogType.CreateOrganization, - variant: "outline", disabled: !isBasicTier || (!isTeamTier && !!numberOfOrganizations), }, ], diff --git a/src/components/dashboard/PinnedOrganizations/PinnedOrganizations.tsx b/src/components/dashboard/PinnedOrganizations/PinnedOrganizations.tsx index ef2c2aaf..c204b9fb 100644 --- a/src/components/dashboard/PinnedOrganizations/PinnedOrganizations.tsx +++ b/src/components/dashboard/PinnedOrganizations/PinnedOrganizations.tsx @@ -1,6 +1,6 @@ "use client"; -import { Button, Flex, Grid } from "@omnidev/sigil"; +import { Grid } from "@omnidev/sigil"; import { LuBuilding2, LuCirclePlus } from "react-icons/lu"; import { Link, SkeletonArray } from "components/core"; @@ -53,33 +53,6 @@ const PinnedOrganizations = ({ isBasicTier }: Props) => { description={app.dashboardPage.organizations.description} icon={LuBuilding2} > - - - - - - - - {isError ? ( ) : ( From 62e5a1f2230742515c3e61c3fbf75811b75058a1 Mon Sep 17 00:00:00 2001 From: hobbescodes <87732294+hobbescodes@users.noreply.github.com> Date: Tue, 29 Apr 2025 17:44:41 -0500 Subject: [PATCH 029/103] Refresh Token Rotation (#118) --- next.config.ts | 4 ++++ src/auth.ts | 7 ++----- src/middleware.ts | 6 ++++++ 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/next.config.ts b/next.config.ts index b2baecac..da274ebe 100644 --- a/next.config.ts +++ b/next.config.ts @@ -10,6 +10,10 @@ const corsHeaders = [ // TODO remove this split once `NEXT_PUBLIC_AUTH_ISSUER` set to base URL (https://linear.app/omnidev/issue/OMNI-254/move-apiauth-paths-to-base-path-or-subpath-eg-auth) value: process.env.NEXT_PUBLIC_AUTH_ISSUER!.split("/api")[0], }, + { + key: "Access-Control-Allow-Headers", + value: "Content-Type, Authorization", + }, ]; const nextConfig: NextConfig = { diff --git a/src/auth.ts b/src/auth.ts index 41e1387b..3b2e82a3 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -116,12 +116,9 @@ export const { handlers, auth } = NextAuth({ params: { // explicitly request scopes (otherwise defaults to `openid profile email`) // `offline_access` is required for refresh tokens (https://openid.net/specs/openid-connect-core-1_0.html#offlineaccess) - // scope: "openid profile email offline_access", - // TODO enable above (replace below) for refresh tokens - scope: "openid profile email", + scope: "openid profile email offline_access", // `prompt=consent` parameter is required for refresh token flow - // TODO enable below for refresh tokens - // prompt: "consent", + prompt: "consent", }, }, style: { diff --git a/src/middleware.ts b/src/middleware.ts index daddffac..4e233820 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -61,6 +61,12 @@ const signOut = async (request: NextAuthRequest) => { * Middleware function for handling authentication flows on designated routes. */ export const middleware = auth(async (request) => { + // NB: Used to bypass preflight checks. See: https://github.com/vercel/next.js/discussions/75668 + // TODO: look into the security of this as this is a temporary workaround to allow for sign in / sign up flows to work properly in Safari + if (request.method === "OPTIONS") { + return NextResponse.json({}, { status: 200 }); + } + // If the user is not authenticated, redirect to the landing page if (!request.auth) { return redirect(request); From 0e99cc78c146588a99c5e8a9b305daf4b513ebb3 Mon Sep 17 00:00:00 2001 From: Brian Cooper Date: Tue, 29 Apr 2025 23:55:54 -0500 Subject: [PATCH 030/103] fix(copy): set singular if value is 1, plural otherwise, update casing and punctuation --- .../dashboard/DashboardMetric/DashboardMetric.tsx | 5 +++-- .../dashboard/OrganizationCard/OrganizationCard.tsx | 4 ++-- src/components/feedback/FeedbackCard/FeedbackCard.tsx | 2 +- .../OrganizationListItem/OrganizationListItem.tsx | 2 +- src/components/organization/ProjectCard/ProjectCard.tsx | 9 +++++---- 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/components/dashboard/DashboardMetric/DashboardMetric.tsx b/src/components/dashboard/DashboardMetric/DashboardMetric.tsx index 3c2f1fd8..5a2b312f 100644 --- a/src/components/dashboard/DashboardMetric/DashboardMetric.tsx +++ b/src/components/dashboard/DashboardMetric/DashboardMetric.tsx @@ -6,7 +6,7 @@ import type { IconType } from "react-icons"; interface Props { /** Metric type. */ - type: "Members" | "Projects"; + type: "member" | "project"; /** Metric value. */ value: number | undefined; /** Visual icon. */ @@ -24,7 +24,8 @@ const DashboardMetric = ({ type, value = 0, icon }: Props) => ( {value} - {type} + {/* singular if 1, plural otherwise */} + {value === 1 ? type : `${type}s`} diff --git a/src/components/dashboard/OrganizationCard/OrganizationCard.tsx b/src/components/dashboard/OrganizationCard/OrganizationCard.tsx index 974ee6e4..56d197b2 100644 --- a/src/components/dashboard/OrganizationCard/OrganizationCard.tsx +++ b/src/components/dashboard/OrganizationCard/OrganizationCard.tsx @@ -53,13 +53,13 @@ const OrganizationCard = ({ organization, ...rest }: Props) => ( diff --git a/src/components/feedback/FeedbackCard/FeedbackCard.tsx b/src/components/feedback/FeedbackCard/FeedbackCard.tsx index 86ea6683..81b56da0 100644 --- a/src/components/feedback/FeedbackCard/FeedbackCard.tsx +++ b/src/components/feedback/FeedbackCard/FeedbackCard.tsx @@ -207,7 +207,7 @@ const FeedbackCard = ({ fontSize="sm" color="foreground.subtle" > - {`Updated: ${dayjs(isPending ? new Date() : feedback.statusUpdatedAt).fromNow()}`} + {`Updated ${dayjs(isPending ? new Date() : feedback.statusUpdatedAt).fromNow()}`} diff --git a/src/components/organization/OrganizationListItem/OrganizationListItem.tsx b/src/components/organization/OrganizationListItem/OrganizationListItem.tsx index 068e32c8..49ac2ed8 100644 --- a/src/components/organization/OrganizationListItem/OrganizationListItem.tsx +++ b/src/components/organization/OrganizationListItem/OrganizationListItem.tsx @@ -65,7 +65,7 @@ const OrganizationListItem = ({ organization }: Props) => { {`Updated: ${dayjs(organization.updatedAt).fromNow()}`} + >{`Updated ${dayjs(organization.updatedAt).fromNow()}`} diff --git a/src/components/organization/ProjectCard/ProjectCard.tsx b/src/components/organization/ProjectCard/ProjectCard.tsx index 422e9973..cfb2e38b 100644 --- a/src/components/organization/ProjectCard/ProjectCard.tsx +++ b/src/components/organization/ProjectCard/ProjectCard.tsx @@ -19,7 +19,7 @@ interface ProjectMetric { /** Metric value. */ value: number | undefined; /** Metric type. */ - type: "Responses" | "Users" | "Updated"; + type: "response" | "user"; } interface Props extends FlexProps { @@ -35,12 +35,12 @@ const ProjectCard = ({ project, ...rest }: Props) => { { icon: HiOutlineChatBubbleLeftRight, value: project?.posts?.totalCount, - type: "Responses", + type: "response", }, { icon: HiOutlineUserGroup, value: Number(project?.posts?.aggregates?.distinctCount?.userId), - type: "Users", + type: "user", }, ]; @@ -102,7 +102,8 @@ const ProjectCard = ({ project, ...rest }: Props) => { xl: "inline", }} > - {type} + {/* singular if 1, plural otherwise */} + {value === 1 ? type : `${type}s`} {value ?? 0} From 27d7feff7c9905ef8be409b3ddab19fccae7154d Mon Sep 17 00:00:00 2001 From: Brian Cooper Date: Tue, 29 Apr 2025 23:59:39 -0500 Subject: [PATCH 031/103] chore: reorder actions (settings on bottom) --- .../ManagementNavigation/ManagementNavigation.tsx | 14 +++++++------- .../OrganizationActions/OrganizationActions.tsx | 14 +++++++------- src/lib/config/app.config.ts | 6 +++--- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/components/organization/ManagementNavigation/ManagementNavigation.tsx b/src/components/organization/ManagementNavigation/ManagementNavigation.tsx index f4b05f19..02c56c9e 100644 --- a/src/components/organization/ManagementNavigation/ManagementNavigation.tsx +++ b/src/components/organization/ManagementNavigation/ManagementNavigation.tsx @@ -68,21 +68,21 @@ const ManagementNavigation = ({ }, }, { - label: app.organizationSettingsPage.breadcrumb, - icon: LuSettings, + label: app.organizationInvitationsPage.breadcrumb, + icon: FiUserPlus, onClick: () => { onClose?.(); - router.push(`/organizations/${organizationSlug}/settings`); + router.push(`/organizations/${organizationSlug}/invitations`); }, + disabled: !isAdmin, }, { - label: app.organizationInvitationsPage.breadcrumb, - icon: FiUserPlus, + label: app.organizationSettingsPage.breadcrumb, + icon: LuSettings, onClick: () => { onClose?.(); - router.push(`/organizations/${organizationSlug}/invitations`); + router.push(`/organizations/${organizationSlug}/settings`); }, - disabled: !isAdmin, }, ]; diff --git a/src/components/organization/OrganizationActions/OrganizationActions.tsx b/src/components/organization/OrganizationActions/OrganizationActions.tsx index e8770408..0539b57f 100644 --- a/src/components/organization/OrganizationActions/OrganizationActions.tsx +++ b/src/components/organization/OrganizationActions/OrganizationActions.tsx @@ -44,9 +44,10 @@ const OrganizationActions = ({ const ORGANIZATION_ACTIONS: Action[] = [ { - label: app.organizationPage.actions.cta.settings.label, - icon: LuSettings, - onClick: () => router.push(`/organizations/${organizationSlug}/settings`), + label: app.organizationPage.actions.cta.createProject.label, + icon: LuCirclePlus, + onClick: () => setIsCreateProjectDialogOpen(true), + disabled: !canCreateProjects, }, { label: app.organizationPage.actions.cta.manageTeam.label, @@ -61,10 +62,9 @@ const OrganizationActions = ({ disabled: !hasAdminPrivileges, }, { - label: app.organizationPage.actions.cta.createProject.label, - icon: LuCirclePlus, - onClick: () => setIsCreateProjectDialogOpen(true), - disabled: !canCreateProjects, + label: app.organizationPage.actions.cta.settings.label, + icon: LuSettings, + onClick: () => router.push(`/organizations/${organizationSlug}/settings`), }, ]; diff --git a/src/lib/config/app.config.ts b/src/lib/config/app.config.ts index 00e3601f..e3e1f850 100644 --- a/src/lib/config/app.config.ts +++ b/src/lib/config/app.config.ts @@ -414,12 +414,12 @@ const app = { manageTeam: { label: "Members", }, - settings: { - label: "Settings", - }, invitations: { label: "Invitations", }, + settings: { + label: "Settings", + }, }, }, }, From 0cd395de6416b07433945b3a5e4c4a1e096149d1 Mon Sep 17 00:00:00 2001 From: Brian Cooper Date: Wed, 30 Apr 2025 00:04:05 -0500 Subject: [PATCH 032/103] fix(dashboard-metric): display type from small breakpoint + --- src/components/dashboard/DashboardMetric/DashboardMetric.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/dashboard/DashboardMetric/DashboardMetric.tsx b/src/components/dashboard/DashboardMetric/DashboardMetric.tsx index 5a2b312f..282b74f4 100644 --- a/src/components/dashboard/DashboardMetric/DashboardMetric.tsx +++ b/src/components/dashboard/DashboardMetric/DashboardMetric.tsx @@ -23,7 +23,7 @@ const DashboardMetric = ({ type, value = 0, icon }: Props) => ( {value} - + {/* singular if 1, plural otherwise */} {value === 1 ? type : `${type}s`} From 65d916562d4c7fcd3574a517e98c02c54a14d7f5 Mon Sep 17 00:00:00 2001 From: Brian Cooper Date: Wed, 30 Apr 2025 00:04:59 -0500 Subject: [PATCH 033/103] fix(project-card): display type from small breakpoint + --- src/components/organization/ProjectCard/ProjectCard.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/components/organization/ProjectCard/ProjectCard.tsx b/src/components/organization/ProjectCard/ProjectCard.tsx index cfb2e38b..2b6237b6 100644 --- a/src/components/organization/ProjectCard/ProjectCard.tsx +++ b/src/components/organization/ProjectCard/ProjectCard.tsx @@ -96,12 +96,7 @@ const ProjectCard = ({ project, ...rest }: Props) => { gap={1} direction="row-reverse" > - + {/* singular if 1, plural otherwise */} {value === 1 ? type : `${type}s`} From 08b86f10ab91615fd8e53625dbd0c544ca76f526 Mon Sep 17 00:00:00 2001 From: Brian Cooper Date: Wed, 30 Apr 2025 00:07:23 -0500 Subject: [PATCH 034/103] style(organization-list): display metric type --- .../OrganizationListItem/OrganizationListItem.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/organization/OrganizationListItem/OrganizationListItem.tsx b/src/components/organization/OrganizationListItem/OrganizationListItem.tsx index 49ac2ed8..700b0a63 100644 --- a/src/components/organization/OrganizationListItem/OrganizationListItem.tsx +++ b/src/components/organization/OrganizationListItem/OrganizationListItem.tsx @@ -20,12 +20,12 @@ interface Props { const OrganizationListItem = ({ organization }: Props) => { const AGGREGATES = [ { - type: "Users", + type: "user", icon: HiOutlineUserGroup, value: organization?.members?.totalCount, }, { - type: "Projects", + type: "project", icon: HiOutlineFolder, value: organization?.projects?.totalCount, }, @@ -83,12 +83,14 @@ const OrganizationListItem = ({ organization }: Props) => { {AGGREGATES.map(({ icon, value = 0, type }) => ( + - {value} + {/* singular if 1, plural otherwise */} + {value} {value === 1 ? type : `${type}s`} ))} From f97669a10674450038d40f242867d1925cd686d3 Mon Sep 17 00:00:00 2001 From: Brian Cooper Date: Wed, 30 Apr 2025 00:09:20 -0500 Subject: [PATCH 035/103] style(project-list): display metric type --- src/components/project/ProjectListItem/ProjectListItem.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/project/ProjectListItem/ProjectListItem.tsx b/src/components/project/ProjectListItem/ProjectListItem.tsx index 48d16731..14730f08 100644 --- a/src/components/project/ProjectListItem/ProjectListItem.tsx +++ b/src/components/project/ProjectListItem/ProjectListItem.tsx @@ -31,12 +31,12 @@ const ProjectListItem = ({ const AGGREGATES = [ { - type: "Users", + type: "user", icon: HiOutlineUserGroup, value: posts?.aggregates?.distinctCount?.userId ?? 0, }, { - type: "Responses", + type: "response", icon: HiOutlineChatBubbleLeftRight, value: posts?.totalCount ?? 0, }, @@ -105,7 +105,8 @@ const ProjectListItem = ({ color="foreground.subtle" fontVariant="tabular-nums" > - {value} + {/* singular if 1, plural otherwise */} + {value} {value === 1 ? type : `${type}s`} ))} From 22f9ea0b099d172d60c132866c813f73f3233637 Mon Sep 17 00:00:00 2001 From: Brian Cooper Date: Wed, 30 Apr 2025 00:18:14 -0500 Subject: [PATCH 036/103] refactor: create and extract 'setSingularOrPlural' util --- .../DashboardMetric/DashboardMetric.tsx | 5 +-- .../OrganizationListItem.tsx | 4 +-- .../organization/ProjectCard/ProjectCard.tsx | 10 +++--- .../ProjectListItem/ProjectListItem.tsx | 4 +-- src/lib/util/index.ts | 1 + .../setSingularOrPlural.ts | 31 +++++++++++++++++++ 6 files changed, 45 insertions(+), 10 deletions(-) create mode 100644 src/lib/util/setSingularOrPlural/setSingularOrPlural.ts diff --git a/src/components/dashboard/DashboardMetric/DashboardMetric.tsx b/src/components/dashboard/DashboardMetric/DashboardMetric.tsx index 282b74f4..7719255a 100644 --- a/src/components/dashboard/DashboardMetric/DashboardMetric.tsx +++ b/src/components/dashboard/DashboardMetric/DashboardMetric.tsx @@ -2,6 +2,8 @@ import { Flex, Icon, Text } from "@omnidev/sigil"; +import { setSingularOrPlural } from "lib/util"; + import type { IconType } from "react-icons"; interface Props { @@ -24,8 +26,7 @@ const DashboardMetric = ({ type, value = 0, icon }: Props) => ( {value} - {/* singular if 1, plural otherwise */} - {value === 1 ? type : `${type}s`} + {setSingularOrPlural({ value, label: type })} diff --git a/src/components/organization/OrganizationListItem/OrganizationListItem.tsx b/src/components/organization/OrganizationListItem/OrganizationListItem.tsx index 700b0a63..879c206b 100644 --- a/src/components/organization/OrganizationListItem/OrganizationListItem.tsx +++ b/src/components/organization/OrganizationListItem/OrganizationListItem.tsx @@ -6,6 +6,7 @@ import { HiOutlineFolder, HiOutlineUserGroup } from "react-icons/hi2"; import { LuSettings } from "react-icons/lu"; import { Link, OverflowText } from "components/core"; +import { setSingularOrPlural } from "lib/util"; import type { Organization } from "generated/graphql"; @@ -89,8 +90,7 @@ const OrganizationListItem = ({ organization }: Props) => { color="foreground.subtle" fontVariant="tabular-nums" > - {/* singular if 1, plural otherwise */} - {value} {value === 1 ? type : `${type}s`} + {value} {setSingularOrPlural({ value, label: type })} ))} diff --git a/src/components/organization/ProjectCard/ProjectCard.tsx b/src/components/organization/ProjectCard/ProjectCard.tsx index 2b6237b6..11418801 100644 --- a/src/components/organization/ProjectCard/ProjectCard.tsx +++ b/src/components/organization/ProjectCard/ProjectCard.tsx @@ -8,6 +8,7 @@ import { } from "react-icons/hi2"; import { OverflowText } from "components/core"; +import { setSingularOrPlural } from "lib/util"; import type { FlexProps } from "@omnidev/sigil"; import type { Project } from "generated/graphql"; @@ -96,10 +97,11 @@ const ProjectCard = ({ project, ...rest }: Props) => { gap={1} direction="row-reverse" > - - {/* singular if 1, plural otherwise */} - {value === 1 ? type : `${type}s`} - + {value && ( + + {setSingularOrPlural({ value, label: type })} + + )} {value ?? 0} diff --git a/src/components/project/ProjectListItem/ProjectListItem.tsx b/src/components/project/ProjectListItem/ProjectListItem.tsx index 14730f08..e4b0c8aa 100644 --- a/src/components/project/ProjectListItem/ProjectListItem.tsx +++ b/src/components/project/ProjectListItem/ProjectListItem.tsx @@ -9,6 +9,7 @@ import { LuSettings } from "react-icons/lu"; import { Link, OverflowText } from "components/core"; import { useAuth, useOrganizationMembership } from "lib/hooks"; +import { setSingularOrPlural } from "lib/util"; import type { Project } from "generated/graphql"; @@ -105,8 +106,7 @@ const ProjectListItem = ({ color="foreground.subtle" fontVariant="tabular-nums" > - {/* singular if 1, plural otherwise */} - {value} {value === 1 ? type : `${type}s`} + {value} {setSingularOrPlural({ value: +value, label: type })} ))} diff --git a/src/lib/util/index.ts b/src/lib/util/index.ts index d2d4cd5c..4eeaff53 100644 --- a/src/lib/util/index.ts +++ b/src/lib/util/index.ts @@ -3,5 +3,6 @@ export { default as generateSlug } from "./generateSlug/generateSlug"; export { default as getAuthSession } from "./getAuthSession/getAuthSession"; export { default as getQueryClient } from "./getQueryClient/getQueryClient"; export { default as getSearchParams } from "./getSearchParams/getSearchParams"; +export { default as setSingularOrPlural } from "./setSingularOrPlural/setSingularOrPlural"; export { default as searchParams } from "./searchParams"; export { default as toaster } from "./toaster"; diff --git a/src/lib/util/setSingularOrPlural/setSingularOrPlural.ts b/src/lib/util/setSingularOrPlural/setSingularOrPlural.ts new file mode 100644 index 00000000..22429fc9 --- /dev/null +++ b/src/lib/util/setSingularOrPlural/setSingularOrPlural.ts @@ -0,0 +1,31 @@ +interface Params { + /** Value to be used for determining the label's singular or plural form. */ + value: number; + /** Human-readable label. */ + label: string; + /** Human-readable singular form of the label. */ + singular?: string; + /** Human-readable plural form of the label. */ + plural?: string; +} + +/** + * Set a value's label to its singular version if the value is 1, or to its plural version otherwise. `singular` or `plural` can be passed to override the default singular and plural forms. + * @returns transformed string + * + * @example + * setSingularOrPlural({ value: 1, label: 'item' }); // 'item' + * setSingularOrPlural({ value: 2, label: 'item' }); // 'items' + * setSingularOrPlural({ value: 1, label: 'item', singular: 'one' }); // 'one' + * setSingularOrPlural({ value: 2, label: 'item', plural: 'many' }); // 'many' + */ +const setSingularOrPlural = ({ + value, + label, + singular, + plural, +}: Params): string => { + return value === 1 ? singular || label : plural || `${label}s`; +}; + +export default setSingularOrPlural; From 2dc835be7216696251d18add8e3159f260112937 Mon Sep 17 00:00:00 2001 From: Brian Cooper Date: Wed, 30 Apr 2025 00:23:33 -0500 Subject: [PATCH 037/103] style: wrap containers with singular/plural metrics --- src/components/dashboard/DashboardMetric/DashboardMetric.tsx | 4 ++-- .../OrganizationListItem/OrganizationListItem.tsx | 4 ++-- src/components/organization/ProjectCard/ProjectCard.tsx | 3 ++- src/components/project/ProjectListItem/ProjectListItem.tsx | 4 ++-- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/components/dashboard/DashboardMetric/DashboardMetric.tsx b/src/components/dashboard/DashboardMetric/DashboardMetric.tsx index 7719255a..72411a9d 100644 --- a/src/components/dashboard/DashboardMetric/DashboardMetric.tsx +++ b/src/components/dashboard/DashboardMetric/DashboardMetric.tsx @@ -19,10 +19,10 @@ interface Props { * Dashboard metric. */ const DashboardMetric = ({ type, value = 0, icon }: Props) => ( - + - + {value} diff --git a/src/components/organization/OrganizationListItem/OrganizationListItem.tsx b/src/components/organization/OrganizationListItem/OrganizationListItem.tsx index 879c206b..65abd285 100644 --- a/src/components/organization/OrganizationListItem/OrganizationListItem.tsx +++ b/src/components/organization/OrganizationListItem/OrganizationListItem.tsx @@ -80,9 +80,9 @@ const OrganizationListItem = ({ organization }: Props) => { - + {AGGREGATES.map(({ icon, value = 0, type }) => ( - + { {PROJECT_METRICS.map(({ icon, value, type }) => ( - + { fontSize="sm" gap={1} direction="row-reverse" + wrap="wrap" > {value && ( diff --git a/src/components/project/ProjectListItem/ProjectListItem.tsx b/src/components/project/ProjectListItem/ProjectListItem.tsx index e4b0c8aa..033be672 100644 --- a/src/components/project/ProjectListItem/ProjectListItem.tsx +++ b/src/components/project/ProjectListItem/ProjectListItem.tsx @@ -96,9 +96,9 @@ const ProjectListItem = ({ - + {AGGREGATES.map(({ icon, value, type }) => ( - + Date: Wed, 30 Apr 2025 00:25:29 -0500 Subject: [PATCH 038/103] fix(dashboard): fix 'View All Organizations' CTA not working --- src/components/dashboard/DashboardPage/DashboardPage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/dashboard/DashboardPage/DashboardPage.tsx b/src/components/dashboard/DashboardPage/DashboardPage.tsx index 8130a797..41043fc2 100644 --- a/src/components/dashboard/DashboardPage/DashboardPage.tsx +++ b/src/components/dashboard/DashboardPage/DashboardPage.tsx @@ -91,6 +91,7 @@ const DashboardPage = ({ isBasicTier, isTeamTier, oneWeekAgo }: Props) => { { label: app.dashboardPage.cta.viewOrganizations.label, variant: "outline", + href: "/organizations", disabled: !numberOfOrganizations, }, { From d444dc0ace71fa8984b46bbd623e4971b439f04b Mon Sep 17 00:00:00 2001 From: Beau Hawkinson <72956780+Twonarly1@users.noreply.github.com> Date: Wed, 30 Apr 2025 08:39:24 -0500 Subject: [PATCH 039/103] feat: add an optional tooltip to the emty state button if the action is disabled --- .../PinnedOrganizations.tsx | 8 ++++ .../layout/EmptyState/EmptyState.tsx | 37 +++++++++++++++---- src/lib/config/app.config.ts | 1 + 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/src/components/dashboard/PinnedOrganizations/PinnedOrganizations.tsx b/src/components/dashboard/PinnedOrganizations/PinnedOrganizations.tsx index c204b9fb..1fdd17f5 100644 --- a/src/components/dashboard/PinnedOrganizations/PinnedOrganizations.tsx +++ b/src/components/dashboard/PinnedOrganizations/PinnedOrganizations.tsx @@ -86,6 +86,7 @@ const PinnedOrganizations = ({ isBasicTier }: Props) => { ) : ( { variant: "outline", color: "brand.primary", borderColor: "brand.primary", + bgColor: { + _hover: { base: "brand.primary.50", _dark: "neutral.900" }, + }, onClick: () => setIsCreateOrganizationDialogOpen(true), disabled: !isBasicTier, + _disabled: { + color: "foreground.disabled", + borderColor: "border.disabled", + }, }, }} h={48} diff --git a/src/components/layout/EmptyState/EmptyState.tsx b/src/components/layout/EmptyState/EmptyState.tsx index b5426de3..68285b70 100644 --- a/src/components/layout/EmptyState/EmptyState.tsx +++ b/src/components/layout/EmptyState/EmptyState.tsx @@ -1,6 +1,6 @@ "use client"; -import { Button, Flex, Icon } from "@omnidev/sigil"; +import { Button, Center, Flex, Icon, Tooltip } from "@omnidev/sigil"; import type { ButtonProps, FlexProps } from "@omnidev/sigil"; import type { IconType } from "react-icons"; @@ -17,12 +17,14 @@ interface Props extends FlexProps { /** Action props. */ actionProps?: ButtonProps; }; + /** Optional tooltip for disabled action state. */ + tooltip?: string; } /** * Empty state component. Displays a message and an optional CTA when a successful query has no results. */ -const EmptyState = ({ message, action, ...rest }: Props) => ( +const EmptyState = ({ message, action, tooltip, ...rest }: Props) => ( ( > {message} - {action && ( - - )} + {action.label} + + + } + triggerProps={{ style: { all: "unset" } }} + > + {tooltip} + + ) : ( + + ))} ); diff --git a/src/lib/config/app.config.ts b/src/lib/config/app.config.ts index e3e1f850..1659976c 100644 --- a/src/lib/config/app.config.ts +++ b/src/lib/config/app.config.ts @@ -145,6 +145,7 @@ const app = { description: "Quickly view organizations that you are a member of", emptyState: { message: "No organizations found. Would you like to create one?", + tooltip: "Your current plan doesn’t support this feature.", cta: { label: "Create Organization", }, From cc17e801bdec66bf28511481d8349fa695ee55dc Mon Sep 17 00:00:00 2001 From: hobbescodes <87732294+hobbescodes@users.noreply.github.com> Date: Wed, 30 Apr 2025 09:36:22 -0500 Subject: [PATCH 040/103] refactor(pinned-organizations): update responsive design of org cards --- .../dashboard/DashboardMetric/DashboardMetric.tsx | 12 +++++++----- .../dashboard/OrganizationCard/OrganizationCard.tsx | 2 +- .../PinnedOrganizations/PinnedOrganizations.tsx | 2 -- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/dashboard/DashboardMetric/DashboardMetric.tsx b/src/components/dashboard/DashboardMetric/DashboardMetric.tsx index 72411a9d..83050aaa 100644 --- a/src/components/dashboard/DashboardMetric/DashboardMetric.tsx +++ b/src/components/dashboard/DashboardMetric/DashboardMetric.tsx @@ -19,15 +19,17 @@ interface Props { * Dashboard metric. */ const DashboardMetric = ({ type, value = 0, icon }: Props) => ( - + - + {value} - - {setSingularOrPlural({ value, label: type })} - + {setSingularOrPlural({ value, label: type })} ); diff --git a/src/components/dashboard/OrganizationCard/OrganizationCard.tsx b/src/components/dashboard/OrganizationCard/OrganizationCard.tsx index 56d197b2..c04b1402 100644 --- a/src/components/dashboard/OrganizationCard/OrganizationCard.tsx +++ b/src/components/dashboard/OrganizationCard/OrganizationCard.tsx @@ -24,7 +24,7 @@ const OrganizationCard = ({ organization, ...rest }: Props) => ( direction="column" bgColor="card-item" borderRadius="lg" - p={8} + p={6} {...rest} > - } - triggerProps={{ style: { all: "unset" } }} - > - {tooltip} - - ) : ( - - ))} + {action.label} + + + } + triggerProps={{ style: { all: "unset" } }} + contentProps={{ + display: !action?.disabled || !action.tooltip ? "none" : undefined, + }} + > + {action.tooltip} + + )} ); diff --git a/src/components/organization/OrganizationActions/OrganizationActions.tsx b/src/components/organization/OrganizationActions/OrganizationActions.tsx index 0539b57f..94d0da91 100644 --- a/src/components/organization/OrganizationActions/OrganizationActions.tsx +++ b/src/components/organization/OrganizationActions/OrganizationActions.tsx @@ -74,13 +74,15 @@ const OrganizationActions = ({ description={app.organizationPage.actions.description} > - {ORGANIZATION_ACTIONS.map(({ label, icon, ...rest }) => ( - - ))} + {label} + + ), + )} ); diff --git a/src/components/organization/OrganizationList/OrganizationList.tsx b/src/components/organization/OrganizationList/OrganizationList.tsx index 3b128b36..2a12f86b 100644 --- a/src/components/organization/OrganizationList/OrganizationList.tsx +++ b/src/components/organization/OrganizationList/OrganizationList.tsx @@ -16,10 +16,15 @@ import { DialogType } from "store"; import type { StackProps } from "@omnidev/sigil"; import type { Organization } from "generated/graphql"; +interface Props extends StackProps { + /** Whether the current user can create organizations. */ + canCreateOrganization: boolean; +} + /** * Organization list. */ -const OrganizationList = ({ ...props }: StackProps) => { +const OrganizationList = ({ canCreateOrganization, ...rest }: Props) => { const [{ page, pageSize, search }, setSearchParams] = useSearchParams(); const { setIsOpen: setIsCreateOrganizationDialogOpen } = useDialogStore({ @@ -62,19 +67,16 @@ const OrganizationList = ({ ...props }: StackProps) => { action={{ label: app.organizationsPage.emptyState.cta.label, icon: LuCirclePlus, - actionProps: { - variant: "outline", - color: "brand.primary", - borderColor: "brand.primary", - onClick: () => setIsCreateOrganizationDialogOpen(true), - }, + onClick: () => setIsCreateOrganizationDialogOpen(true), + disabled: !canCreateOrganization, + tooltip: app.organizationsPage.emptyState.tooltip, }} minH={64} /> ); return ( - + {organizations.map((organization) => ( { +const OrganizationProjects = ({ + canCreateProjects, + organizationSlug, +}: Props) => { const { isLoading: isAuthLoading } = useAuth(); const { setIsOpen: setIsCreateProjectDialogOpen } = useDialogStore({ @@ -90,13 +94,9 @@ const OrganizationProjects = ({ organizationSlug }: Props) => { action={{ label: app.organizationPage.projects.emptyState.cta.label, icon: LuCirclePlus, - actionProps: { - variant: "outline", - color: "brand.primary", - borderColor: "brand.primary", - onClick: () => setIsCreateProjectDialogOpen(true), - disabled: isAuthLoading, - }, + onClick: () => setIsCreateProjectDialogOpen(true), + disabled: isAuthLoading || !canCreateProjects, + tooltip: app.organizationPage.projects.emptyState.tooltip, }} h={48} /> diff --git a/src/components/project/ProjectList/ProjectList.tsx b/src/components/project/ProjectList/ProjectList.tsx index 1854dc5c..56c1542b 100644 --- a/src/components/project/ProjectList/ProjectList.tsx +++ b/src/components/project/ProjectList/ProjectList.tsx @@ -74,12 +74,7 @@ const ProjectList = ({ canCreateProjects }: Props) => { ? { label: app.projectsPage.emptyState.cta.label, icon: LuCirclePlus, - actionProps: { - variant: "outline", - color: "brand.primary", - borderColor: "brand.primary", - onClick: () => setIsCreateProjectDialogOpen(true), - }, + onClick: () => setIsCreateProjectDialogOpen(true), } : undefined } diff --git a/src/components/project/ProjectSettings/ProjectSettings.tsx b/src/components/project/ProjectSettings/ProjectSettings.tsx index 7fb9f462..21570eca 100644 --- a/src/components/project/ProjectSettings/ProjectSettings.tsx +++ b/src/components/project/ProjectSettings/ProjectSettings.tsx @@ -56,6 +56,9 @@ const ProjectSettings = ({ excludeRoles: [Role.Member], }), }); + queryClient.invalidateQueries({ + queryKey: ["Projects"], + }); }, }); diff --git a/src/lib/config/app.config.ts b/src/lib/config/app.config.ts index dac3da80..bfb6904f 100644 --- a/src/lib/config/app.config.ts +++ b/src/lib/config/app.config.ts @@ -144,7 +144,7 @@ const app = { description: "Quickly view organizations that you are a member of", emptyState: { message: "No organizations found. Would you like to create one?", - tooltip: "Your current plan doesn’t support this feature.", + tooltip: "Your current plan doesn't support this feature.", cta: { label: "Create Organization", }, @@ -357,6 +357,7 @@ const app = { }, emptyState: { message: "No organizations found. Would you like to create one?", + tooltip: "Your current plan doesn't support this feature.", cta: { label: "Create Organization", }, @@ -383,6 +384,7 @@ const app = { description: "Manage projects across this organization", emptyState: { message: "No projects found. Would you like to create one?", + tooltip: "Your current plan doesn't support this feature.", cta: { label: "Create Project", }, diff --git a/src/lib/flags/enableTeamTierPrivilegesFlag.ts b/src/lib/flags/enableTeamTierPrivilegesFlag.ts index 8b4b1daa..bdb9d3a9 100644 --- a/src/lib/flags/enableTeamTierPrivilegesFlag.ts +++ b/src/lib/flags/enableTeamTierPrivilegesFlag.ts @@ -12,7 +12,7 @@ const enableTeamTierPrivilegesFlag = flag({ identify: dedupeSubscription, decide: ({ entities }) => { // If we are in a development environment, always return true. Comment out this line to test feature flag behaviors in development. - if (isDevEnv) return true; + // if (isDevEnv) return true; if (!entities) return false; From dd27f86c6b31107762abab11a9dc4911d6cf1a40 Mon Sep 17 00:00:00 2001 From: Beau Hawkinson <72956780+Twonarly1@users.noreply.github.com> Date: Wed, 30 Apr 2025 14:54:29 -0500 Subject: [PATCH 044/103] chore: uncomment code for development tier mocking --- src/lib/flags/enableTeamTierPrivilegesFlag.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/flags/enableTeamTierPrivilegesFlag.ts b/src/lib/flags/enableTeamTierPrivilegesFlag.ts index bdb9d3a9..8b4b1daa 100644 --- a/src/lib/flags/enableTeamTierPrivilegesFlag.ts +++ b/src/lib/flags/enableTeamTierPrivilegesFlag.ts @@ -12,7 +12,7 @@ const enableTeamTierPrivilegesFlag = flag({ identify: dedupeSubscription, decide: ({ entities }) => { // If we are in a development environment, always return true. Comment out this line to test feature flag behaviors in development. - // if (isDevEnv) return true; + if (isDevEnv) return true; if (!entities) return false; From 669e0dc856f71f9e872230ad92ad467799c979f8 Mon Sep 17 00:00:00 2001 From: hobbescodes <87732294+hobbescodes@users.noreply.github.com> Date: Wed, 30 Apr 2025 15:32:37 -0500 Subject: [PATCH 045/103] refactor(pricing): update copy, add reference to discount code --- .../pricing/PricingHeader/PricingHeader.tsx | 71 ++++++++++++------- src/lib/config/app.config.ts | 3 +- 2 files changed, 49 insertions(+), 25 deletions(-) diff --git a/src/components/pricing/PricingHeader/PricingHeader.tsx b/src/components/pricing/PricingHeader/PricingHeader.tsx index 1c47624e..9959482a 100644 --- a/src/components/pricing/PricingHeader/PricingHeader.tsx +++ b/src/components/pricing/PricingHeader/PricingHeader.tsx @@ -1,34 +1,57 @@ "use client"; -import { Flex, Text } from "@omnidev/sigil"; +import { Code, Flex, Icon, Text } from "@omnidev/sigil"; +import { TbClipboard } from "react-icons/tb"; +import { useCopyToClipboard } from "usehooks-ts"; import { app } from "lib/config"; +import { toaster } from "lib/util"; /** * Pricing header. */ -const PricingHeader = () => ( - - - {app.pricingPage.pricingHeader.title} - - - - {app.pricingPage.pricingHeader.description} - - -); +const PricingHeader = () => { + const [, copy] = useCopyToClipboard(); + + const handleCopyDiscountCode = async () => { + try { + await copy(app.pricingPage.pricingHeader.discountCode); + + toaster.success({ title: "Copied to clipboard!" }); + } catch (err) { + toaster.error({ title: "Failed to copy" }); + } + }; + + return ( + + + {app.pricingPage.pricingHeader.title} + + + + {app.pricingPage.pricingHeader.description} + + + + {app.pricingPage.pricingHeader.discountCode} + + + + + ); +}; export default PricingHeader; diff --git a/src/lib/config/app.config.ts b/src/lib/config/app.config.ts index bfb6904f..285e14c1 100644 --- a/src/lib/config/app.config.ts +++ b/src/lib/config/app.config.ts @@ -617,7 +617,8 @@ const app = { pricingHeader: { title: "Simple, transparent pricing", description: - "Choose the perfect plan for your business. All plans include a 14-day free trial with no credit card required.", + "Choose the perfect plan for your business. All plans offer a 1-month free trial using our discount code:", + discountCode: "1MONTHFREE", monthly: "Monthly", annual: "Annual", savings: "save 25%", From 1e7766dec07c929b60a96e49578b6558089fb1ac Mon Sep 17 00:00:00 2001 From: hobbescodes <87732294+hobbescodes@users.noreply.github.com> Date: Wed, 30 Apr 2025 16:10:38 -0500 Subject: [PATCH 046/103] refactor(sidebar-navigation): add pricing route if subscription is not found --- src/lib/hooks/useSidebarNavigationItems.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/lib/hooks/useSidebarNavigationItems.tsx b/src/lib/hooks/useSidebarNavigationItems.tsx index 2c0bf109..d4c9373b 100644 --- a/src/lib/hooks/useSidebarNavigationItems.tsx +++ b/src/lib/hooks/useSidebarNavigationItems.tsx @@ -9,6 +9,8 @@ import { useOrganizationQuery, useProjectBySlugQuery } from "generated/graphql"; import { app } from "lib/config"; import { useAuth } from "lib/hooks"; +import { useQuery } from "@tanstack/react-query"; +import { subscriptionOptions } from "lib/options"; import type { IconType } from "react-icons"; interface NavItem { @@ -32,13 +34,19 @@ interface NavItem { * Custom hook to generate sidebar navigation items based on authentication state, current route, and available organization/project data. */ const useSidebarNavigationItems = () => { - const { isAuthenticated } = useAuth(); + const { isAuthenticated, user } = useAuth(); const pathname = usePathname(); const { organizationSlug, projectSlug } = useParams<{ organizationSlug: string; projectSlug: string; }>(); + const { error: subscriptionNotFound } = useQuery( + subscriptionOptions({ + hidraId: user?.hidraId, + }), + ); + const { data: organization } = useOrganizationQuery( { slug: organizationSlug, @@ -64,7 +72,7 @@ const useSidebarNavigationItems = () => { { href: "/pricing", label: app.pricingPage.title, - isVisible: !isAuthenticated, + isVisible: !isAuthenticated || !!subscriptionNotFound, isActive: pathname === "/pricing", }, { @@ -113,6 +121,7 @@ const useSidebarNavigationItems = () => { ], [ isAuthenticated, + subscriptionNotFound, organization, organizationSlug, pathname, From e43f196d6843567bf61d2bbec1e2db2892b0d0bf Mon Sep 17 00:00:00 2001 From: hobbescodes <87732294+hobbescodes@users.noreply.github.com> Date: Wed, 30 Apr 2025 16:11:16 -0500 Subject: [PATCH 047/103] chore: reorganize imports --- src/app/api/invite/route.ts | 2 +- src/app/profile/[userId]/account/page.tsx | 2 +- src/components/profile/Account/Account.tsx | 2 +- src/lib/hooks/useSidebarNavigationItems.tsx | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app/api/invite/route.ts b/src/app/api/invite/route.ts index d5f58059..4c4e5d0d 100644 --- a/src/app/api/invite/route.ts +++ b/src/app/api/invite/route.ts @@ -2,7 +2,7 @@ import { Resend } from "resend"; import { auth } from "auth"; import { InviteMemberEmailTemplate } from "components/organization"; -import { app, FROM_EMAIL_ADDRESS, TO_EMAIL_ADDRESS } from "lib/config"; +import { FROM_EMAIL_ADDRESS, TO_EMAIL_ADDRESS, app } from "lib/config"; import type { OrganizationInvitation } from "components/organization"; import type { NextRequest } from "next/server"; diff --git a/src/app/profile/[userId]/account/page.tsx b/src/app/profile/[userId]/account/page.tsx index ae082e59..2ff39b91 100644 --- a/src/app/profile/[userId]/account/page.tsx +++ b/src/app/profile/[userId]/account/page.tsx @@ -4,7 +4,7 @@ import { FaRegEdit } from "react-icons/fa"; import { auth } from "auth"; import { Page } from "components/layout"; import { Account } from "components/profile"; -import { app, AUTH_ISSUER } from "lib/config"; +import { AUTH_ISSUER, app } from "lib/config"; import { getSdk } from "lib/graphql"; export const metadata = { diff --git a/src/components/profile/Account/Account.tsx b/src/components/profile/Account/Account.tsx index 6c3728a7..8e583421 100644 --- a/src/components/profile/Account/Account.tsx +++ b/src/components/profile/Account/Account.tsx @@ -1,9 +1,9 @@ "use client"; import { Button, Flex, Input, Label, Stack } from "@omnidev/sigil"; +import { app } from "lib/config"; import { useMemo, useState } from "react"; import { IoEyeOffOutline, IoEyeOutline } from "react-icons/io5"; -import { app } from "lib/config"; import type { InputProps } from "@omnidev/sigil"; import type { UserFragment } from "generated/graphql"; diff --git a/src/lib/hooks/useSidebarNavigationItems.tsx b/src/lib/hooks/useSidebarNavigationItems.tsx index d4c9373b..d87d4688 100644 --- a/src/lib/hooks/useSidebarNavigationItems.tsx +++ b/src/lib/hooks/useSidebarNavigationItems.tsx @@ -1,5 +1,6 @@ "use client"; +import { useQuery } from "@tanstack/react-query"; import { useParams, usePathname } from "next/navigation"; import { useMemo } from "react"; import { HiOutlineFolder } from "react-icons/hi2"; @@ -8,9 +9,8 @@ import { LuBuilding2 } from "react-icons/lu"; import { useOrganizationQuery, useProjectBySlugQuery } from "generated/graphql"; import { app } from "lib/config"; import { useAuth } from "lib/hooks"; - -import { useQuery } from "@tanstack/react-query"; import { subscriptionOptions } from "lib/options"; + import type { IconType } from "react-icons"; interface NavItem { From 39ef689b63431d34df60c3b8a03b47836f732940 Mon Sep 17 00:00:00 2001 From: hobbescodes <87732294+hobbescodes@users.noreply.github.com> Date: Wed, 30 Apr 2025 17:59:45 -0500 Subject: [PATCH 048/103] refactor: add mask image to scrollable containers when scrollbar is hidden --- panda.config.ts | 6 ++++++ src/components/feedback/Comments/Comments.tsx | 11 +++++++++-- .../OrganizationProjects/OrganizationProjects.tsx | 5 +---- .../project/ProjectFeedback/ProjectFeedback.tsx | 11 +++++++++-- src/lib/config/app.config.ts | 2 ++ 5 files changed, 27 insertions(+), 8 deletions(-) diff --git a/panda.config.ts b/panda.config.ts index fad0ee81..d35931b2 100644 --- a/panda.config.ts +++ b/panda.config.ts @@ -25,6 +25,12 @@ const pandaConfig = defineConfig({ }, ], }, + globalVars: { + extend: { + "--scrollable-mask": + "linear-gradient(to bottom, rgba(0,0,0,1) 80%, rgba(0,0,0,0))", + }, + }, globalCss: { extend: { html: { diff --git a/src/components/feedback/Comments/Comments.tsx b/src/components/feedback/Comments/Comments.tsx index 8b5b0c9a..8cfd29e9 100644 --- a/src/components/feedback/Comments/Comments.tsx +++ b/src/components/feedback/Comments/Comments.tsx @@ -1,6 +1,6 @@ "use client"; -import { Grid, Stack, VStack } from "@omnidev/sigil"; +import { Grid, Stack, Text, VStack } from "@omnidev/sigil"; import { useMutationState } from "@tanstack/react-query"; import { LuMessageSquare } from "react-icons/lu"; import useInfiniteScroll from "react-infinite-scroll-hook"; @@ -106,6 +106,9 @@ const Comments = ({ organizationId, feedbackId }: Props) => { overflow="auto" p="1px" scrollbar="hidden" + WebkitMaskImage={ + allComments?.length ? "var(--scrollable-mask)" : undefined + } > {isLoading ? ( @@ -130,7 +133,11 @@ const Comments = ({ organizationId, feedbackId }: Props) => { ); })} - {hasNextPage && } + {hasNextPage ? ( + + ) : ( + {app.feedbackPage.comments.endOf} + )} ) : ( { - const { isLoading: isAuthLoading } = useAuth(); - const { setIsOpen: setIsCreateProjectDialogOpen } = useDialogStore({ type: DialogType.CreateProject, }); @@ -95,7 +92,7 @@ const OrganizationProjects = ({ label: app.organizationPage.projects.emptyState.cta.label, icon: LuCirclePlus, onClick: () => setIsCreateProjectDialogOpen(true), - disabled: isAuthLoading || !canCreateProjects, + disabled: !canCreateProjects, tooltip: app.organizationPage.projects.emptyState.tooltip, }} h={48} diff --git a/src/components/project/ProjectFeedback/ProjectFeedback.tsx b/src/components/project/ProjectFeedback/ProjectFeedback.tsx index 6f3f3268..2420d830 100644 --- a/src/components/project/ProjectFeedback/ProjectFeedback.tsx +++ b/src/components/project/ProjectFeedback/ProjectFeedback.tsx @@ -1,6 +1,6 @@ "use client"; -import { Button, Grid, Icon, Stack, VStack } from "@omnidev/sigil"; +import { Button, Grid, Icon, Stack, Text, VStack } from "@omnidev/sigil"; import { useMutationState } from "@tanstack/react-query"; import { useParams, useRouter } from "next/navigation"; import { FiArrowUpRight } from "react-icons/fi"; @@ -129,6 +129,9 @@ const ProjectFeedback = ({ projectId }: Props) => { overflow="auto" p="1px" scrollbar="hidden" + WebkitMaskImage={ + allPosts.length ? "var(--scrollable-mask)" : undefined + } > {isLoading ? ( @@ -176,7 +179,11 @@ const ProjectFeedback = ({ projectId }: Props) => { ); })} - {hasNextPage && } + {hasNextPage ? ( + + ) : ( + {app.projectPage.projectFeedback.endOf} + )} ) : ( Date: Wed, 30 Apr 2025 21:35:54 -0500 Subject: [PATCH 049/103] refactor: remove auth restriction from most pages --- .../[organizationSlug]/(manage)/layout.tsx | 6 -- .../(manage)/members/page.tsx | 45 ++++++---- .../(manage)/settings/page.tsx | 35 ++++---- .../organizations/[organizationSlug]/page.tsx | 14 ++- .../[projectSlug]/[feedbackId]/page.tsx | 89 ++++++++++--------- .../projects/[projectSlug]/page.tsx | 36 +++++--- .../[organizationSlug]/projects/page.tsx | 15 ++-- src/app/organizations/page.tsx | 7 +- .../OrganizationSettings.tsx | 8 +- src/lib/actions/getOrganization.ts | 2 +- src/lib/actions/getProject.ts | 2 +- src/lib/graphql/getSdk.ts | 4 +- src/middleware.ts | 10 +-- 13 files changed, 151 insertions(+), 122 deletions(-) diff --git a/src/app/organizations/[organizationSlug]/(manage)/layout.tsx b/src/app/organizations/[organizationSlug]/(manage)/layout.tsx index 28dde1a6..bdb20031 100644 --- a/src/app/organizations/[organizationSlug]/(manage)/layout.tsx +++ b/src/app/organizations/[organizationSlug]/(manage)/layout.tsx @@ -1,8 +1,6 @@ import { HStack } from "@omnidev/sigil"; import { HydrationBoundary, dehydrate } from "@tanstack/react-query"; -import { notFound } from "next/navigation"; -import { auth } from "auth"; import { ManagementSidebar } from "components/organization"; import { useOrganizationQuery } from "generated/graphql"; import { getQueryClient } from "lib/util"; @@ -19,10 +17,6 @@ interface Props extends PropsWithChildren { const ManageOrganizationLayout = async ({ params, children }: Props) => { const { organizationSlug } = await params; - const session = await auth(); - - if (!session) notFound(); - const queryClient = getQueryClient(); await queryClient.prefetchQuery({ diff --git a/src/app/organizations/[organizationSlug]/(manage)/members/page.tsx b/src/app/organizations/[organizationSlug]/(manage)/members/page.tsx index 8b747400..aa6b340d 100644 --- a/src/app/organizations/[organizationSlug]/(manage)/members/page.tsx +++ b/src/app/organizations/[organizationSlug]/(manage)/members/page.tsx @@ -11,11 +11,7 @@ import { MembershipFilters, Owners, } from "components/organization"; -import { - Role, - useMembersQuery, - useOrganizationRoleQuery, -} from "generated/graphql"; +import { Role, useMembersQuery } from "generated/graphql"; import { getOrganization } from "lib/actions"; import { app } from "lib/config"; import { enableOwnershipTransferFlag } from "lib/flags"; @@ -23,6 +19,7 @@ import { getSdk } from "lib/graphql"; import { getQueryClient, getSearchParams } from "lib/util"; import { DialogType } from "store"; +import type { Member } from "generated/graphql"; import type { SearchParams } from "nuqs/server"; export const generateMetadata = async ({ params }: Props) => { @@ -54,7 +51,7 @@ const OrganizationMembersPage = async ({ params, searchParams }: Props) => { const session = await auth(); - if (!session) notFound(); + // if (!session) notFound(); const organization = await getOrganization({ organizationSlug, @@ -64,12 +61,17 @@ const OrganizationMembersPage = async ({ params, searchParams }: Props) => { const sdk = getSdk({ session }); - const { memberByUserIdAndOrganizationId: member } = - await sdk.OrganizationRole({ - userId: session.user.rowId!, + let member: Partial | null = null; + + if (session) { + const { memberByUserIdAndOrganizationId } = await sdk.OrganizationRole({ + userId: session?.user.rowId!, organizationId: organization.rowId, }); + member = memberByUserIdAndOrganizationId ?? null; + } + const queryClient = getQueryClient(); const { search, roles } = await getSearchParams.parse(searchParams); @@ -99,16 +101,21 @@ const OrganizationMembersPage = async ({ params, searchParams }: Props) => { excludeRoles: [Role.Owner], }), }), - queryClient.prefetchQuery({ - queryKey: useOrganizationRoleQuery.getKey({ - organizationId: organization.rowId, - userId: session.user.rowId!, - }), - queryFn: useOrganizationRoleQuery.fetcher({ - organizationId: organization.rowId, - userId: session.user.rowId!, - }), - }), + // TODO: determine need for prefetching, update client state accordingly + // ...(session + // ? [ + // queryClient.prefetchQuery({ + // queryKey: useOrganizationRoleQuery.getKey({ + // organizationId: organization.rowId, + // userId: session.user.rowId!, + // }), + // queryFn: useOrganizationRoleQuery.fetcher({ + // organizationId: organization.rowId, + // userId: session.user.rowId!, + // }), + // }), + // ] + // : []), ]); return ( diff --git a/src/app/organizations/[organizationSlug]/(manage)/settings/page.tsx b/src/app/organizations/[organizationSlug]/(manage)/settings/page.tsx index ecf35f7c..f3873b34 100644 --- a/src/app/organizations/[organizationSlug]/(manage)/settings/page.tsx +++ b/src/app/organizations/[organizationSlug]/(manage)/settings/page.tsx @@ -4,11 +4,7 @@ import { notFound } from "next/navigation"; import { auth } from "auth"; import { Page } from "components/layout"; import { OrganizationSettings } from "components/organization"; -import { - Role, - useMembersQuery, - useOrganizationRoleQuery, -} from "generated/graphql"; +import { Role, useMembersQuery } from "generated/graphql"; import { getOrganization } from "lib/actions"; import { app } from "lib/config"; import { @@ -48,7 +44,7 @@ const OrganizationSettingsPage = async ({ params }: Props) => { const session = await auth(); - if (!session) notFound(); + // if (!session) notFound(); const organization = await getOrganization({ organizationSlug }); @@ -57,16 +53,21 @@ const OrganizationSettingsPage = async ({ params }: Props) => { const queryClient = getQueryClient(); await Promise.all([ - queryClient.prefetchQuery({ - queryKey: useOrganizationRoleQuery.getKey({ - userId: session.user.rowId!, - organizationId: organization.rowId, - }), - queryFn: useOrganizationRoleQuery.fetcher({ - userId: session.user.rowId!, - organizationId: organization.rowId, - }), - }), + // TODO: determine need for prefetching, update client state accordingly + // ...(session + // ? [ + // queryClient.prefetchQuery({ + // queryKey: useOrganizationRoleQuery.getKey({ + // userId: session.user.rowId!, + // organizationId: organization.rowId, + // }), + // queryFn: useOrganizationRoleQuery.fetcher({ + // userId: session.user.rowId!, + // organizationId: organization.rowId, + // }), + // }), + // ] + // : []), queryClient.prefetchQuery({ queryKey: useMembersQuery.getKey({ organizationId: organization.rowId, @@ -88,7 +89,7 @@ const OrganizationSettingsPage = async ({ params }: Props) => { pt={0} > { const { organizationSlug } = await params; @@ -53,7 +54,7 @@ const OrganizationPage = async ({ params }: Props) => { const session = await auth(); - if (!session) notFound(); + // if (!session) notFound(); const [organization, isBasicTier, isTeamTier] = await Promise.all([ getOrganization({ organizationSlug }), @@ -65,12 +66,17 @@ const OrganizationPage = async ({ params }: Props) => { const sdk = getSdk({ session }); - const { memberByUserIdAndOrganizationId: member } = - await sdk.OrganizationRole({ - userId: session.user.rowId!, + let member: Partial | null = null; + + if (session) { + const { memberByUserIdAndOrganizationId } = await sdk.OrganizationRole({ + userId: session?.user.rowId!, organizationId: organization.rowId, }); + member = memberByUserIdAndOrganizationId ?? null; + } + const hasAdminPrivileges = member?.role === Role.Admin || member?.role === Role.Owner; diff --git a/src/app/organizations/[organizationSlug]/projects/[projectSlug]/[feedbackId]/page.tsx b/src/app/organizations/[organizationSlug]/projects/[projectSlug]/[feedbackId]/page.tsx index 12aee1a3..da58c78b 100644 --- a/src/app/organizations/[organizationSlug]/projects/[projectSlug]/[feedbackId]/page.tsx +++ b/src/app/organizations/[organizationSlug]/projects/[projectSlug]/[feedbackId]/page.tsx @@ -7,18 +7,16 @@ import { Page } from "components/layout"; import { Role, useCommentsQuery, - useDownvoteQuery, useFeedbackByIdQuery, useInfiniteCommentsQuery, - useOrganizationRoleQuery, useProjectStatusesQuery, - useUpvoteQuery, } from "generated/graphql"; import { app } from "lib/config"; import { getSdk } from "lib/graphql"; import { getQueryClient } from "lib/util"; import type { BreadcrumbRecord } from "components/core"; +import type { Member } from "generated/graphql"; export const metadata = { title: app.feedbackPage.breadcrumb, @@ -41,7 +39,7 @@ const FeedbackPage = async ({ params }: Props) => { const session = await auth(); - if (!session) notFound(); + // if (!session) notFound(); const sdk = getSdk({ session }); @@ -49,14 +47,18 @@ const FeedbackPage = async ({ params }: Props) => { if (!feedback) notFound(); - const { memberByUserIdAndOrganizationId } = await sdk.OrganizationRole({ - userId: session.user?.rowId!, - organizationId: feedback.project?.organization?.rowId!, - }); + let member: Partial | null = null; - const isAdmin = - memberByUserIdAndOrganizationId?.role === Role.Admin || - memberByUserIdAndOrganizationId?.role === Role.Owner; + if (session) { + const { memberByUserIdAndOrganizationId } = await sdk.OrganizationRole({ + userId: session?.user?.rowId!, + organizationId: feedback.project?.organization?.rowId!, + }); + + member = memberByUserIdAndOrganizationId ?? null; + } + + const isAdmin = member?.role === Role.Admin || member?.role === Role.Owner; const queryClient = getQueryClient(); @@ -100,36 +102,41 @@ const FeedbackPage = async ({ params }: Props) => { }), ] : []), - queryClient.prefetchQuery({ - queryKey: useOrganizationRoleQuery.getKey({ - userId: session.user.rowId!, - organizationId: feedback.project?.organization?.rowId!, - }), - queryFn: useOrganizationRoleQuery.fetcher({ - userId: session.user.rowId!, - organizationId: feedback.project?.organization?.rowId!, - }), - }), - queryClient.prefetchQuery({ - queryKey: useDownvoteQuery.getKey({ - userId: session?.user?.rowId!, - feedbackId, - }), - queryFn: useDownvoteQuery.fetcher({ - userId: session?.user?.rowId!, - feedbackId, - }), - }), - queryClient.prefetchQuery({ - queryKey: useUpvoteQuery.getKey({ - userId: session?.user?.rowId!, - feedbackId, - }), - queryFn: useUpvoteQuery.fetcher({ - userId: session?.user?.rowId!, - feedbackId, - }), - }), + // TODO: determine need for prefetching, update client state accordingly + // ...(session + // ? [ + // queryClient.prefetchQuery({ + // queryKey: useOrganizationRoleQuery.getKey({ + // userId: session.user.rowId!, + // organizationId: feedback.project?.organization?.rowId!, + // }), + // queryFn: useOrganizationRoleQuery.fetcher({ + // userId: session.user.rowId!, + // organizationId: feedback.project?.organization?.rowId!, + // }), + // }), + // queryClient.prefetchQuery({ + // queryKey: useDownvoteQuery.getKey({ + // userId: session?.user?.rowId!, + // feedbackId, + // }), + // queryFn: useDownvoteQuery.fetcher({ + // userId: session?.user?.rowId!, + // feedbackId, + // }), + // }), + // queryClient.prefetchQuery({ + // queryKey: useUpvoteQuery.getKey({ + // userId: session?.user?.rowId!, + // feedbackId, + // }), + // queryFn: useUpvoteQuery.fetcher({ + // userId: session?.user?.rowId!, + // feedbackId, + // }), + // }), + // ] + // : []), queryClient.prefetchInfiniteQuery({ queryKey: useInfiniteCommentsQuery.getKey({ pageSize: 5, feedbackId }), queryFn: useCommentsQuery.fetcher({ pageSize: 5, feedbackId }), diff --git a/src/app/organizations/[organizationSlug]/projects/[projectSlug]/page.tsx b/src/app/organizations/[organizationSlug]/projects/[projectSlug]/page.tsx index d2fd55f5..c9beebbe 100644 --- a/src/app/organizations/[organizationSlug]/projects/[projectSlug]/page.tsx +++ b/src/app/organizations/[organizationSlug]/projects/[projectSlug]/page.tsx @@ -7,6 +7,7 @@ import { auth } from "auth"; import { Page } from "components/layout"; import { ProjectOverview } from "components/project"; import { + type Member, Role, useInfinitePostsQuery, usePostsQuery, @@ -48,7 +49,7 @@ const ProjectPage = async ({ params }: Props) => { const session = await auth(); - if (!session) notFound(); + // if (!session) notFound(); const project = await getProject({ organizationSlug, projectSlug }); @@ -56,10 +57,16 @@ const ProjectPage = async ({ params }: Props) => { const sdk = getSdk({ session }); - const { memberByUserIdAndOrganizationId } = await sdk.OrganizationRole({ - userId: session.user?.rowId!, - organizationId: project.organization?.rowId!, - }); + let member: Partial | null = null; + + if (session) { + const { memberByUserIdAndOrganizationId } = await sdk.OrganizationRole({ + userId: session?.user.rowId!, + organizationId: project.organization?.rowId!, + }); + + member = memberByUserIdAndOrganizationId ?? null; + } const queryClient = getQueryClient(); @@ -125,15 +132,16 @@ const ProjectPage = async ({ params }: Props) => { title: project.name!, description: project.description!, cta: [ - { - label: app.projectPage.header.cta.settings.label, - // TODO: get Sigil Icon component working and update accordingly. Context: https://github.com/omnidotdev/backfeed-app/pull/44#discussion_r1897974331 - icon: , - disabled: - !memberByUserIdAndOrganizationId || - memberByUserIdAndOrganizationId.role === Role.Member, - href: `/organizations/${organizationSlug}/projects/${projectSlug}/settings`, - }, + ...(member && member.role !== Role.Member + ? [ + { + label: app.projectPage.header.cta.settings.label, + // TODO: get Sigil Icon component working and update accordingly. Context: https://github.com/omnidotdev/backfeed-app/pull/44#discussion_r1897974331 + icon: , + href: `/organizations/${organizationSlug}/projects/${projectSlug}/settings`, + }, + ] + : []), { label: app.projectPage.header.cta.viewAllProjects.label, // TODO: get Sigil Icon component working and update accordingly. Context: https://github.com/omnidotdev/backfeed-app/pull/44#discussion_r1897974331 diff --git a/src/app/organizations/[organizationSlug]/projects/page.tsx b/src/app/organizations/[organizationSlug]/projects/page.tsx index b2ba6f16..51af2d2d 100644 --- a/src/app/organizations/[organizationSlug]/projects/page.tsx +++ b/src/app/organizations/[organizationSlug]/projects/page.tsx @@ -18,7 +18,7 @@ import { getQueryClient, getSearchParams } from "lib/util"; import { DialogType } from "store"; import type { BreadcrumbRecord } from "components/core"; -import type { ProjectsQueryVariables } from "generated/graphql"; +import type { Member, ProjectsQueryVariables } from "generated/graphql"; import type { SearchParams } from "nuqs/server"; export const generateMetadata = async ({ params }: Props) => { @@ -48,7 +48,7 @@ const ProjectsPage = async ({ params, searchParams }: Props) => { const session = await auth(); - if (!session) notFound(); + // if (!session) notFound(); const [organization, isBasicTier, isTeamTier] = await Promise.all([ getOrganization({ organizationSlug }), @@ -60,12 +60,17 @@ const ProjectsPage = async ({ params, searchParams }: Props) => { const sdk = getSdk({ session }); - const { memberByUserIdAndOrganizationId: member } = - await sdk.OrganizationRole({ - userId: session.user.rowId!, + let member: Partial | null = null; + + if (session) { + const { memberByUserIdAndOrganizationId } = await sdk.OrganizationRole({ + userId: session?.user.rowId!, organizationId: organization.rowId, }); + member = memberByUserIdAndOrganizationId ?? null; + } + const hasAdminPrivileges = member?.role === Role.Admin || member?.role === Role.Owner; diff --git a/src/app/organizations/page.tsx b/src/app/organizations/page.tsx index d1f7716b..728efd72 100644 --- a/src/app/organizations/page.tsx +++ b/src/app/organizations/page.tsx @@ -1,5 +1,4 @@ import { HydrationBoundary, dehydrate } from "@tanstack/react-query"; -import { notFound } from "next/navigation"; import { LuCirclePlus } from "react-icons/lu"; import { auth } from "auth"; @@ -44,15 +43,17 @@ interface Props { const OrganizationsPage = async ({ searchParams }: Props) => { const session = await auth(); - if (!session) notFound(); + // if (!session) notFound(); const sdk = getSdk({ session }); const [{ organizations }, isBasicTier, isTeamTier] = await Promise.all([ sdk.Organizations({ - userId: session?.user.rowId!, + userId: session?.user.rowId, isMember: true, excludeRoles: [Role.Member], + // NB: only need to determine in there are any number of orgs given the other variables. + pageSize: 1, }), enableBasicTierPrivilegesFlag(), enableTeamTierPrivilegesFlag(), diff --git a/src/components/organization/OrganizationSettings/OrganizationSettings.tsx b/src/components/organization/OrganizationSettings/OrganizationSettings.tsx index 53a06401..b93b53ec 100644 --- a/src/components/organization/OrganizationSettings/OrganizationSettings.tsx +++ b/src/components/organization/OrganizationSettings/OrganizationSettings.tsx @@ -37,7 +37,7 @@ const joinOrganizationDetails = interface Props { /** User ID. */ - userId: User["rowId"]; + userId: User["rowId"] | undefined; /** Organization ID. */ organizationId: Organization["rowId"]; /** Whether the join organization functionality is enabled. */ @@ -93,7 +93,7 @@ const OrganizationSettings = ({ const onSettled = () => queryClient.invalidateQueries({ queryKey: useOrganizationRoleQuery.getKey({ - userId, + userId: userId!, organizationId, }), }); @@ -186,7 +186,7 @@ const OrganizationSettings = ({ {/* NB: if the user is not currently a member, the only action that would be available is to join the organization, which we are currently putting behind a feature flag. */} - {(isCurrentMember || isJoinOrganizationEnabled) && ( + {(isCurrentMember || isJoinOrganizationEnabled) && userId && ( )} - {!isCurrentMember && ( + {!isCurrentMember && userId && ( + {label} + + + } + triggerProps={{ + style: { all: "unset" }, + }} + contentProps={{ + display: !disabled || !tooltip ? "none" : undefined, + zIndex: "foreground", + fontSize: "sm", + }} + > + {action.tooltip} + ); }; diff --git a/src/components/dashboard/DashboardPage/DashboardPage.test.tsx b/src/components/dashboard/DashboardPage/DashboardPage.test.tsx index 17fb7a46..71b8ac24 100644 --- a/src/components/dashboard/DashboardPage/DashboardPage.test.tsx +++ b/src/components/dashboard/DashboardPage/DashboardPage.test.tsx @@ -1,13 +1,17 @@ import { beforeEach, describe, expect, it } from "bun:test"; import { DashboardPage } from "components/dashboard"; +import dayjs from "dayjs"; import { app } from "lib/config"; import { render } from "test/unit/util"; +// Not sure if this is the best way to do this, but it works after stumbling across this file. +const oneWeekAgo = dayjs().utc().subtract(6, "days").startOf("day").toDate(); + describe("dashboard page", () => { beforeEach(() => { // TODO: add tests for different tier level renders (i.e. disabled create org button, etc) - render(); + render(); }); // TODO enable below, blocked by MSW integration (see test setup file for corresponding TODO) which is further blocked by https://github.com/oven-sh/bun/issues/13072 diff --git a/src/components/dashboard/DashboardPage/DashboardPage.tsx b/src/components/dashboard/DashboardPage/DashboardPage.tsx index 41043fc2..4b836933 100644 --- a/src/components/dashboard/DashboardPage/DashboardPage.tsx +++ b/src/components/dashboard/DashboardPage/DashboardPage.tsx @@ -92,7 +92,6 @@ const DashboardPage = ({ isBasicTier, isTeamTier, oneWeekAgo }: Props) => { label: app.dashboardPage.cta.viewOrganizations.label, variant: "outline", href: "/organizations", - disabled: !numberOfOrganizations, }, { label: app.dashboardPage.cta.newOrganization.label, @@ -100,6 +99,9 @@ const DashboardPage = ({ isBasicTier, isTeamTier, oneWeekAgo }: Props) => { icon: , dialogType: DialogType.CreateOrganization, disabled: !isBasicTier || (!isTeamTier && !!numberOfOrganizations), + tooltip: isBasicTier + ? app.dashboardPage.cta.newOrganization.basicTierTooltip + : app.dashboardPage.cta.newOrganization.noSubscriptionTooltip, }, ], }} diff --git a/src/components/dashboard/PinnedOrganizations/PinnedOrganizations.tsx b/src/components/dashboard/PinnedOrganizations/PinnedOrganizations.tsx index 2833120b..f4f661d4 100644 --- a/src/components/dashboard/PinnedOrganizations/PinnedOrganizations.tsx +++ b/src/components/dashboard/PinnedOrganizations/PinnedOrganizations.tsx @@ -89,7 +89,10 @@ const PinnedOrganizations = ({ isBasicTier }: Props) => { onClick: () => setIsCreateOrganizationDialogOpen(true), icon: LuCirclePlus, disabled: !isBasicTier, - tooltip: app.dashboardPage.organizations.emptyState.tooltip, + tooltip: isBasicTier + ? app.dashboardPage.organizations.emptyState.basicTierTooltip + : app.dashboardPage.organizations.emptyState + .noSubscriptionTooltip, }} h={48} /> diff --git a/src/components/landing/Hero/Hero.tsx b/src/components/landing/Hero/Hero.tsx index 1bda9cd2..b784bc2f 100644 --- a/src/components/landing/Hero/Hero.tsx +++ b/src/components/landing/Hero/Hero.tsx @@ -58,6 +58,7 @@ const Hero = () => { priority width={isMediumViewport ? 224 : 150} height={isMediumViewport ? 323 : 216} + draggable={false} /> ( triggerProps={{ style: { all: "unset" } }} contentProps={{ display: !action?.disabled || !action.tooltip ? "none" : undefined, + fontSize: "sm", }} > {action.tooltip} diff --git a/src/components/layout/Header/Header.tsx b/src/components/layout/Header/Header.tsx index 11121879..f4fa8c88 100644 --- a/src/components/layout/Header/Header.tsx +++ b/src/components/layout/Header/Header.tsx @@ -68,15 +68,15 @@ const Header = () => { )} - Docs - + {app.header.routes.docs.label} + diff --git a/src/components/organization/OrganizationList/OrganizationList.tsx b/src/components/organization/OrganizationList/OrganizationList.tsx index 2a12f86b..928bbad9 100644 --- a/src/components/organization/OrganizationList/OrganizationList.tsx +++ b/src/components/organization/OrganizationList/OrganizationList.tsx @@ -19,12 +19,18 @@ import type { Organization } from "generated/graphql"; interface Props extends StackProps { /** Whether the current user can create organizations. */ canCreateOrganization: boolean; + /** Whether the current user is on a basic subscription tier. */ + isBasicTier?: boolean; } /** * Organization list. */ -const OrganizationList = ({ canCreateOrganization, ...rest }: Props) => { +const OrganizationList = ({ + canCreateOrganization, + isBasicTier, + ...rest +}: Props) => { const [{ page, pageSize, search }, setSearchParams] = useSearchParams(); const { setIsOpen: setIsCreateOrganizationDialogOpen } = useDialogStore({ @@ -69,7 +75,9 @@ const OrganizationList = ({ canCreateOrganization, ...rest }: Props) => { icon: LuCirclePlus, onClick: () => setIsCreateOrganizationDialogOpen(true), disabled: !canCreateOrganization, - tooltip: app.organizationsPage.emptyState.tooltip, + tooltip: isBasicTier + ? app.organizationsPage.emptyState.basicTierTooltip + : app.organizationsPage.emptyState.noSubscriptionTooltip, }} minH={64} /> diff --git a/src/components/organization/OrganizationActions/OrganizationActions.tsx b/src/components/organization/OrganizationManagement/OrganizationManagement.tsx similarity index 61% rename from src/components/organization/OrganizationActions/OrganizationActions.tsx rename to src/components/organization/OrganizationManagement/OrganizationManagement.tsx index 94d0da91..e4b962a5 100644 --- a/src/components/organization/OrganizationActions/OrganizationActions.tsx +++ b/src/components/organization/OrganizationManagement/OrganizationManagement.tsx @@ -4,12 +4,10 @@ import { Button, Grid, Icon } from "@omnidev/sigil"; import { useParams, useRouter } from "next/navigation"; import { FiUserPlus } from "react-icons/fi"; import { HiOutlineUserGroup } from "react-icons/hi2"; -import { LuCirclePlus, LuSettings } from "react-icons/lu"; +import { LuSettings } from "react-icons/lu"; import { SectionContainer } from "components/layout"; import { app } from "lib/config"; -import { useDialogStore } from "lib/hooks/store"; -import { DialogType } from "store"; import type { ButtonProps } from "@omnidev/sigil"; import type { IconType } from "react-icons"; @@ -24,45 +22,30 @@ interface Action extends ButtonProps { interface Props { /** Whether the user has admin privileges for the organization. */ hasAdminPrivileges: boolean; - /** Whether the user has necessary subscription permissions to create projects. */ - canCreateProjects: boolean; } /** - * Organization actions. + * Organization management. */ -const OrganizationActions = ({ - hasAdminPrivileges, - canCreateProjects, -}: Props) => { +const OrganizationManagement = ({ hasAdminPrivileges }: Props) => { const { organizationSlug } = useParams<{ organizationSlug: string }>(); const router = useRouter(); - const { setIsOpen: setIsCreateProjectDialogOpen } = useDialogStore({ - type: DialogType.CreateProject, - }); - const ORGANIZATION_ACTIONS: Action[] = [ { - label: app.organizationPage.actions.cta.createProject.label, - icon: LuCirclePlus, - onClick: () => setIsCreateProjectDialogOpen(true), - disabled: !canCreateProjects, - }, - { - label: app.organizationPage.actions.cta.manageTeam.label, + label: app.organizationPage.management.cta.manageTeam.label, icon: HiOutlineUserGroup, onClick: () => router.push(`/organizations/${organizationSlug}/members`), }, { - label: app.organizationPage.actions.cta.invitations.label, + label: app.organizationPage.management.cta.invitations.label, icon: FiUserPlus, onClick: () => router.push(`/organizations/${organizationSlug}/invitations`), disabled: !hasAdminPrivileges, }, { - label: app.organizationPage.actions.cta.settings.label, + label: app.organizationPage.management.cta.settings.label, icon: LuSettings, onClick: () => router.push(`/organizations/${organizationSlug}/settings`), }, @@ -70,8 +53,8 @@ const OrganizationActions = ({ return ( {ORGANIZATION_ACTIONS.filter(({ disabled }) => !disabled).map( @@ -88,4 +71,4 @@ const OrganizationActions = ({ ); }; -export default OrganizationActions; +export default OrganizationManagement; diff --git a/src/components/organization/OrganizationProjects/OrganizationProjects.tsx b/src/components/organization/OrganizationProjects/OrganizationProjects.tsx index f4828d1e..207e2d3e 100644 --- a/src/components/organization/OrganizationProjects/OrganizationProjects.tsx +++ b/src/components/organization/OrganizationProjects/OrganizationProjects.tsx @@ -15,6 +15,11 @@ import { DialogType } from "store"; import type { Organization, Project } from "generated/graphql"; interface Props { + /** Whether the user has admin privileges for the organization. */ + hasAdminPrivileges: boolean; + /** Whether the user has basic tier subscription permissions. */ + isBasicTier: boolean; + /** Whether the user has necessary subscription permissions to create projects. */ canCreateProjects: boolean; /** Whether the user has necessary subscription permissions to create projects. */ organizationSlug: Organization["slug"]; @@ -24,6 +29,8 @@ interface Props { * Organization projects overview. */ const OrganizationProjects = ({ + hasAdminPrivileges, + isBasicTier, canCreateProjects, organizationSlug, }: Props) => { @@ -87,14 +94,28 @@ const OrganizationProjects = ({ )) ) : ( setIsCreateProjectDialogOpen(true), - disabled: !canCreateProjects, - tooltip: app.organizationPage.projects.emptyState.tooltip, - }} + message={ + hasAdminPrivileges + ? app.organizationPage.projects.emptyState + .organizationOwnerMessage + : app.organizationPage.projects.emptyState + .organizationUserMessage + } + action={ + hasAdminPrivileges + ? { + label: app.organizationPage.projects.emptyState.cta.label, + icon: LuCirclePlus, + onClick: () => setIsCreateProjectDialogOpen(true), + disabled: !canCreateProjects, + tooltip: isBasicTier + ? app.organizationPage.projects.emptyState + .basicTierTooltip + : app.organizationPage.projects.emptyState + .noSubscriptionTooltip, + } + : undefined + } h={48} /> )} diff --git a/src/components/organization/index.ts b/src/components/organization/index.ts index 48216b11..1c2ec839 100644 --- a/src/components/organization/index.ts +++ b/src/components/organization/index.ts @@ -12,7 +12,7 @@ export { default as ManagementSidebar } from "./ManagementSidebar/ManagementSide export { default as Members } from "./Members/Members"; export { default as MembershipFilters } from "./MembershipFilters/MembershipFilters"; export { default as MembershipMenu } from "./MembershipMenu/MembershipMenu"; -export { default as OrganizationActions } from "./OrganizationActions/OrganizationActions"; +export { default as OrganizationManagement } from "./OrganizationManagement/OrganizationManagement"; export { default as OrganizationFilters } from "./OrganizationFilters/OrganizationFilters"; export { default as OrganizationList } from "./OrganizationList/OrganizationList"; export { default as OrganizationListItem } from "./OrganizationListItem/OrganizationListItem"; diff --git a/src/components/project/ProjectList/ProjectList.tsx b/src/components/project/ProjectList/ProjectList.tsx index 56c1542b..80fd0247 100644 --- a/src/components/project/ProjectList/ProjectList.tsx +++ b/src/components/project/ProjectList/ProjectList.tsx @@ -17,7 +17,7 @@ import { DialogType } from "store"; import type { Project } from "generated/graphql"; interface Props { - /** Whether the current user can create projects. */ + /** Whether the user has necessary subscription permissions to create projects. */ canCreateProjects: boolean; } diff --git a/src/lib/config/app.config.ts b/src/lib/config/app.config.ts index 2c019b0b..43f5c638 100644 --- a/src/lib/config/app.config.ts +++ b/src/lib/config/app.config.ts @@ -86,14 +86,17 @@ const app = { pricing: { label: "Pricing", }, + docs: { + label: "Docs", + }, }, }, landingPage: { hero: { - imageAlt: "Hero", title: "Transform User Feedback into Actionable Insights", description: "Collect, analyze, and act on user feedback with our powerful platform. Make data-driven decisions and improve your product faster than ever.", + imageAlt: "Hero", cta: { collect: { label: { @@ -145,7 +148,10 @@ const app = { description: "Quickly view organizations that you are a member of", emptyState: { message: "No organizations found. Would you like to create one?", - tooltip: "Your current plan doesn't support this feature.", + basicTierTooltip: + "Your plan only allows you to create 1 organization. Upgrade to the Team plan to create unlimited organizations.", + noSubscriptionTooltip: + "Upgrade to a paid plan to create an organization.", cta: { label: "Create Organization", }, @@ -168,6 +174,9 @@ const app = { }, }, cta: { + viewOrganizations: { + label: "View All Organizations", + }, newOrganization: { action: { submit: "Create Organization", @@ -182,6 +191,10 @@ const app = { }, }, label: "New Organization", + basicTierTooltip: + "Your plan only allows you to create 1 organization. Upgrade to the Team plan to create unlimited organizations.", + noSubscriptionTooltip: + "Upgrade to a paid plan to create an organization.", description: "Create a new organization", organizationName: { id: "Organization Name", @@ -203,6 +216,7 @@ const app = { }, }, }, + // TODO: This is not being used on the dashboard page currently. Move to another page location. newProject: { action: { submit: "Create Project", @@ -262,9 +276,6 @@ const app = { }, }, }, - viewOrganizations: { - label: "View All Organizations", - }, }, }, profileAccountPage: { @@ -353,12 +364,19 @@ const app = { cta: { newOrganization: { label: "New Organization", + basicTierTooltip: + "Your plan only allows you to create 1 organization. Upgrade to the Team plan to create unlimited organizations.", + noSubscriptionTooltip: + "Upgrade to a paid plan to create an organization.", }, }, }, emptyState: { message: "No organizations found. Would you like to create one?", - tooltip: "Your current plan doesn't support this feature.", + basicTierTooltip: + "Your plan only allows you to create 1 organization. Upgrade to the Team plan to create unlimited organizations.", + noSubscriptionTooltip: + "Upgrade to a paid plan to create an organization.", cta: { label: "Create Organization", }, @@ -372,11 +390,14 @@ const app = { organizationPage: { header: { cta: { - viewAllProjects: { + viewProjects: { label: "View All Projects", }, newProject: { label: "New Project", + basicTierTooltip: + "Your plan only allows you to create 3 projects. Upgrade to the Team plan to create unlimited projects.", + noSubscriptionTooltip: "Upgrade to a paid plan to create a project.", }, }, }, @@ -384,8 +405,12 @@ const app = { title: "Projects", description: "Manage projects across this organization", emptyState: { - message: "No projects found. Would you like to create one?", - tooltip: "Your current plan doesn't support this feature.", + organizationOwnerMessage: + "No projects found. Would you like to create one?", + organizationUserMessage: "No projects found.", + basicTierTooltip: + "Your plan only allows you to create 3 projects. Upgrade to the Team plan to create unlimited projects.", + noSubscriptionTooltip: "Upgrade to a paid plan to create a project.", cta: { label: "Create Project", }, @@ -407,13 +432,10 @@ const app = { }, }, }, - actions: { - title: "Quick Actions", - description: "Common organization details and actions", + management: { + title: "Organization Management", + description: "Manage your organization details, members, are more", cta: { - createProject: { - label: "Create New Project", - }, manageTeam: { label: "Members", }, @@ -698,6 +720,9 @@ const app = { cta: { newProject: { label: "New Project", + basicTierTooltip: + "Your plan only allows you to create 3 projects. Upgrade to the Team plan to create unlimited projects.", + noSubscriptionTooltip: "Upgrade to a paid plan to create a project.", }, }, }, diff --git a/src/lib/hooks/useSidebarNavigationItems.tsx b/src/lib/hooks/useSidebarNavigationItems.tsx index d87d4688..c3c9e82c 100644 --- a/src/lib/hooks/useSidebarNavigationItems.tsx +++ b/src/lib/hooks/useSidebarNavigationItems.tsx @@ -101,7 +101,7 @@ const useSidebarNavigationItems = () => { children: [ { href: `/organizations/${organizationSlug}/projects`, - label: app.organizationPage.header.cta.viewAllProjects.label, + label: app.organizationPage.header.cta.viewProjects.label, isVisible: true, isActive: pathname === `/organizations/${organizationSlug}/projects`, From 65ac927d1aeff6dffcdc0b14b72dbeeb3cf335bb Mon Sep 17 00:00:00 2001 From: hobbescodes <87732294+hobbescodes@users.noreply.github.com> Date: Thu, 1 May 2025 15:44:59 -0500 Subject: [PATCH 065/103] chore: format --- .../[organizationSlug]/projects/[projectSlug]/page.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/app/organizations/[organizationSlug]/projects/[projectSlug]/page.tsx b/src/app/organizations/[organizationSlug]/projects/[projectSlug]/page.tsx index 2b2b2aec..9f3d1644 100644 --- a/src/app/organizations/[organizationSlug]/projects/[projectSlug]/page.tsx +++ b/src/app/organizations/[organizationSlug]/projects/[projectSlug]/page.tsx @@ -122,9 +122,7 @@ const ProjectPage = async ({ params }: Props) => { }), ]); - const hasAdminPrivileges = - member && - member.role !== Role.Member; + const hasAdminPrivileges = member && member.role !== Role.Member; return ( From 73275eb3b3c9267d6427fa1e53556c0d46f5c93b Mon Sep 17 00:00:00 2001 From: hobbescodes <87732294+hobbescodes@users.noreply.github.com> Date: Thu, 1 May 2025 16:04:26 -0500 Subject: [PATCH 066/103] refactor(header): stabilize spacing and size for nav items, update external link icon sizing --- src/components/layout/Header/Header.tsx | 73 ++++++++++++++----------- 1 file changed, 40 insertions(+), 33 deletions(-) diff --git a/src/components/layout/Header/Header.tsx b/src/components/layout/Header/Header.tsx index f4fa8c88..0b13e1f6 100644 --- a/src/components/layout/Header/Header.tsx +++ b/src/components/layout/Header/Header.tsx @@ -1,6 +1,6 @@ "use client"; -import { Flex, Icon, css, sigil } from "@omnidev/sigil"; +import { Flex, HStack, Icon, css, sigil } from "@omnidev/sigil"; import { Link as SigilLink } from "@omnidev/sigil"; import { useQuery } from "@tanstack/react-query"; import { usePathname } from "next/navigation"; @@ -45,39 +45,46 @@ const Header = () => { - {showPricingLink && ( - - - - {app.header.routes.pricing.label} - - - - )} + + {showPricingLink && ( + + + + {app.header.routes.pricing.label} + + + + )} - - {app.header.routes.docs.label} - - + + {app.header.routes.docs.label} + + + + From 10bc176a6968d622e89ecb1ec4bc4e32b8c53137 Mon Sep 17 00:00:00 2001 From: Beau Hawkinson <72956780+Twonarly1@users.noreply.github.com> Date: Thu, 1 May 2025 16:53:29 -0500 Subject: [PATCH 067/103] style: sticky headers with a backdrop filter --- src/components/core/CallToAction/CallToAction.tsx | 1 - src/components/core/LogoLink/LogoLink.tsx | 6 ++++-- src/components/landing/Hero/Hero.tsx | 12 +++--------- src/components/layout/Header/Header.tsx | 1 - src/components/layout/Layout/Layout.tsx | 9 ++++++++- .../ManagementSidebar/ManagementSidebar.tsx | 2 +- .../profile/ProfileSidebar/ProfileSidebar.tsx | 2 +- 7 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/components/core/CallToAction/CallToAction.tsx b/src/components/core/CallToAction/CallToAction.tsx index 5c0078a9..4d6a5b09 100644 --- a/src/components/core/CallToAction/CallToAction.tsx +++ b/src/components/core/CallToAction/CallToAction.tsx @@ -71,7 +71,6 @@ const CallToAction = ({ action }: Props) => { trigger={ ))} - + ); }; diff --git a/src/components/layout/Header/Header.tsx b/src/components/layout/Header/Header.tsx index 0b13e1f6..7aae8f64 100644 --- a/src/components/layout/Header/Header.tsx +++ b/src/components/layout/Header/Header.tsx @@ -38,7 +38,6 @@ const Header = () => { css={css.raw({ borderBottom: "1px solid", borderColor: "border.subtle", - backgroundColor: "background.default", })} > diff --git a/src/components/layout/Layout/Layout.tsx b/src/components/layout/Layout/Layout.tsx index 91de8520..9c910d25 100644 --- a/src/components/layout/Layout/Layout.tsx +++ b/src/components/layout/Layout/Layout.tsx @@ -31,7 +31,14 @@ const Layout = ({ children }: PropsWithChildren) => { ) : ( <> {/* NB: needs to be outside of main container in order to stay fixed to top of page, see: https://github.com/tailwindlabs/tailwindcss/discussions/3096#discussioncomment-212263 */} - +
diff --git a/src/components/organization/ManagementSidebar/ManagementSidebar.tsx b/src/components/organization/ManagementSidebar/ManagementSidebar.tsx index 555f03f7..e623a00a 100644 --- a/src/components/organization/ManagementSidebar/ManagementSidebar.tsx +++ b/src/components/organization/ManagementSidebar/ManagementSidebar.tsx @@ -156,8 +156,8 @@ const ManagementSidebar = ({ children }: PropsWithChildren) => { py={2} ml={{ base: 0, lg: -4 }} minH={14} - bgColor="background.default" gap={2} + style={{ backdropFilter: "blur(12px)" }} > + ) : ( + + {app.dashboardPage.recentFeedback.endOf} + + )} + ) : ( { - * const { userId } = variables; + * const { userId, after } = variables; * return HttpResponse.json({ * data: { posts } * }) diff --git a/src/generated/graphql.sdk.ts b/src/generated/graphql.sdk.ts index 3d7b1593..7247620a 100644 --- a/src/generated/graphql.sdk.ts +++ b/src/generated/graphql.sdk.ts @@ -5249,10 +5249,11 @@ export type ProjectsQuery = { __typename?: 'Query', projects?: { __typename?: 'P export type RecentFeedbackQueryVariables = Exact<{ userId: Scalars['UUID']['input']; + after?: InputMaybe; }>; -export type RecentFeedbackQuery = { __typename?: 'Query', posts?: { __typename?: 'PostConnection', nodes: Array<{ __typename?: 'Post', rowId: string, createdAt?: Date | null, title?: string | null, description?: string | null, project?: { __typename?: 'Project', name: string, slug: string, organization?: { __typename?: 'Organization', slug: string } | null } | null, status?: { __typename?: 'PostStatus', rowId: string, status: string, color?: string | null } | null, user?: { __typename?: 'User', rowId: string, username?: string | null } | null } | null> } | null }; +export type RecentFeedbackQuery = { __typename?: 'Query', posts?: { __typename?: 'PostConnection', totalCount: number, pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, endCursor?: string | null }, edges: Array<{ __typename?: 'PostEdge', node?: { __typename?: 'Post', rowId: string, createdAt?: Date | null, title?: string | null, description?: string | null, project?: { __typename?: 'Project', name: string, slug: string, organization?: { __typename?: 'Organization', slug: string } | null } | null, status?: { __typename?: 'PostStatus', rowId: string, status: string, color?: string | null } | null, user?: { __typename?: 'User', rowId: string, username?: string | null } | null } | null } | null> } | null }; export type StatusBreakdownQueryVariables = Exact<{ projectId: Scalars['UUID']['input']; @@ -5845,32 +5846,40 @@ export const ProjectsDocument = gql` } `; export const RecentFeedbackDocument = gql` - query RecentFeedback($userId: UUID!) { + query RecentFeedback($userId: UUID!, $after: Cursor) { posts( - first: 5 + first: 10 + after: $after orderBy: CREATED_AT_DESC filter: {project: {organization: {members: {some: {userId: {equalTo: $userId}}}}}} ) { - nodes { - rowId - createdAt - title - description - project { - name - slug - organization { + totalCount + pageInfo { + hasNextPage + endCursor + } + edges { + node { + rowId + createdAt + title + description + project { + name slug + organization { + slug + } + } + status { + rowId + status + color + } + user { + rowId + username } - } - status { - rowId - status - color - } - user { - rowId - username } } } diff --git a/src/generated/graphql.ts b/src/generated/graphql.ts index 2e885d59..c2f46d70 100644 --- a/src/generated/graphql.ts +++ b/src/generated/graphql.ts @@ -5248,10 +5248,11 @@ export type ProjectsQuery = { __typename?: 'Query', projects?: { __typename?: 'P export type RecentFeedbackQueryVariables = Exact<{ userId: Scalars['UUID']['input']; + after?: InputMaybe; }>; -export type RecentFeedbackQuery = { __typename?: 'Query', posts?: { __typename?: 'PostConnection', nodes: Array<{ __typename?: 'Post', rowId: string, createdAt?: Date | null, title?: string | null, description?: string | null, project?: { __typename?: 'Project', name: string, slug: string, organization?: { __typename?: 'Organization', slug: string } | null } | null, status?: { __typename?: 'PostStatus', rowId: string, status: string, color?: string | null } | null, user?: { __typename?: 'User', rowId: string, username?: string | null } | null } | null> } | null }; +export type RecentFeedbackQuery = { __typename?: 'Query', posts?: { __typename?: 'PostConnection', totalCount: number, pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, endCursor?: string | null }, edges: Array<{ __typename?: 'PostEdge', node?: { __typename?: 'Post', rowId: string, createdAt?: Date | null, title?: string | null, description?: string | null, project?: { __typename?: 'Project', name: string, slug: string, organization?: { __typename?: 'Organization', slug: string } | null } | null, status?: { __typename?: 'PostStatus', rowId: string, status: string, color?: string | null } | null, user?: { __typename?: 'User', rowId: string, username?: string | null } | null } | null } | null> } | null }; export type StatusBreakdownQueryVariables = Exact<{ projectId: Scalars['UUID']['input']; @@ -7008,32 +7009,40 @@ useInfiniteProjectsQuery.getKey = (variables: ProjectsQueryVariables) => ['Proje useProjectsQuery.fetcher = (variables: ProjectsQueryVariables, options?: RequestInit['headers']) => graphqlFetch(ProjectsDocument, variables, options); export const RecentFeedbackDocument = ` - query RecentFeedback($userId: UUID!) { + query RecentFeedback($userId: UUID!, $after: Cursor) { posts( - first: 5 + first: 10 + after: $after orderBy: CREATED_AT_DESC filter: {project: {organization: {members: {some: {userId: {equalTo: $userId}}}}}} ) { - nodes { - rowId - createdAt - title - description - project { - name - slug - organization { + totalCount + pageInfo { + hasNextPage + endCursor + } + edges { + node { + rowId + createdAt + title + description + project { + name slug + organization { + slug + } + } + status { + rowId + status + color + } + user { + rowId + username } - } - status { - rowId - status - color - } - user { - rowId - username } } } diff --git a/src/lib/config/app.config.ts b/src/lib/config/app.config.ts index 43f5c638..4f9d5c97 100644 --- a/src/lib/config/app.config.ts +++ b/src/lib/config/app.config.ts @@ -158,6 +158,8 @@ const app = { }, }, recentFeedback: { + loadMore: "Load More", + endOf: "End of Feedback", emptyState: { message: "No recent feedback found.", }, diff --git a/src/lib/graphql/queries/recentFeedback.query.graphql b/src/lib/graphql/queries/recentFeedback.query.graphql index 28f37f64..77252057 100644 --- a/src/lib/graphql/queries/recentFeedback.query.graphql +++ b/src/lib/graphql/queries/recentFeedback.query.graphql @@ -1,6 +1,7 @@ -query RecentFeedback($userId: UUID!) { +query RecentFeedback($userId: UUID!, $after: Cursor) { posts( - first: 5 + first: 10 + after: $after orderBy: CREATED_AT_DESC filter: { project: { @@ -8,26 +9,33 @@ query RecentFeedback($userId: UUID!) { } } ) { - nodes { - rowId - createdAt - title - description - project { - name - slug - organization { + totalCount + pageInfo { + hasNextPage + endCursor + } + edges { + node { + rowId + createdAt + title + description + project { + name slug + organization { + slug + } + } + status { + rowId + status + color + } + user { + rowId + username } - } - status { - rowId - status - color - } - user { - rowId - username } } } From 43d7b14bca9589884d5989e27f3d68aa8647952f Mon Sep 17 00:00:00 2001 From: hobbescodes <87732294+hobbescodes@users.noreply.github.com> Date: Thu, 1 May 2025 17:34:36 -0500 Subject: [PATCH 069/103] fix(forms): add explicit tabIndex to SubmitForm for Safari support --- src/app/organizations/[organizationSlug]/page.tsx | 2 ++ src/components/form/SubmitForm/SubmitForm.tsx | 1 + src/lib/config/app.config.ts | 1 + 3 files changed, 4 insertions(+) diff --git a/src/app/organizations/[organizationSlug]/page.tsx b/src/app/organizations/[organizationSlug]/page.tsx index f2e53708..94f54291 100644 --- a/src/app/organizations/[organizationSlug]/page.tsx +++ b/src/app/organizations/[organizationSlug]/page.tsx @@ -122,6 +122,8 @@ const OrganizationPage = async ({ params }: Props) => { // TODO: get Sigil Icon component working and update accordingly. Context: https://github.com/omnidotdev/backfeed-app/pull/44#discussion_r1897974331 icon: , href: `/organizations/${organizationSlug}/projects`, + disabled: !organization.projects.totalCount, + tooltip: app.organizationPage.header.cta.viewProjects.tooltip, }, ...(hasAdminPrivileges ? [ diff --git a/src/components/form/SubmitForm/SubmitForm.tsx b/src/components/form/SubmitForm/SubmitForm.tsx index 5bc473ae..d7abe1bd 100644 --- a/src/components/form/SubmitForm/SubmitForm.tsx +++ b/src/components/form/SubmitForm/SubmitForm.tsx @@ -51,6 +51,7 @@ const SubmitForm = ({