diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index 0ca884bff3f..b0f7508b53f 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -11,6 +11,7 @@ "@radix-ui/react-alert-dialog": "^1.1.14", "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-checkbox": "^1.3.2", + "@radix-ui/react-collapsible": "^1.1.11", "@radix-ui/react-dialog": "1.1.14", "@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-hover-card": "^1.1.14", diff --git a/apps/dashboard/redirects.js b/apps/dashboard/redirects.js index b8e2f83154c..2ff384ca960 100644 --- a/apps/dashboard/redirects.js +++ b/apps/dashboard/redirects.js @@ -106,9 +106,27 @@ const projectPageRedirects = [ source: `${projectRoute}/nebula/:path*`, }, { - source: `${projectRoute}/connect/analytics`, destination: `${projectRoute}`, permanent: false, + source: `${projectRoute}/connect/analytics`, + }, +]; + +const teamPageRedirects = [ + { + destination: "/team/:team_slug/~/billing/:path*", + permanent: false, + source: "/team/:team_slug/~/settings/billing/:path*", + }, + { + destination: "/team/:team_slug/~/billing/invoices/:path*", + permanent: false, + source: "/team/:team_slug/~/settings/invoices/:path*", + }, + { + destination: "/team/:team_slug/~/billing", + permanent: false, + source: "/team/:team_slug/~/settings/credits", }, ]; @@ -426,6 +444,7 @@ async function redirects() { }, ...legacyDashboardToTeamRedirects, ...projectPageRedirects, + ...teamPageRedirects, ]; } diff --git a/apps/dashboard/src/@/components/analytics/date-range-selector.tsx b/apps/dashboard/src/@/components/analytics/date-range-selector.tsx index e7ff878c346..0628a2e885a 100644 --- a/apps/dashboard/src/@/components/analytics/date-range-selector.tsx +++ b/apps/dashboard/src/@/components/analytics/date-range-selector.tsx @@ -8,11 +8,13 @@ import { SelectValue, } from "@/components/ui/select"; import { normalizeTime } from "@/lib/time"; +import { cn } from "@/lib/utils"; export function DateRangeSelector(props: { range: Range; setRange: (range: Range) => void; popoverAlign?: "start" | "end" | "center"; + className?: string; }) { const { range, setRange } = props; const daysDiff = differenceInCalendarDays(range.to, range.from); @@ -26,7 +28,7 @@ export function DateRangeSelector(props: { return ( diff --git a/apps/dashboard/src/@/components/analytics/range-selector.tsx b/apps/dashboard/src/@/components/analytics/range-selector.tsx index b3a42292929..e092420ad80 100644 --- a/apps/dashboard/src/@/components/analytics/range-selector.tsx +++ b/apps/dashboard/src/@/components/analytics/range-selector.tsx @@ -76,8 +76,9 @@ export function RangeSelector({ }); return ( -
+
{ setRange(newRange); @@ -89,7 +90,7 @@ export function RangeSelector({ }} /> { setInterval(newInterval); diff --git a/apps/dashboard/src/@/components/billing/CancelPlanModal/CancelPlanModal.tsx b/apps/dashboard/src/@/components/billing/CancelPlanModal/CancelPlanModal.tsx index b74dbcae1bc..68e162e2ca7 100644 --- a/apps/dashboard/src/@/components/billing/CancelPlanModal/CancelPlanModal.tsx +++ b/apps/dashboard/src/@/components/billing/CancelPlanModal/CancelPlanModal.tsx @@ -82,7 +82,7 @@ function UnpaidInvoicesWarning({ teamSlug }: { teamSlug: string }) {
); } - -export function FullWidthSidebarLayout(props: { - contentSidebarLinks: SidebarLink[]; - footerSidebarLinks?: SidebarLink[]; - children: React.ReactNode; - className?: string; -}) { - const { contentSidebarLinks, children, footerSidebarLinks } = props; - return ( -
- {/* left - sidebar */} - - - - - - {footerSidebarLinks && ( - - - - )} - - - - - {/* right - content */} -
- - -
- {children} -
- -
-
- ); -} - -function RenderSidebarGroup(props: { - sidebarLinks: SidebarLink[]; - groupName: string; -}) { - return ( - - - {props.groupName} - - - - - - ); -} - -function RenderSidebarMenu(props: { links: SidebarLink[] }) { - const sidebar = useSidebar(); - return ( - - {props.links.map((link, idx) => { - // link - if ("href" in link) { - return ( - - - { - sidebar.setOpenMobile(false); - }} - > - {link.icon && } - {link.label} - - - - ); - } - - // separator - if ("separator" in link) { - return ( - - ); - } - - // group - return ( - - ); - })} - - ); -} - -function MobileSidebarTrigger(props: { links: SidebarLink[] }) { - const activeLink = useActiveSidebarLink(props.links); - - return ( -
- - - {activeLink && {activeLink.label}} -
- ); -} diff --git a/apps/dashboard/src/@/components/blocks/TeamPlanBadge.tsx b/apps/dashboard/src/@/components/blocks/TeamPlanBadge.tsx index b7d0876e823..96e80469b84 100644 --- a/apps/dashboard/src/@/components/blocks/TeamPlanBadge.tsx +++ b/apps/dashboard/src/@/components/blocks/TeamPlanBadge.tsx @@ -45,7 +45,7 @@ export function TeamPlanBadge(props: { } e.stopPropagation(); e.preventDefault(); - router.push(`/team/${props.teamSlug}/~/settings/billing?showPlans=true`); + router.push(`/team/${props.teamSlug}/~/billing?showPlans=true`); } return ( diff --git a/apps/dashboard/src/@/components/blocks/full-width-sidebar-layout.tsx b/apps/dashboard/src/@/components/blocks/full-width-sidebar-layout.tsx new file mode 100644 index 00000000000..17958a9b3f9 --- /dev/null +++ b/apps/dashboard/src/@/components/blocks/full-width-sidebar-layout.tsx @@ -0,0 +1,297 @@ +"use client"; + +import { ChevronDownIcon, ChevronRightIcon } from "lucide-react"; +import { usePathname } from "next/navigation"; +import { useMemo } from "react"; +import { AppFooter } from "@/components/footers/app-footer"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; +import { DynamicHeight } from "@/components/ui/DynamicHeight"; +import { NavLink } from "@/components/ui/NavLink"; +import { Separator } from "@/components/ui/separator"; +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarMenuSub, + SidebarMenuSubItem, + SidebarRail, + SidebarSeparator, + SidebarTrigger, + useSidebar, +} from "@/components/ui/sidebar"; +import { cn } from "@/lib/utils"; + +type ShadcnSidebarBaseLink = { + href: string; + label: React.ReactNode; + exactMatch?: boolean; + icon?: React.FC<{ className?: string }>; + isActive?: (pathname: string) => boolean; +}; + +type ShadcnSidebarLink = + | ShadcnSidebarBaseLink + | { + group: string; + links: ShadcnSidebarBaseLink[]; + } + | { + separator: true; + } + | { + subMenu: Omit; + links: Omit[]; + }; + +export function FullWidthSidebarLayout(props: { + contentSidebarLinks: ShadcnSidebarLink[]; + footerSidebarLinks?: ShadcnSidebarLink[]; + children: React.ReactNode; + className?: string; +}) { + const { contentSidebarLinks, children, footerSidebarLinks } = props; + return ( +
+ {/* left - sidebar */} + + + + + + {footerSidebarLinks && ( + + + + )} + + + + + {/* right - content */} +
+ + +
+ {children} +
+ +
+
+ ); +} + +function MobileSidebarTrigger(props: { links: ShadcnSidebarLink[] }) { + const activeLink = useActiveShadcnSidebarLink(props.links); + const parentSubNav = props.links.find( + (link) => + "subMenu" in link && link.links.some((l) => l.href === activeLink?.href), + ); + + return ( +
+ + + {parentSubNav && "subMenu" in parentSubNav && ( + <> + {parentSubNav.subMenu.label} + + + )} + {activeLink && {activeLink.label}} +
+ ); +} + +function useActiveShadcnSidebarLink(links: ShadcnSidebarLink[]) { + const pathname = usePathname(); + + const activeLink = useMemo(() => { + function isActive(link: ShadcnSidebarBaseLink) { + if (link.exactMatch) { + return link.href === pathname; + } + return pathname?.startsWith(link.href); + } + + for (const link of links) { + if ("links" in link) { + for (const subLink of link.links) { + if (isActive(subLink)) { + return subLink; + } + } + } else if ("href" in link) { + if (isActive(link)) { + return link; + } + } + } + }, [links, pathname]); + + return activeLink; +} + +function useIsSubnavActive(links: ShadcnSidebarBaseLink[]) { + const pathname = usePathname(); + + const isSubnavActive = useMemo(() => { + function isActive(link: ShadcnSidebarBaseLink) { + if (link.exactMatch) { + return link.href === pathname; + } + return pathname?.startsWith(link.href); + } + + return links.some(isActive); + }, [links, pathname]); + + return isSubnavActive; +} + +function RenderSidebarGroup(props: { + sidebarLinks: ShadcnSidebarLink[]; + groupName: string; +}) { + return ( + + + {props.groupName} + + + + + + ); +} + +function RenderSidebarSubmenu(props: { + links: ShadcnSidebarBaseLink[]; + subMenu: Omit; +}) { + const sidebar = useSidebar(); + const isSubnavActive = useIsSubnavActive(props.links); + return ( + + + + + + + {props.subMenu.icon && ( + + )} + {props.subMenu.label} + + + + + + + {props.links.map((link) => { + return ( + + { + sidebar.setOpenMobile(false); + }} + > + {link.icon && } + {link.label} + + + ); + })} + + + + + + + ); +} + +function RenderSidebarMenu(props: { links: ShadcnSidebarLink[] }) { + const sidebar = useSidebar(); + return ( + + {props.links.map((link, idx) => { + // link + if ("href" in link) { + return ( + + + { + sidebar.setOpenMobile(false); + }} + > + {link.icon && } + {link.label} + + + + ); + } + + // separator + if ("separator" in link) { + return ( + + ); + } + + // subnav + if ("subMenu" in link) { + return ( + + ); + } + + // group + return ( + + ); + })} + + ); +} diff --git a/apps/dashboard/src/@/components/blocks/pagination-buttons.tsx b/apps/dashboard/src/@/components/blocks/pagination-buttons.tsx index c69406b2eec..d4e98b76ec8 100644 --- a/apps/dashboard/src/@/components/blocks/pagination-buttons.tsx +++ b/apps/dashboard/src/@/components/blocks/pagination-buttons.tsx @@ -48,7 +48,7 @@ export const PaginationButtons = (props: { { setPage(activePage - 1); @@ -57,7 +57,7 @@ export const PaginationButtons = (props: { { setPage(activePage + 1); @@ -78,7 +78,7 @@ export const PaginationButtons = (props: { {pages.map((page) => ( { setPage(page); @@ -98,7 +98,7 @@ export const PaginationButtons = (props: { { setPage(activePage - 1); @@ -111,7 +111,7 @@ export const PaginationButtons = (props: { <> { setPage(1); }} @@ -129,7 +129,7 @@ export const PaginationButtons = (props: { {activePage - 1 > 0 && ( { setPage(activePage - 1); }} @@ -140,7 +140,7 @@ export const PaginationButtons = (props: { )} - + {activePage} @@ -148,7 +148,7 @@ export const PaginationButtons = (props: { {activePage + 1 <= totalPages && ( { setPage(activePage + 1); }} @@ -167,7 +167,7 @@ export const PaginationButtons = (props: { { setPage(totalPages); }} @@ -180,7 +180,7 @@ export const PaginationButtons = (props: { { setPage(activePage + 1); diff --git a/apps/dashboard/src/@/components/blocks/upsell-wrapper.tsx b/apps/dashboard/src/@/components/blocks/upsell-wrapper.tsx index 31e71e3fb32..d632d163f66 100644 --- a/apps/dashboard/src/@/components/blocks/upsell-wrapper.tsx +++ b/apps/dashboard/src/@/components/blocks/upsell-wrapper.tsx @@ -138,7 +138,7 @@ export function UpsellContent(props: {
diff --git a/apps/dashboard/src/@/components/ui/collapsible.tsx b/apps/dashboard/src/@/components/ui/collapsible.tsx new file mode 100644 index 00000000000..cb003d17563 --- /dev/null +++ b/apps/dashboard/src/@/components/ui/collapsible.tsx @@ -0,0 +1,11 @@ +"use client"; + +import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"; + +const Collapsible = CollapsiblePrimitive.Root; + +const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger; + +const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent; + +export { Collapsible, CollapsibleTrigger, CollapsibleContent }; diff --git a/apps/dashboard/src/@/components/ui/pagination.tsx b/apps/dashboard/src/@/components/ui/pagination.tsx index 6e1de48bee3..d22b03225b2 100644 --- a/apps/dashboard/src/@/components/ui/pagination.tsx +++ b/apps/dashboard/src/@/components/ui/pagination.tsx @@ -50,9 +50,10 @@ const PaginationLink = ({ diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/_components/FreePlanUpsellBannerUI.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/_components/FreePlanUpsellBannerUI.tsx index 18e84337500..d9a53c65418 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/_components/FreePlanUpsellBannerUI.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/_components/FreePlanUpsellBannerUI.tsx @@ -18,7 +18,7 @@ export function FreePlanUpsellBannerUI(props: { accentColor="green" cta={{ icon: , - link: `/team/${props.teamSlug}/~/settings/billing?showPlans=true&highlight=${ + link: `/team/${props.teamSlug}/~/billing?showPlans=true&highlight=${ props.highlightPlan || "growth" }`, text: "View plans", diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/_components/StaffModeNotice.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/_components/StaffModeNotice.tsx new file mode 100644 index 00000000000..0e006e41794 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/_components/StaffModeNotice.tsx @@ -0,0 +1,29 @@ +import { ArrowRightIcon } from "lucide-react"; +import Link from "next/link"; +import { Button } from "@/components/ui/button"; + +export function StaffModeNotice() { + return ( +
+
+
+

Staff Mode

+

+ You can only view this team, not take any actions. +

+
+ +
+
+ ); +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/layout.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/layout.tsx index ee12a44ebe2..88810bcbf4d 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/layout.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/layout.tsx @@ -1,17 +1,18 @@ -import Link from "next/link"; import { redirect } from "next/navigation"; import { getAuthToken, getAuthTokenWalletAddress } from "@/api/auth-token"; import { getProjects } from "@/api/projects"; import { getTeamBySlug, getTeams } from "@/api/team"; import { CustomChatButton } from "@/components/chat/CustomChatButton"; -import { AppFooter } from "@/components/footers/app-footer"; import { AnnouncementBanner } from "@/components/misc/AnnouncementBanner"; -import { Button } from "@/components/ui/button"; -import { TabPathLinks } from "@/components/ui/tabs"; +import { SidebarProvider } from "@/components/ui/sidebar"; +import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs"; import { getClientThirdwebClient } from "@/constants/thirdweb-client.client"; import { siwaExamplePrompts } from "../../../(dashboard)/support/definitions"; import { getValidAccount } from "../../../account/settings/getAccount"; import { TeamHeaderLoggedIn } from "../../components/TeamHeader/team-header-logged-in.client"; +import { StaffModeNotice } from "./_components/StaffModeNotice"; +import type { Ecosystem } from "./~/ecosystem/types"; +import { TeamSidebarLayout } from "./TeamSidebarLayout"; export default async function TeamLayout(props: { children: React.ReactNode; @@ -43,81 +44,66 @@ export default async function TeamLayout(props: { teamId: team.id, }); + const ecosystems = await fetchEcosystemList(team.id, authToken); + + const isStaffMode = !teams.some((t) => t.slug === team.slug); + return ( -
- {!teams.some((t) => t.slug === team.slug) && ( -
-
-
-

👀 STAFF MODE 👀

-

- You can only view this team, not take any actions. -

-
- -
+ +
+ {isStaffMode && } + +
+
- )} - -
- - + ({ + name: ecosystem.name, + slug: ecosystem.slug, + }))} + layoutPath={`/team/${params.team_slug}`} + > + {props.children} + +
+ +
+ + ); +} -
{props.children}
-
- -
- -
+async function fetchEcosystemList(teamId: string, authToken: string) { + const res = await fetch( + `${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${teamId}/ecosystem-wallet`, + { + headers: { + Authorization: `Bearer ${authToken}`, + }, + }, ); + + if (!res.ok) { + return []; + } + + return (await res.json()).result as Ecosystem[]; } diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/analytics/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/analytics/page.tsx index 77f4e64b130..14efbfe0b93 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/analytics/page.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/analytics/page.tsx @@ -73,7 +73,7 @@ export default async function TeamOverviewPage(props: { title="Analytics" />
-
+
} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/audit-log/layout.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/audit-log/layout.tsx index 16f58d38a37..6e827f49953 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/audit-log/layout.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/audit-log/layout.tsx @@ -24,13 +24,15 @@ export default async function Layout(props: { >
-
+

Audit Log

- {props.children} +
+ {props.children} +
); diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/audit-log/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/audit-log/page.tsx index a7b514dffee..8fb940cd670 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/audit-log/page.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/audit-log/page.tsx @@ -47,7 +47,7 @@ export default async function Page(props: { } return ( -
+
{auditLogs.data.result.length === 0 ? (

No audit events found

diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/billing/components/PlanInfoCard.client.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/billing/components/PlanInfoCard.client.tsx similarity index 100% rename from apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/billing/components/PlanInfoCard.client.tsx rename to apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/billing/components/PlanInfoCard.client.tsx diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/billing/components/PlanInfoCard.stories.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/billing/components/PlanInfoCard.stories.tsx similarity index 100% rename from apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/billing/components/PlanInfoCard.stories.tsx rename to apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/billing/components/PlanInfoCard.stories.tsx diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/billing/components/PlanInfoCard.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/billing/components/PlanInfoCard.tsx similarity index 99% rename from apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/billing/components/PlanInfoCard.tsx rename to apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/billing/components/PlanInfoCard.tsx index da1a9ff95ea..e3bcb381330 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/billing/components/PlanInfoCard.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/billing/components/PlanInfoCard.tsx @@ -237,7 +237,7 @@ export function PlanInfoCardUI(props: { size="sm" variant="outline" > - + View Invoices diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/billing/components/credit-balance-section.client.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/billing/components/credit-balance-section.client.tsx similarity index 99% rename from apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/billing/components/credit-balance-section.client.tsx rename to apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/billing/components/credit-balance-section.client.tsx index 2ff5f403932..82e6a8a443e 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/billing/components/credit-balance-section.client.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/billing/components/credit-balance-section.client.tsx @@ -16,7 +16,7 @@ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Separator } from "@/components/ui/separator"; import { Skeleton } from "@/components/ui/skeleton"; import { ToolTipLabel } from "@/components/ui/tooltip"; -import { ThirdwebMiniLogo } from "../../../../../../../components/ThirdwebMiniLogo"; +import { ThirdwebMiniLogo } from "../../../../../../components/ThirdwebMiniLogo"; const predefinedAmounts = [ { label: "$25", value: "25" }, diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/invoices/components/billing-filter.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/billing/invoices/components/billing-filter.tsx similarity index 100% rename from apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/invoices/components/billing-filter.tsx rename to apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/billing/invoices/components/billing-filter.tsx diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/invoices/components/billing-history.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/billing/invoices/components/billing-history.tsx similarity index 100% rename from apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/invoices/components/billing-history.tsx rename to apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/billing/invoices/components/billing-history.tsx diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/invoices/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/billing/invoices/page.tsx similarity index 96% rename from apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/invoices/page.tsx rename to apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/billing/invoices/page.tsx index baee48e1592..a578106f911 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/invoices/page.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/billing/invoices/page.tsx @@ -19,7 +19,7 @@ export default async function Page(props: { searchParamLoader(props.searchParams), ]); - const pagePath = `/team/${params.team_slug}/settings/invoices`; + const pagePath = `/team/${params.team_slug}/invoices`; const account = await getValidAccount(pagePath); diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/invoices/search-params.ts b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/billing/invoices/search-params.ts similarity index 100% rename from apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/invoices/search-params.ts rename to apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/billing/invoices/search-params.ts diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/billing/layout.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/billing/layout.tsx new file mode 100644 index 00000000000..733259536e5 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/billing/layout.tsx @@ -0,0 +1,56 @@ +import { redirect } from "next/navigation"; +import { getAuthToken } from "@/api/auth-token"; +import { getTeamBySlug } from "@/api/team"; +import { TabPathLinks } from "../../../../../../../@/components/ui/tabs"; +import { loginRedirect } from "../../../../../login/loginRedirect"; + +export default async function Layout(props: { + params: Promise<{ + team_slug: string; + }>; + children: React.ReactNode; +}) { + const params = await props.params; + + const [team, authToken] = await Promise.all([ + getTeamBySlug(params.team_slug), + getAuthToken(), + ]); + + if (!authToken) { + loginRedirect(`/team/${params.team_slug}/~/settings`); + } + + if (!team) { + redirect("/team"); + } + + return ( +
+
+
+

Billing

+
+
+ + + +
+ {props.children} +
+
+ ); +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/billing/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/billing/page.tsx similarity index 93% rename from apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/billing/page.tsx rename to apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/billing/page.tsx index 68814a6fed7..39f6eaf1863 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/billing/page.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/billing/page.tsx @@ -7,7 +7,7 @@ import { getTeamSubscriptions } from "@/api/team-subscription"; import { CreditsInfoCard } from "@/components/billing/PlanCard"; import { Coupons } from "@/components/billing/SubscriptionCoupons/Coupons"; import { getClientThirdwebClient } from "@/constants/thirdweb-client.client"; -import { getValidAccount } from "../../../../../../account/settings/getAccount"; +import { getValidAccount } from "../../../../../account/settings/getAccount"; import { CreditBalanceSection } from "./components/credit-balance-section.client"; import { PlanInfoCardClient } from "./components/PlanInfoCard.client"; @@ -24,7 +24,7 @@ export default async function Page(props: { props.params, props.searchParams, ]); - const pagePath = `/team/${params.team_slug}/settings/billing`; + const pagePath = `/team/${params.team_slug}/billing`; const account = await getValidAccount(pagePath); @@ -64,7 +64,7 @@ export default async function Page(props: { team.billingStatus === "validPayment" || team.billingStatus === "pastDue"; return ( -
+
- + scrollableClassName="container max-w-7xl" + /> +
{children} - +
); } diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/components/ecosystem-header.client.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/components/ecosystem-header.client.tsx index e7cd387631c..a09416faeee 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/components/ecosystem-header.client.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/components/ecosystem-header.client.tsx @@ -1,12 +1,5 @@ "use client"; -import { - AlertTriangleIcon, - CheckIcon, - ChevronsUpDownIcon, - ExternalLinkIcon, - PencilIcon, - PlusCircleIcon, -} from "lucide-react"; +import { AlertTriangleIcon, ExternalLinkIcon, PencilIcon } from "lucide-react"; import Link from "next/link"; import { useState } from "react"; import { toast } from "sonner"; @@ -25,14 +18,6 @@ import { DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; import { ImageUpload } from "@/components/ui/image-upload"; import { Input } from "@/components/ui/input"; import { Spinner } from "@/components/ui/Spinner/Spinner"; @@ -41,7 +26,6 @@ import { useDashboardStorageUpload } from "@/hooks/useDashboardStorageUpload"; import { useDashboardRouter } from "@/lib/DashboardRouter"; import { cn } from "@/lib/utils"; import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler"; -import { useEcosystemList } from "../../../hooks/use-ecosystem-list"; import type { Ecosystem } from "../../../types"; import { useUpdateEcosystem } from "../configuration/hooks/use-update-ecosystem"; import { useEcosystem } from "../hooks/use-ecosystem"; @@ -78,56 +62,6 @@ function EcosystemAlertBanner({ ecosystem }: { ecosystem: Ecosystem }) { } } -function EcosystemSelect(props: { - ecosystem: Ecosystem; - ecosystemLayoutPath: string; - teamIdOrSlug: string; -}) { - const { data: ecosystems, isPending } = useEcosystemList({ - teamIdOrSlug: props.teamIdOrSlug, - }); - - return isPending ? ( - - ) : ( - - - - - - - {ecosystems?.map((ecosystem) => ( - - - {ecosystem.slug === props.ecosystem.slug && ( - - )} -
{ecosystem.name}
- -
- ))} -
- - - - -
New Ecosystem
-
- -
-
- ); -} - export function EcosystemHeader(props: { ecosystem: Ecosystem; ecosystemLayoutPath: string; @@ -236,8 +170,8 @@ export function EcosystemHeader(props: { } return ( -
-
+
+
@@ -402,13 +336,6 @@ export function EcosystemHeader(props: { )}
-
- -
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/create/EcosystemCreatePage.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/create/EcosystemCreatePage.tsx index e6b9026b6c4..72aaa2e3f6f 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/create/EcosystemCreatePage.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/create/EcosystemCreatePage.tsx @@ -1,3 +1,4 @@ +import { UnderlineLink } from "@/components/ui/UnderlineLink"; import { CreateEcosystemForm } from "./components/client/create-ecosystem-form.client"; import { EcosystemWalletPricingCard } from "./components/pricing-card"; @@ -6,8 +7,29 @@ export async function EcosystemCreatePage(props: { teamId: string; }) { return ( -
-
+
+
+
+

+ Create Ecosystem +

+

+ Ecosystem Wallets enable your users to seamlessly access their + assets across various apps and games within your ecosystem. +
You can control which apps join your ecosystem and how their + users interact with your wallet.{" "} + + Learn more + +

+
+
+ +
-
+
); } diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/create/components/client/create-ecosystem-form.client.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/create/components/client/create-ecosystem-form.client.tsx index 49b0dbcfe61..b7e185cc29e 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/create/components/client/create-ecosystem-form.client.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/create/components/client/create-ecosystem-form.client.tsx @@ -1,7 +1,6 @@ "use client"; import { zodResolver } from "@hookform/resolvers/zod"; import { ArrowRightIcon } from "lucide-react"; -import Link from "next/link"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; @@ -20,6 +19,7 @@ import { ImageUpload } from "@/components/ui/image-upload"; import { Input } from "@/components/ui/input"; import { RadioGroup, RadioGroupItemButton } from "@/components/ui/radio-group"; import { Spinner } from "@/components/ui/Spinner/Spinner"; +import { UnderlineLink } from "@/components/ui/UnderlineLink"; import { createEcosystem } from "../../actions/create-ecosystem"; const formSchema = z.object({ @@ -87,15 +87,6 @@ export function CreateEcosystemForm(props: { })} >
-
-

- Create Ecosystem -

-

- Create wallets that work across every chain and every app -

-
- - Learn more about ecosystem permissions - + diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/create/loading.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/create/loading.tsx index 6c54ef15def..0528bd15ae9 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/create/loading.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/create/loading.tsx @@ -1,3 +1,7 @@ "use client"; -export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage"; +import { GenericLoadingPage } from "@/components/blocks/skeletons/GenericLoadingPage"; + +export default function Loading() { + return ; +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/hooks/use-ecosystem-list.ts b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/hooks/use-ecosystem-list.ts deleted file mode 100644 index b33dbaf0db7..00000000000 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/hooks/use-ecosystem-list.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { useActiveAccount } from "thirdweb/react"; -import { apiServerProxy } from "@/actions/proxies"; -import type { Ecosystem } from "../types"; - -export function useEcosystemList({ teamIdOrSlug }: { teamIdOrSlug: string }) { - const address = useActiveAccount()?.address; - return useQuery({ - queryFn: async () => { - const res = await apiServerProxy({ - method: "GET", - pathname: `/v1/teams/${teamIdOrSlug}/ecosystem-wallet`, - }); - - if (!res.ok) { - throw new Error(res.error ?? "Failed to fetch ecosystems"); - } - - const data = res.data as { result: Ecosystem[] }; - return data.result; - }, - queryKey: ["ecosystems", teamIdOrSlug, address], - retry: false, - }); -} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/page.tsx index 32a392ea36e..a3858f9555d 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/page.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/page.tsx @@ -1,11 +1,6 @@ -import { ArrowRightIcon, ExternalLinkIcon } from "lucide-react"; -import Image from "next/image"; -import Link from "next/link"; import { redirect } from "next/navigation"; -import { Button } from "@/components/ui/button"; -import { getAuthToken } from "../../../../../../../@/api/auth-token"; +import { getAuthToken } from "@/api/auth-token"; import { loginRedirect } from "../../../../../login/loginRedirect"; -import headerImage from "./assets/header.png"; import { fetchEcosystemList } from "./utils/fetchEcosystemList"; export default async function Page(props: { @@ -26,61 +21,10 @@ export default async function Page(props: { return []; }, ); + if (ecosystems[0]) { redirect(`${ecosystemLayoutPath}/${ecosystems[0].slug}`); } - return ; -} - -async function EcosystemLandingPage(props: { ecosystemLayoutPath: string }) { - return ( -
- {/* Card */} -
- Ecosystems - - {/* body */} -
-

- One wallet, a whole ecosystem of apps and games -

- -

- With Ecosystem Wallets, your users can access their assets across - hundreds of apps and games within your ecosystem. You can control - which apps join your ecosystem and how their users interact with - your wallet. -

-
- - {/* Footer */} -
- - - -
-
-
- ); + redirect(`${ecosystemLayoutPath}/create`); } diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/SettingsLayout.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/SettingsLayout.tsx deleted file mode 100644 index bbefc964c44..00000000000 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/SettingsLayout.tsx +++ /dev/null @@ -1,68 +0,0 @@ -"use client"; - -import { usePathname } from "next/navigation"; -import { useState } from "react"; -import type { ThirdwebClient } from "thirdweb"; -import type { Team } from "@/api/team"; -import type { Account } from "@/hooks/useApi"; -import { cn } from "@/lib/utils"; -import { getTeamSettingsLinks } from "./_components/sidebar/getTeamSettingsLinks"; -import { TeamSettingsSidebar } from "./_components/sidebar/TeamSettingsSidebar"; -import { TeamSettingsMobileNav } from "./_components/sidebar/TeamsMobileNav"; - -// on the /~/settings page -// - On desktop: show the general settings as usual -// - On mobile: show the full nav instead of page content and when user clicks on the "General Settings" ( first link ) - hide the full nav and show the page content - -export function SettingsLayout(props: { - team: Team; - children: React.ReactNode; - account: Account; - client: ThirdwebClient; -}) { - const [_showFullNavOnMobile, setShowFullNavOnMobile] = useState(true); - const pathname = usePathname(); - const isSettingsOverview = (pathname || "").endsWith("/~/settings"); - const showFullNavOnMobile = _showFullNavOnMobile && isSettingsOverview; - const links = getTeamSettingsLinks(props.team.slug); - const activeLink = links.find((link) => pathname === link.href); - - return ( -
- {/* Huge page title */} -
-
-

- Team Settings -

-
-
- -
- -
- -
- -
- {props.children} -
-
-
- ); -} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/_components/settings-cards/dedicated-support.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/_components/settings-cards/dedicated-support.tsx index dc3981b6f59..78b86ec7663 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/_components/settings-cards/dedicated-support.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/_components/settings-cards/dedicated-support.tsx @@ -131,7 +131,7 @@ export function TeamDedicatedSupportCard(props: { label: "Upgrade Plan", onClick: () => router.push( - `/team/${props.team.slug}/~/settings/billing?showPlans=true&highlight=scale`, + `/team/${props.team.slug}/~/billing?showPlans=true&highlight=scale`, ), } } diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/_components/sidebar/TeamsMobileNav.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/_components/sidebar/TeamsMobileNav.tsx index caeeea0fd79..909778ab094 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/_components/sidebar/TeamsMobileNav.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/_components/sidebar/TeamsMobileNav.tsx @@ -13,7 +13,7 @@ export function TeamSettingsMobileNav(props: { if (!showFull) { return ( -
+
{ - return ( -
-
-
-

- Credits -

-

- Apply to the Optimism Superchain App Accelerator.{" "} - - Learn More - -

-
-
-
- -
- ); -}; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/credits/components/ApplyForOpCreditsForm.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/credits/components/ApplyForOpCreditsForm.tsx deleted file mode 100644 index 478cd6dc9b0..00000000000 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/credits/components/ApplyForOpCreditsForm.tsx +++ /dev/null @@ -1,216 +0,0 @@ -import { Flex, FormControl } from "@chakra-ui/react"; -import { FormHelperText, FormLabel } from "chakra/form"; -import { Select as ChakraSelect } from "chakra-react-select"; -import { useMemo } from "react"; -import { useForm } from "react-hook-form"; -import type { Team } from "@/api/team"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Textarea } from "@/components/ui/textarea"; -import type { Account } from "@/hooks/useApi"; -import { useLocalStorage } from "@/hooks/useLocalStorage"; -import { useTxNotifications } from "@/hooks/useTxNotifications"; -import { PlanToCreditsRecord } from "./ApplyForOpCreditsModal"; -import { applyOpSponsorship } from "./applyOpSponsorship"; - -interface FormSchema { - firstname: string; - lastname: string; - thirdweb_account_id: string; - plan_type: string; - email: string; - company: string; - website: string; - twitterhandle: string; - superchain_verticals: string; - superchain_chain: string; - what_would_you_like_to_meet_about_: string; -} - -interface ApplyForOpCreditsFormProps { - onClose: () => void; - plan: Team["billingPlan"]; - account: Account; -} - -export const ApplyForOpCreditsForm: React.FC = ({ - onClose, - account, - plan, -}) => { - const [, setHasAppliedForOpGrant] = useLocalStorage( - `appliedForOpGrant-${account?.id}`, - false, - ); - const transformedQueryData = useMemo( - () => ({ - company: "", - email: account?.email || "", - firstname: "", - lastname: "", - plan_type: PlanToCreditsRecord[plan].plan, - superchain_chain: "", - superchain_verticals: "", - thirdweb_account_id: account?.id || "", - twitterhandle: "", - website: "", - what_would_you_like_to_meet_about_: "", - }), - [account, plan], - ); - - const form = useForm({ - defaultValues: transformedQueryData, - values: transformedQueryData, - }); - - const { onSuccess, onError } = useTxNotifications( - "We have received your application and will notify you if you are selected.", - "Something went wrong, please try again.", - ); - - return ( - { - const fields = Object.keys(data).map((key) => ({ - name: key, - // biome-ignore lint/suspicious/noExplicitAny: FIXME - value: (data as any)[key], - })); - - try { - const response = await applyOpSponsorship({ - fields, - }); - - if (!response.ok) { - throw new Error("Form submission failed"); - } - - onSuccess(); - onClose(); - setHasAppliedForOpGrant(true); - - form.reset(); - } catch (error) { - onError(error); - } - })} - > - - - - First Name - - - - Last Name - - - - - - Company Name - - - - - Company Website - - URL should start with https:// - - - Company Social Account - - URL should start with https:// - - - Industry - { - if (value?.value) { - form.setValue("superchain_verticals", value.value); - } - }} - options={[ - "DAOs", - "Education & Community", - "Fandom & Rewards", - "Gaming & Metaverse", - "Infra & Dev Tools", - "NFTs", - "Payments & Finance (DeFi)", - "Security & Identity", - "Social", - "Other", - ].map((vertical) => ({ - label: vertical, - value: - vertical === "Payments & Finance (DeFi)" ? "DeFi" : vertical, - }))} - placeholder="Select industry" - /> - - - Chain - { - form.setValue( - "superchain_chain", - values.map(({ value }) => value).join(";"), - ); - }} - options={[ - "Optimism", - "Base", - "Zora", - "Mode", - "Frax", - "Cyber", - "Redstone", - "Ancient8", - "Donatuz", - "Mantle", - "Soneium", - "Lisk", - "Arena-Z", - "Superseed", - "Ink", - ].map((chain) => ({ - label: chain === "Optimism" ? "OP Mainnet" : chain, - value: chain, - }))} - placeholder="Select chains" - selectedOptionStyle="check" - /> - - - Tell us more about your project -