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 }) => (
-
+ ),
+ )}
);
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 && (
{
const session = await getAuthSession();
- if (!session) return null;
+ // if (!session) return null;
const sdk = getSdk({ session });
diff --git a/src/lib/actions/getProject.ts b/src/lib/actions/getProject.ts
index 6b7c4d8c..68dd9f78 100644
--- a/src/lib/actions/getProject.ts
+++ b/src/lib/actions/getProject.ts
@@ -19,7 +19,7 @@ const getProject = cache(
async ({ organizationSlug, projectSlug }: ProjectOptions) => {
const session = await getAuthSession();
- if (!session) return null;
+ // if (!session) return null;
const sdk = getSdk({ session });
diff --git a/src/lib/graphql/getSdk.ts b/src/lib/graphql/getSdk.ts
index fdeb4937..6d8d2f5d 100644
--- a/src/lib/graphql/getSdk.ts
+++ b/src/lib/graphql/getSdk.ts
@@ -7,7 +7,7 @@ import type { Session } from "next-auth";
interface Options {
/** Auth session required to retrieve the appropriate `accessToken`. */
- session: Session;
+ session: Session | null;
}
/**
@@ -16,7 +16,7 @@ interface Options {
const getSdk = ({ session }: Options) => {
const graphqlClient = new GraphQLClient(API_GRAPHQL_URL!, {
headers: {
- Authorization: `Bearer ${session.accessToken}`,
+ Authorization: `Bearer ${session?.accessToken ?? ""}`,
},
});
diff --git a/src/middleware.ts b/src/middleware.ts
index 4e233820..4fcea395 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -68,19 +68,19 @@ export const middleware = auth(async (request) => {
}
// If the user is not authenticated, redirect to the landing page
- if (!request.auth) {
- return redirect(request);
- }
+ // if (!request.auth) {
+ // return redirect(request);
+ // }
// If there is an error from the refresh token rotation, sign out the user (i.e. refresh token was expired)
- if (request.auth.error) {
+ if (request.auth?.error) {
return await signOut(request);
}
// Redirect user to their profile page upon successful checkout (or force redirect when trying to access confirmation route)
if (request.nextUrl.pathname.startsWith("/confirmation")) {
return NextResponse.redirect(
- `${process.env.NEXT_PUBLIC_BASE_URL}/profile/${request.auth.user?.hidraId}/subscription`,
+ `${process.env.NEXT_PUBLIC_BASE_URL}/profile/${request.auth?.user?.hidraId}/subscription`,
);
}
From a2c15a549e953b0c76761791e74ce79576d37181 Mon Sep 17 00:00:00 2001
From: hobbescodes <87732294+hobbescodes@users.noreply.github.com>
Date: Wed, 30 Apr 2025 21:53:59 -0500
Subject: [PATCH 050/103] refactor: disable or conditionally render actions
depending on session status
---
.../[organizationSlug]/projects/page.tsx | 18 +++++++++++-------
src/app/organizations/page.tsx | 19 ++++++++++++-------
.../feedback/CreateComment/CreateComment.tsx | 4 ++--
.../CreateFeedback/CreateFeedback.tsx | 2 ++
.../FeedbackDetails/FeedbackDetails.tsx | 3 +++
5 files changed, 30 insertions(+), 16 deletions(-)
diff --git a/src/app/organizations/[organizationSlug]/projects/page.tsx b/src/app/organizations/[organizationSlug]/projects/page.tsx
index 51af2d2d..d3150527 100644
--- a/src/app/organizations/[organizationSlug]/projects/page.tsx
+++ b/src/app/organizations/[organizationSlug]/projects/page.tsx
@@ -119,13 +119,17 @@ const ProjectsPage = async ({ params, searchParams }: Props) => {
header={{
title: app.projectsPage.header.title,
cta: [
- {
- label: app.projectsPage.header.cta.newProject.label,
- // TODO: get Sigil Icon component working and update accordingly. Context: https://github.com/omnidotdev/backfeed-app/pull/44#discussion_r1897974331
- icon: ,
- disabled: !canCreateProjects,
- dialogType: DialogType.CreateProject,
- },
+ ...(session
+ ? [
+ {
+ label: app.projectsPage.header.cta.newProject.label,
+ // TODO: get Sigil Icon component working and update accordingly. Context: https://github.com/omnidotdev/backfeed-app/pull/44#discussion_r1897974331
+ icon: ,
+ disabled: !canCreateProjects,
+ dialogType: DialogType.CreateProject,
+ },
+ ]
+ : []),
],
}}
>
diff --git a/src/app/organizations/page.tsx b/src/app/organizations/page.tsx
index 728efd72..a6c98443 100644
--- a/src/app/organizations/page.tsx
+++ b/src/app/organizations/page.tsx
@@ -94,13 +94,18 @@ const OrganizationsPage = async ({ searchParams }: Props) => {
header={{
title: app.organizationsPage.header.title,
cta: [
- {
- label: app.organizationsPage.header.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,
- disabled: !canCreateOrganization,
- },
+ ...(session
+ ? [
+ {
+ label:
+ app.organizationsPage.header.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,
+ disabled: !canCreateOrganization,
+ },
+ ]
+ : []),
],
}}
>
diff --git a/src/components/feedback/CreateComment/CreateComment.tsx b/src/components/feedback/CreateComment/CreateComment.tsx
index 9320502b..b3a4e086 100644
--- a/src/components/feedback/CreateComment/CreateComment.tsx
+++ b/src/components/feedback/CreateComment/CreateComment.tsx
@@ -39,7 +39,7 @@ const createCommentSchema = z.object({
const CreateComment = () => {
const queryClient = useQueryClient();
- const { user, isLoading: isAuthLoading } = useAuth();
+ const { user } = useAuth();
const { feedbackId } = useParams<{ feedbackId: string }>();
@@ -116,7 +116,7 @@ const CreateComment = () => {
placeholder={app.feedbackPage.comments.textAreaPlaceholder}
fontSize="sm"
minH={16}
- disabled={isAuthLoading}
+ disabled={!user}
maxLength={MAX_COMMENT_LENGTH}
errorProps={{
top: -6,
diff --git a/src/components/feedback/CreateFeedback/CreateFeedback.tsx b/src/components/feedback/CreateFeedback/CreateFeedback.tsx
index 5ff6a174..6e4ef628 100644
--- a/src/components/feedback/CreateFeedback/CreateFeedback.tsx
+++ b/src/components/feedback/CreateFeedback/CreateFeedback.tsx
@@ -172,6 +172,7 @@ const CreateFeedback = () => {
placeholder={
app.projectPage.projectFeedback.feedbackTitle.placeholder
}
+ disabled={!user}
/>
)}
@@ -186,6 +187,7 @@ const CreateFeedback = () => {
rows={5}
minH={32}
maxLength={MAX_DESCRIPTION_LENGTH}
+ disabled={!user}
/>
)}
diff --git a/src/components/feedback/FeedbackDetails/FeedbackDetails.tsx b/src/components/feedback/FeedbackDetails/FeedbackDetails.tsx
index 1fd868be..d1be43b3 100644
--- a/src/components/feedback/FeedbackDetails/FeedbackDetails.tsx
+++ b/src/components/feedback/FeedbackDetails/FeedbackDetails.tsx
@@ -127,6 +127,7 @@ const FeedbackDetails = ({ feedbackId, ...rest }: Props) => {
icon: hasUpvoted ? PiArrowFatLineUpFill : PiArrowFatLineUp,
color: "brand.tertiary",
onClick: () => handleUpvote(),
+ disabled: !user,
},
{
id: "downvote",
@@ -135,6 +136,7 @@ const FeedbackDetails = ({ feedbackId, ...rest }: Props) => {
icon: hasDownvoted ? PiArrowFatLineDownFill : PiArrowFatLineDown,
color: "brand.quinary",
onClick: () => handleDownvote(),
+ disabled: !user,
},
];
@@ -172,6 +174,7 @@ const FeedbackDetails = ({ feedbackId, ...rest }: Props) => {
},
...rest,
}}
+ contentProps={{ display: !user ? "none" : undefined }}
>
{tooltip}
From 2fda3b2e06fc8c1ffb128e81560bc56846029ff3 Mon Sep 17 00:00:00 2001
From: hobbescodes <87732294+hobbescodes@users.noreply.github.com>
Date: Wed, 30 Apr 2025 22:01:22 -0500
Subject: [PATCH 051/103] chore: clean up files, remove commented code
---
.../(manage)/members/page.tsx | 17 ---------
.../(manage)/settings/page.tsx | 17 ---------
.../organizations/[organizationSlug]/page.tsx | 2 -
.../[projectSlug]/[feedbackId]/page.tsx | 37 -------------------
.../projects/[projectSlug]/page.tsx | 2 -
.../[organizationSlug]/projects/page.tsx | 2 -
src/app/organizations/page.tsx | 3 --
7 files changed, 80 deletions(-)
diff --git a/src/app/organizations/[organizationSlug]/(manage)/members/page.tsx b/src/app/organizations/[organizationSlug]/(manage)/members/page.tsx
index aa6b340d..75e6681b 100644
--- a/src/app/organizations/[organizationSlug]/(manage)/members/page.tsx
+++ b/src/app/organizations/[organizationSlug]/(manage)/members/page.tsx
@@ -51,8 +51,6 @@ const OrganizationMembersPage = async ({ params, searchParams }: Props) => {
const session = await auth();
- // if (!session) notFound();
-
const organization = await getOrganization({
organizationSlug,
});
@@ -101,21 +99,6 @@ const OrganizationMembersPage = async ({ params, searchParams }: Props) => {
excludeRoles: [Role.Owner],
}),
}),
- // 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 f3873b34..fe564ac8 100644
--- a/src/app/organizations/[organizationSlug]/(manage)/settings/page.tsx
+++ b/src/app/organizations/[organizationSlug]/(manage)/settings/page.tsx
@@ -44,8 +44,6 @@ const OrganizationSettingsPage = async ({ params }: Props) => {
const session = await auth();
- // if (!session) notFound();
-
const organization = await getOrganization({ organizationSlug });
if (!organization) notFound();
@@ -53,21 +51,6 @@ const OrganizationSettingsPage = async ({ params }: Props) => {
const queryClient = getQueryClient();
await Promise.all([
- // 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,
diff --git a/src/app/organizations/[organizationSlug]/page.tsx b/src/app/organizations/[organizationSlug]/page.tsx
index 150b7c2b..a7b42a0a 100644
--- a/src/app/organizations/[organizationSlug]/page.tsx
+++ b/src/app/organizations/[organizationSlug]/page.tsx
@@ -54,8 +54,6 @@ const OrganizationPage = async ({ params }: Props) => {
const session = await auth();
- // if (!session) notFound();
-
const [organization, isBasicTier, isTeamTier] = await Promise.all([
getOrganization({ organizationSlug }),
enableBasicTierPrivilegesFlag(),
diff --git a/src/app/organizations/[organizationSlug]/projects/[projectSlug]/[feedbackId]/page.tsx b/src/app/organizations/[organizationSlug]/projects/[projectSlug]/[feedbackId]/page.tsx
index da58c78b..51292f50 100644
--- a/src/app/organizations/[organizationSlug]/projects/[projectSlug]/[feedbackId]/page.tsx
+++ b/src/app/organizations/[organizationSlug]/projects/[projectSlug]/[feedbackId]/page.tsx
@@ -39,8 +39,6 @@ const FeedbackPage = async ({ params }: Props) => {
const session = await auth();
- // if (!session) notFound();
-
const sdk = getSdk({ session });
const { post: feedback } = await sdk.FeedbackById({ rowId: feedbackId });
@@ -102,41 +100,6 @@ const FeedbackPage = async ({ params }: Props) => {
}),
]
: []),
- // 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 c9beebbe..6052d05c 100644
--- a/src/app/organizations/[organizationSlug]/projects/[projectSlug]/page.tsx
+++ b/src/app/organizations/[organizationSlug]/projects/[projectSlug]/page.tsx
@@ -49,8 +49,6 @@ const ProjectPage = async ({ params }: Props) => {
const session = await auth();
- // if (!session) notFound();
-
const project = await getProject({ organizationSlug, projectSlug });
if (!project) notFound();
diff --git a/src/app/organizations/[organizationSlug]/projects/page.tsx b/src/app/organizations/[organizationSlug]/projects/page.tsx
index d3150527..998bdd5d 100644
--- a/src/app/organizations/[organizationSlug]/projects/page.tsx
+++ b/src/app/organizations/[organizationSlug]/projects/page.tsx
@@ -48,8 +48,6 @@ const ProjectsPage = async ({ params, searchParams }: Props) => {
const session = await auth();
- // if (!session) notFound();
-
const [organization, isBasicTier, isTeamTier] = await Promise.all([
getOrganization({ organizationSlug }),
enableBasicTierPrivilegesFlag(),
diff --git a/src/app/organizations/page.tsx b/src/app/organizations/page.tsx
index a6c98443..b2305ef2 100644
--- a/src/app/organizations/page.tsx
+++ b/src/app/organizations/page.tsx
@@ -43,8 +43,6 @@ interface Props {
const OrganizationsPage = async ({ searchParams }: Props) => {
const session = await auth();
- // if (!session) notFound();
-
const sdk = getSdk({ session });
const [{ organizations }, isBasicTier, isTeamTier] = await Promise.all([
@@ -82,7 +80,6 @@ const OrganizationsPage = async ({ searchParams }: Props) => {
queryFn: useOrganizationsQuery.fetcher(variables),
});
- // TODO: discuss the below. Should the check be strictly scoped to ownership??
// NB: To create an organization, user must be subscribed. If they are subscribed, we validate that they are either on the team tier subscription (unlimited organizations) or that they are not currently an owner/admin of another organization
const canCreateOrganization =
isBasicTier && (isTeamTier || !organizations?.totalCount);
From a545de3977dc781c17eb879493917d49b5071920 Mon Sep 17 00:00:00 2001
From: hobbescodes <87732294+hobbescodes@users.noreply.github.com>
Date: Wed, 30 Apr 2025 22:20:19 -0500
Subject: [PATCH 052/103] refactor: reduce auth requests for subscription
queries
---
src/lib/actions/getCustomer.ts | 6 +++---
src/lib/actions/getSubscription.ts | 4 ++--
src/lib/flags/identity/subscription.identity.ts | 4 +---
src/lib/options/subscription.options.ts | 2 +-
4 files changed, 7 insertions(+), 9 deletions(-)
diff --git a/src/lib/actions/getCustomer.ts b/src/lib/actions/getCustomer.ts
index a9acd80a..b8889f76 100644
--- a/src/lib/actions/getCustomer.ts
+++ b/src/lib/actions/getCustomer.ts
@@ -6,15 +6,15 @@ import { polar } from "lib/polar";
/**
* Server action to get customer details.
*/
-const getCustomer = async (userId: string) => {
+const getCustomer = async () => {
const session = await auth();
if (!session) {
- throw new Error("Unauthorized");
+ throw new Error("No customer found");
}
return await polar.customers.getStateExternal({
- externalId: userId,
+ externalId: session.user?.hidraId!,
});
};
diff --git a/src/lib/actions/getSubscription.ts b/src/lib/actions/getSubscription.ts
index 725b17c1..9bf13b54 100644
--- a/src/lib/actions/getSubscription.ts
+++ b/src/lib/actions/getSubscription.ts
@@ -5,8 +5,8 @@ import { getCustomer, getProduct } from "lib/actions";
/**
* Server action to get subscription details.
*/
-const getSubscription = async (userId: string) => {
- const customer = await getCustomer(userId);
+const getSubscription = async () => {
+ const customer = await getCustomer();
if (!customer.activeSubscriptions.length) {
throw new Error("No active subscriptions found");
diff --git a/src/lib/flags/identity/subscription.identity.ts b/src/lib/flags/identity/subscription.identity.ts
index 8c202c46..cbb31543 100644
--- a/src/lib/flags/identity/subscription.identity.ts
+++ b/src/lib/flags/identity/subscription.identity.ts
@@ -1,6 +1,5 @@
import { dedupe } from "flags/next";
-import { auth } from "auth";
import { getSubscription } from "lib/actions";
/**
@@ -8,8 +7,7 @@ import { getSubscription } from "lib/actions";
*/
const dedupeSubscription = dedupe(async () => {
try {
- const session = await auth();
- const subscription = await getSubscription(session?.user?.hidraId!);
+ const subscription = await getSubscription();
return subscription;
} catch (error) {
diff --git a/src/lib/options/subscription.options.ts b/src/lib/options/subscription.options.ts
index 9c1c6890..1d3b07ec 100644
--- a/src/lib/options/subscription.options.ts
+++ b/src/lib/options/subscription.options.ts
@@ -12,7 +12,7 @@ interface Options {
const subscriptionOptions = ({ hidraId, enabled = true }: Options) =>
queryOptions({
queryKey: ["Subscription", { hidraId }],
- queryFn: async () => getSubscription(hidraId!),
+ queryFn: async () => getSubscription(),
enabled: enabled && !!hidraId,
retry: false,
});
From 28bf555c818867d1c0c42c10d0a59672b3ba777f Mon Sep 17 00:00:00 2001
From: hobbescodes <87732294+hobbescodes@users.noreply.github.com>
Date: Wed, 30 Apr 2025 22:27:30 -0500
Subject: [PATCH 053/103] chore: remove commented out code
---
src/lib/actions/getOrganization.ts | 2 --
src/lib/actions/getProject.ts | 2 --
2 files changed, 4 deletions(-)
diff --git a/src/lib/actions/getOrganization.ts b/src/lib/actions/getOrganization.ts
index b463a270..62add809 100644
--- a/src/lib/actions/getOrganization.ts
+++ b/src/lib/actions/getOrganization.ts
@@ -17,8 +17,6 @@ const getOrganization = cache(
async ({ organizationSlug }: OrganizationOptions) => {
const session = await getAuthSession();
- // if (!session) return null;
-
const sdk = getSdk({ session });
const { organizationBySlug: organization } = await sdk.Organization({
diff --git a/src/lib/actions/getProject.ts b/src/lib/actions/getProject.ts
index 68dd9f78..641e35cd 100644
--- a/src/lib/actions/getProject.ts
+++ b/src/lib/actions/getProject.ts
@@ -19,8 +19,6 @@ const getProject = cache(
async ({ organizationSlug, projectSlug }: ProjectOptions) => {
const session = await getAuthSession();
- // if (!session) return null;
-
const sdk = getSdk({ session });
const { projects } = await sdk.Project({ projectSlug, organizationSlug });
From 5a8e2ee4d69b749b609828cedea95c8568178a3e Mon Sep 17 00:00:00 2001
From: hobbescodes <87732294+hobbescodes@users.noreply.github.com>
Date: Wed, 30 Apr 2025 22:29:15 -0500
Subject: [PATCH 054/103] chore: remove commented out code
---
src/middleware.ts | 5 -----
1 file changed, 5 deletions(-)
diff --git a/src/middleware.ts b/src/middleware.ts
index 4fcea395..26997128 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -67,11 +67,6 @@ export const middleware = auth(async (request) => {
return NextResponse.json({}, { status: 200 });
}
- // If the user is not authenticated, redirect to the landing page
- // if (!request.auth) {
- // return redirect(request);
- // }
-
// If there is an error from the refresh token rotation, sign out the user (i.e. refresh token was expired)
if (request.auth?.error) {
return await signOut(request);
From ef411a535b44cc92e92df4393b2d1b69b64fa10d Mon Sep 17 00:00:00 2001
From: benjamin-parks
Date: Thu, 1 May 2025 13:24:17 -0500
Subject: [PATCH 055/103] chore: add documents Link to header and footer
---
src/components/layout/Footer/Footer.tsx | 13 ++++++++++++-
src/components/layout/Header/Header.tsx | 10 ++++++++++
2 files changed, 22 insertions(+), 1 deletion(-)
diff --git a/src/components/layout/Footer/Footer.tsx b/src/components/layout/Footer/Footer.tsx
index 0adce19d..adec9083 100644
--- a/src/components/layout/Footer/Footer.tsx
+++ b/src/components/layout/Footer/Footer.tsx
@@ -1,6 +1,6 @@
"use client";
-import { css, sigil } from "@omnidev/sigil";
+import { css, Divider, Link, sigil } from "@omnidev/sigil";
import { app } from "lib/config";
@@ -22,6 +22,17 @@ const Footer = () => (
})}
>
© {new Date().getFullYear()} {app.organization.name}
+
+
+ Docs
+
);
diff --git a/src/components/layout/Header/Header.tsx b/src/components/layout/Header/Header.tsx
index 3b46763f..8b183e5d 100644
--- a/src/components/layout/Header/Header.tsx
+++ b/src/components/layout/Header/Header.tsx
@@ -1,6 +1,7 @@
"use client";
import { Flex, css, sigil } from "@omnidev/sigil";
+import { Link as SigilLink } from "@omnidev/sigil";
import { useQuery } from "@tanstack/react-query";
import { usePathname } from "next/navigation";
@@ -64,6 +65,15 @@ const Header = () => {
)}
+
+ Docs ↗
+
From 74ceed6a16c128afbad66f79d73cf35736d3a9a6 Mon Sep 17 00:00:00 2001
From: Brian Cooper
Date: Thu, 1 May 2025 14:31:29 -0500
Subject: [PATCH 056/103] feature(hero): add hero image
---
public/img/hero.png | Bin 0 -> 102086 bytes
src/components/landing/Hero/Hero.tsx | 30 ++++++++++++++++++++++++---
src/lib/config/app.config.ts | 1 +
3 files changed, 28 insertions(+), 3 deletions(-)
create mode 100644 public/img/hero.png
diff --git a/public/img/hero.png b/public/img/hero.png
new file mode 100644
index 0000000000000000000000000000000000000000..c6ad881e2dc589f082994a8d1b88fdc9b8d22a0a
GIT binary patch
literal 102086
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