refactor: rename navigation function and update sidebar component

- Renamed `getNavMainLinks` to `getSidebarLinks` for improved clarity in the configuration file.
- Updated the `DashboardSidebar` component to utilize the new `getSidebarLinks` function and integrated `useCurrentUser` for user status handling.
- Enhanced the sidebar to conditionally display the upgrade card and user profile based on membership status.
- Updated comments for better understanding of the code functionality.
This commit is contained in:
javayhu 2025-04-05 08:18:55 +08:00
parent b5699b85f1
commit 9dbe79593e
7 changed files with 36 additions and 21 deletions

View File

@ -11,20 +11,22 @@ import {
SidebarMenuButton, SidebarMenuButton,
SidebarMenuItem SidebarMenuItem
} from '@/components/ui/sidebar'; } from '@/components/ui/sidebar';
import { getNavMainLinks } from '@/config'; import { getSidebarLinks } from '@/config';
import { useCurrentUser } from '@/hooks/use-current-user';
import { LocaleLink } from '@/i18n/navigation'; import { LocaleLink } from '@/i18n/navigation';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import * as React from 'react'; import * as React from 'react';
import { Logo } from '../logo'; import { Logo } from '../logo';
import { SidebarUpgradeCard } from './sidebar-upgrade-card'; import { SidebarUpgradeCard } from './sidebar-upgrade-card';
import { authClient } from '@/lib/auth-client';
export function DashboardSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) { export function DashboardSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
const t = useTranslations(); const t = useTranslations();
const mainLinks = getNavMainLinks(); const sidebarLinks = getSidebarLinks();
const currentUser = useCurrentUser();
const { data: session, error } = authClient.useSession(); // user is a member if they have a lifetime membership or an active subscription
const user = session?.user; const isMember = currentUser?.lifetimeMember ||
(currentUser?.subscriptionId && currentUser?.subscriptionStatus === 'active');
return ( return (
<Sidebar collapsible="icon" {...props}> <Sidebar collapsible="icon" {...props}>
@ -47,14 +49,15 @@ export function DashboardSidebar({ ...props }: React.ComponentProps<typeof Sideb
</SidebarHeader> </SidebarHeader>
<SidebarContent> <SidebarContent>
<NavMain items={mainLinks} /> <NavMain items={sidebarLinks} />
</SidebarContent> </SidebarContent>
<SidebarFooter className="flex flex-col gap-4"> <SidebarFooter className="flex flex-col gap-4">
{/* TODO: show or hide based on user status */} {/* show upgrade card if user is not a member */}
<SidebarUpgradeCard /> {!isMember && <SidebarUpgradeCard />}
{user && <NavUser user={user} />} {/* show user profile if user is logged in */}
{currentUser && <NavUser user={currentUser} />}
</SidebarFooter> </SidebarFooter>
</Sidebar> </Sidebar>
); );

View File

@ -20,7 +20,7 @@ import { useEffect, useTransition } from 'react';
* *
* Allows users to switch between available locales using a dropdown menu. * Allows users to switch between available locales using a dropdown menu.
* *
* Based on next-intl's useLocaleRouter and usePathname for locale navigation. * Based on next-intl's useLocaleRouter and useLocalePathname for locale navigation.
* https://next-intl.dev/docs/routing/navigation#userouter * https://next-intl.dev/docs/routing/navigation#userouter
*/ */
export default function LocaleSwitcher() { export default function LocaleSwitcher() {

View File

@ -93,12 +93,15 @@ export function PricingCard({
className className
)} )}
> >
{/* show popular badge if plan is recommended */}
{plan.recommended && ( {plan.recommended && (
<span className="absolute inset-x-0 -top-3 mx-auto flex h-6 w-fit items-center rounded-full px-3 py-1 text-xs font-medium border <span className="absolute inset-x-0 -top-3 mx-auto flex h-6 w-fit items-center rounded-full px-3 py-1 text-xs font-medium border
bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200 border-purple-200 dark:border-purple-800 shadow-sm"> bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200 border-purple-200 dark:border-purple-800 shadow-sm">
{t('popular')} {t('popular')}
</span> </span>
)} )}
{/* show current plan badge if plan is current plan */}
{isCurrentPlan && ( {isCurrentPlan && (
<span className="absolute inset-x-0 -top-3 mx-auto flex h-6 w-fit items-center rounded-full px-3 py-1 text-xs font-medium border <span className="absolute inset-x-0 -top-3 mx-auto flex h-6 w-fit items-center rounded-full px-3 py-1 text-xs font-medium border
bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-100 border-blue-200 dark:border-blue-800 shadow-sm"> bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-100 border-blue-200 dark:border-blue-800 shadow-sm">
@ -109,6 +112,7 @@ export function PricingCard({
<CardHeader> <CardHeader>
<CardTitle className="font-medium">{plan.name}</CardTitle> <CardTitle className="font-medium">{plan.name}</CardTitle>
{/* show price and price label */}
<div className="flex items-baseline gap-2"> <div className="flex items-baseline gap-2">
<span className="my-4 block text-4xl font-semibold"> <span className="my-4 block text-4xl font-semibold">
{formattedPrice} {formattedPrice}
@ -118,6 +122,7 @@ export function PricingCard({
<CardDescription className="text-sm">{plan.description}</CardDescription> <CardDescription className="text-sm">{plan.description}</CardDescription>
{/* show action buttons based on plans */}
{plan.isFree ? ( {plan.isFree ? (
currentUser ? ( currentUser ? (
<Button variant="outline" className="mt-4 w-full disabled"> <Button variant="outline" className="mt-4 w-full disabled">
@ -162,6 +167,7 @@ export function PricingCard({
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<hr className="border-dashed" /> <hr className="border-dashed" />
{/* show trial period if it exists */}
{hasTrialPeriod && ( {hasTrialPeriod && (
<div className="my-4"> <div className="my-4">
<span className="inline-block px-2.5 py-1.5 text-xs font-medium rounded-md <span className="inline-block px-2.5 py-1.5 text-xs font-medium rounded-md
@ -171,6 +177,7 @@ export function PricingCard({
</div> </div>
)} )}
{/* show features of this plan */}
<ul className="list-outside space-y-4 text-sm"> <ul className="list-outside space-y-4 text-sm">
{plan.features.map((feature, i) => ( {plan.features.map((feature, i) => (
<li key={i} className="flex items-center gap-2"> <li key={i} className="flex items-center gap-2">

View File

@ -1,10 +1,9 @@
'use client'; 'use client';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { useLocaleRouter } from '@/i18n/navigation'; import { LocaleLink, useLocaleRouter } from '@/i18n/navigation';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { ArrowLeftIcon } from 'lucide-react'; import { ArrowLeftIcon } from 'lucide-react';
import Link from 'next/link';
interface BackButtonSmallProps { interface BackButtonSmallProps {
href?: string; href?: string;
@ -29,9 +28,9 @@ export default function BackButtonSmall({
asChild asChild
> >
{/* if href is provided, use it, otherwise use the router.back() */} {/* if href is provided, use it, otherwise use the router.back() */}
<Link href={href || '#'} onClick={handleBack}> <LocaleLink href={href || '#'} onClick={handleBack}>
<ArrowLeftIcon className="size-4" /> <ArrowLeftIcon className="size-4" />
</Link> </LocaleLink>
</Button> </Button>
); );
} }

View File

@ -425,14 +425,14 @@ export function getAvatarLinks(): MenuItem[] {
} }
/** /**
* Get sidebar navigation main links with translations * Get sidebar navigation links with translations
* *
* NOTICE: used in client components only * NOTICE: used in client components only
* *
* @param t - The translation function * @param t - The translation function
* @returns The menu links with translated titles and descriptions * @returns The menu links with translated titles and descriptions
*/ */
export function getNavMainLinks(): NestedMenuItem[] { export function getSidebarLinks(): NestedMenuItem[] {
const t = useTranslations(); const t = useTranslations();
return [ return [

View File

@ -14,9 +14,9 @@ export const user = pgTable("user", {
banReason: text('ban_reason'), banReason: text('ban_reason'),
banExpires: timestamp('ban_expires'), banExpires: timestamp('ban_expires'),
customerId: text('customer_id'), customerId: text('customer_id'),
lifetimeMember: boolean('lifetime_member'),
subscriptionId: text('subscription_id'), subscriptionId: text('subscription_id'),
subscriptionStatus: text('subscription_status'), subscriptionStatus: text('subscription_status'),
lifetimeMember: boolean('lifetime_member')
}); });
export const session = pgTable("session", { export const session = pgTable("session", {

View File

@ -116,17 +116,23 @@ export const auth = betterAuth({
defaultValue: "", defaultValue: "",
input: false, // don't allow user to set customerId input: false, // don't allow user to set customerId
}, },
lifetimeMember: {
type: "boolean",
required: false,
defaultValue: false,
input: false, // don't allow user to set lifetimeMember
},
subscriptionId: { subscriptionId: {
type: "string", type: "string",
required: false, required: false,
defaultValue: "", defaultValue: "",
input: false, // don't allow user to set subscriptionId input: false, // don't allow user to set subscriptionId
}, },
lifetimeMember: { subscriptionStatus: {
type: "boolean", type: "string",
required: false, required: false,
defaultValue: false, defaultValue: "",
input: false, // don't allow user to set lifetimeMember input: false, // don't allow user to set subscriptionStatus
}, },
}, },
// https://www.better-auth.com/docs/concepts/users-accounts#delete-user // https://www.better-auth.com/docs/concepts/users-accounts#delete-user