refator: biome lint part 2

This commit is contained in:
javayhu 2025-04-18 21:53:21 +08:00
parent 23cd59bbac
commit 27bc59354f
27 changed files with 377 additions and 390 deletions

View File

@ -22,6 +22,8 @@
"src/components/magicui/*.tsx", "src/components/magicui/*.tsx",
"src/app/[[]locale]/preview/**", "src/app/[[]locale]/preview/**",
"src/db/schema.ts", "src/db/schema.ts",
"src/payment/types.ts",
"src/types/index.d.ts",
"public/sw.js" "public/sw.js"
] ]
}, },
@ -78,4 +80,4 @@
"semicolons": "always" "semicolons": "always"
} }
} }
} }

View File

@ -1,4 +1,4 @@
import path from 'path'; import path from 'node:path';
import { DEFAULT_LOCALE, LOCALES } from '@/i18n/routing'; import { DEFAULT_LOCALE, LOCALES } from '@/i18n/routing';
import { defineCollection, defineConfig } from '@content-collections/core'; import { defineCollection, defineConfig } from '@content-collections/core';
import { import {
@ -313,14 +313,14 @@ function extractLocaleAndBase(fileName: string): {
if (parts.length === 1) { if (parts.length === 1) {
// Simple filename without locale: xxx // Simple filename without locale: xxx
return { locale: DEFAULT_LOCALE, base: parts[0] }; return { locale: DEFAULT_LOCALE, base: parts[0] };
} else if (parts.length === 2 && LOCALES.includes(parts[1])) { }
if (parts.length === 2 && LOCALES.includes(parts[1])) {
// Filename with locale: xxx.zh // Filename with locale: xxx.zh
return { locale: parts[1], base: parts[0] }; return { locale: parts[1], base: parts[0] };
} else {
// Unexpected format, use first part as base and default locale
console.warn(`Unexpected filename format: ${fileName}`);
return { locale: DEFAULT_LOCALE, base: parts[0] };
} }
// Unexpected format, use first part as base and default locale
console.warn(`Unexpected filename format: ${fileName}`);
return { locale: DEFAULT_LOCALE, base: parts[0] };
} }
export default defineConfig({ export default defineConfig({

View File

@ -7,6 +7,7 @@
"build": "content-collections build && next build", "build": "content-collections build && next build",
"start": "next start", "start": "next start",
"lint": "biome check --write .", "lint": "biome check --write .",
"lint:fix": "biome check --fix --unsafe .",
"format": "biome format --write .", "format": "biome format --write .",
"db:generate": "drizzle-kit generate", "db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate", "db:migrate": "drizzle-kit migrate",

View File

@ -1,4 +1,4 @@
import { randomUUID } from 'crypto'; import { randomUUID } from 'node:crypto';
import { getPresignedUploadUrl } from '@/storage'; import { getPresignedUploadUrl } from '@/storage';
import { StorageError } from '@/storage/types'; import { StorageError } from '@/storage/types';
import { type NextRequest, NextResponse } from 'next/server'; import { type NextRequest, NextResponse } from 'next/server';

View File

@ -48,7 +48,7 @@ export default function Features2Section() {
return ( return (
<section className="py-16"> <section className="py-16">
<div className="bg-linear-to-b absolute inset-0 -z-10 sm:inset-6 sm:rounded-b-3xl dark:block dark:to-[color-mix(in_oklab,var(--color-zinc-900)_75%,var(--color-background))]"></div> <div className="bg-linear-to-b absolute inset-0 -z-10 sm:inset-6 sm:rounded-b-3xl dark:block dark:to-[color-mix(in_oklab,var(--color-zinc-900)_75%,var(--color-background))]" />
<div className="mx-auto max-w-6xl space-y-8 px-6 md:space-y-16 lg:space-y-20 dark:[--color-border:color-mix(in_oklab,var(--color-white)_10%,transparent)]"> <div className="mx-auto max-w-6xl space-y-8 px-6 md:space-y-16 lg:space-y-20 dark:[--color-border:color-mix(in_oklab,var(--color-white)_10%,transparent)]">
<div className="relative z-10 mx-auto max-w-2xl space-y-6 text-center"> <div className="relative z-10 mx-auto max-w-2xl space-y-6 text-center">
<h2 className="text-balance text-4xl lg:text-5xl font-semibold"> <h2 className="text-balance text-4xl lg:text-5xl font-semibold">

View File

@ -21,7 +21,7 @@ export default function Integration2Section() {
<div className="mx-auto max-w-5xl px-6"> <div className="mx-auto max-w-5xl px-6">
<div className="grid items-center sm:grid-cols-2"> <div className="grid items-center sm:grid-cols-2">
<div className="dark:bg-muted/50 relative mx-auto w-fit"> <div className="dark:bg-muted/50 relative mx-auto w-fit">
<div className="bg-radial to-muted dark:to-background absolute inset-0 z-10 from-transparent to-75%"></div> <div className="bg-radial to-muted dark:to-background absolute inset-0 z-10 from-transparent to-75%" />
<div className="mx-auto mb-2 flex w-fit justify-center gap-2"> <div className="mx-auto mb-2 flex w-fit justify-center gap-2">
<IntegrationCard> <IntegrationCard>
<Gemini /> <Gemini />

View File

@ -127,7 +127,7 @@ export default function TestimonialsSection() {
width="120" width="120"
height="120" height="120"
/> />
<AvatarFallback></AvatarFallback> <AvatarFallback />
</Avatar> </Avatar>
<div> <div>

View File

@ -69,7 +69,7 @@ export function ContactFormCard() {
// Submit form data using the contact server action // Submit form data using the contact server action
const result = await sendMessageAction(values); const result = await sendMessageAction(values);
if (result && result.data?.success) { if (result?.data?.success) {
toast.success(t('success')); toast.success(t('success'));
form.reset(); form.reset();
} else { } else {

View File

@ -607,16 +607,16 @@ export function DataTable({
value="past-performance" value="past-performance"
className="flex flex-col px-4 lg:px-6" className="flex flex-col px-4 lg:px-6"
> >
<div className="aspect-video w-full flex-1 rounded-lg border border-dashed"></div> <div className="aspect-video w-full flex-1 rounded-lg border border-dashed" />
</TabsContent> </TabsContent>
<TabsContent value="key-personnel" className="flex flex-col px-4 lg:px-6"> <TabsContent value="key-personnel" className="flex flex-col px-4 lg:px-6">
<div className="aspect-video w-full flex-1 rounded-lg border border-dashed"></div> <div className="aspect-video w-full flex-1 rounded-lg border border-dashed" />
</TabsContent> </TabsContent>
<TabsContent <TabsContent
value="focus-documents" value="focus-documents"
className="flex flex-col px-4 lg:px-6" className="flex flex-col px-4 lg:px-6"
> >
<div className="aspect-video w-full flex-1 rounded-lg border border-dashed"></div> <div className="aspect-video w-full flex-1 rounded-lg border border-dashed" />
</TabsContent> </TabsContent>
</Tabs> </Tabs>
); );

View File

@ -16,7 +16,7 @@ export function DiscordIcon(props: SVGProps<SVGSVGElement>) {
<path <path
fill="currentColor" fill="currentColor"
d="M524.531 69.836a1.5 1.5 0 0 0-.764-.7A485 485 0 0 0 404.081 32.03a1.82 1.82 0 0 0-1.923.91a338 338 0 0 0-14.9 30.6a447.9 447.9 0 0 0-134.426 0a310 310 0 0 0-15.135-30.6a1.89 1.89 0 0 0-1.924-.91a483.7 483.7 0 0 0-119.688 37.107a1.7 1.7 0 0 0-.788.676C39.068 183.651 18.186 294.69 28.43 404.354a2.02 2.02 0 0 0 .765 1.375a487.7 487.7 0 0 0 146.825 74.189a1.9 1.9 0 0 0 2.063-.676A348 348 0 0 0 208.12 430.4a1.86 1.86 0 0 0-1.019-2.588a321 321 0 0 1-45.868-21.853a1.885 1.885 0 0 1-.185-3.126a251 251 0 0 0 9.109-7.137a1.82 1.82 0 0 1 1.9-.256c96.229 43.917 200.41 43.917 295.5 0a1.81 1.81 0 0 1 1.924.233a235 235 0 0 0 9.132 7.16a1.884 1.884 0 0 1-.162 3.126a301.4 301.4 0 0 1-45.89 21.83a1.875 1.875 0 0 0-1 2.611a391 391 0 0 0 30.014 48.815a1.86 1.86 0 0 0 2.063.7A486 486 0 0 0 610.7 405.729a1.88 1.88 0 0 0 .765-1.352c12.264-126.783-20.532-236.912-86.934-334.541M222.491 337.58c-28.972 0-52.844-26.587-52.844-59.239s23.409-59.241 52.844-59.241c29.665 0 53.306 26.82 52.843 59.239c0 32.654-23.41 59.241-52.843 59.241m195.38 0c-28.971 0-52.843-26.587-52.843-59.239s23.409-59.241 52.843-59.241c29.667 0 53.307 26.82 52.844 59.239c0 32.654-23.177 59.241-52.844 59.241" d="M524.531 69.836a1.5 1.5 0 0 0-.764-.7A485 485 0 0 0 404.081 32.03a1.82 1.82 0 0 0-1.923.91a338 338 0 0 0-14.9 30.6a447.9 447.9 0 0 0-134.426 0a310 310 0 0 0-15.135-30.6a1.89 1.89 0 0 0-1.924-.91a483.7 483.7 0 0 0-119.688 37.107a1.7 1.7 0 0 0-.788.676C39.068 183.651 18.186 294.69 28.43 404.354a2.02 2.02 0 0 0 .765 1.375a487.7 487.7 0 0 0 146.825 74.189a1.9 1.9 0 0 0 2.063-.676A348 348 0 0 0 208.12 430.4a1.86 1.86 0 0 0-1.019-2.588a321 321 0 0 1-45.868-21.853a1.885 1.885 0 0 1-.185-3.126a251 251 0 0 0 9.109-7.137a1.82 1.82 0 0 1 1.9-.256c96.229 43.917 200.41 43.917 295.5 0a1.81 1.81 0 0 1 1.924.233a235 235 0 0 0 9.132 7.16a1.884 1.884 0 0 1-.162 3.126a301.4 301.4 0 0 1-45.89 21.83a1.875 1.875 0 0 0-1 2.611a391 391 0 0 0 30.014 48.815a1.86 1.86 0 0 0 2.063.7A486 486 0 0 0 610.7 405.729a1.88 1.88 0 0 0 .765-1.352c12.264-126.783-20.532-236.912-86.934-334.541M222.491 337.58c-28.972 0-52.844-26.587-52.844-59.239s23.409-59.241 52.844-59.241c29.665 0 53.306 26.82 52.843 59.239c0 32.654-23.41 59.241-52.843 59.241m195.38 0c-28.971 0-52.843-26.587-52.843-59.239s23.409-59.241 52.843-59.241c29.667 0 53.307 26.82 52.844 59.239c0 32.654-23.177 59.241-52.844 59.241"
></path> />
</svg> </svg>
); );
} }

View File

@ -16,7 +16,7 @@ export function FacebookIcon(props: SVGProps<SVGSVGElement>) {
<path <path
fill="currentColor" fill="currentColor"
d="M512 256C512 114.6 397.4 0 256 0S0 114.6 0 256c0 120 82.7 220.8 194.2 248.5V334.2h-52.8V256h52.8v-33.7c0-87.1 39.4-127.5 125-127.5c16.2 0 44.2 3.2 55.7 6.4V172c-6-.6-16.5-1-29.6-1c-42 0-58.2 15.9-58.2 57.2V256h83.6l-14.4 78.2H287v175.9C413.8 494.8 512 386.9 512 256" d="M512 256C512 114.6 397.4 0 256 0S0 114.6 0 256c0 120 82.7 220.8 194.2 248.5V334.2h-52.8V256h52.8v-33.7c0-87.1 39.4-127.5 125-127.5c16.2 0 44.2 3.2 55.7 6.4V172c-6-.6-16.5-1-29.6-1c-42 0-58.2 15.9-58.2 57.2V256h83.6l-14.4 78.2H287v175.9C413.8 494.8 512 386.9 512 256"
></path> />
</svg> </svg>
); );
} }

View File

@ -16,7 +16,7 @@ export function InstagramIcon(props: SVGProps<SVGSVGElement>) {
<path <path
fill="currentColor" fill="currentColor"
d="M224.1 141c-63.6 0-114.9 51.3-114.9 114.9s51.3 114.9 114.9 114.9S339 319.5 339 255.9S287.7 141 224.1 141m0 189.6c-41.1 0-74.7-33.5-74.7-74.7s33.5-74.7 74.7-74.7s74.7 33.5 74.7 74.7s-33.6 74.7-74.7 74.7m146.4-194.3c0 14.9-12 26.8-26.8 26.8c-14.9 0-26.8-12-26.8-26.8s12-26.8 26.8-26.8s26.8 12 26.8 26.8m76.1 27.2c-1.7-35.9-9.9-67.7-36.2-93.9c-26.2-26.2-58-34.4-93.9-36.2c-37-2.1-147.9-2.1-184.9 0c-35.8 1.7-67.6 9.9-93.9 36.1s-34.4 58-36.2 93.9c-2.1 37-2.1 147.9 0 184.9c1.7 35.9 9.9 67.7 36.2 93.9s58 34.4 93.9 36.2c37 2.1 147.9 2.1 184.9 0c35.9-1.7 67.7-9.9 93.9-36.2c26.2-26.2 34.4-58 36.2-93.9c2.1-37 2.1-147.8 0-184.8M398.8 388c-7.8 19.6-22.9 34.7-42.6 42.6c-29.5 11.7-99.5 9-132.1 9s-102.7 2.6-132.1-9c-19.6-7.8-34.7-22.9-42.6-42.6c-11.7-29.5-9-99.5-9-132.1s-2.6-102.7 9-132.1c7.8-19.6 22.9-34.7 42.6-42.6c29.5-11.7 99.5-9 132.1-9s102.7-2.6 132.1 9c19.6 7.8 34.7 22.9 42.6 42.6c11.7 29.5 9 99.5 9 132.1s2.7 102.7-9 132.1" d="M224.1 141c-63.6 0-114.9 51.3-114.9 114.9s51.3 114.9 114.9 114.9S339 319.5 339 255.9S287.7 141 224.1 141m0 189.6c-41.1 0-74.7-33.5-74.7-74.7s33.5-74.7 74.7-74.7s74.7 33.5 74.7 74.7s-33.6 74.7-74.7 74.7m146.4-194.3c0 14.9-12 26.8-26.8 26.8c-14.9 0-26.8-12-26.8-26.8s12-26.8 26.8-26.8s26.8 12 26.8 26.8m76.1 27.2c-1.7-35.9-9.9-67.7-36.2-93.9c-26.2-26.2-58-34.4-93.9-36.2c-37-2.1-147.9-2.1-184.9 0c-35.8 1.7-67.6 9.9-93.9 36.1s-34.4 58-36.2 93.9c-2.1 37-2.1 147.9 0 184.9c1.7 35.9 9.9 67.7 36.2 93.9s58 34.4 93.9 36.2c37 2.1 147.9 2.1 184.9 0c35.9-1.7 67.7-9.9 93.9-36.2c26.2-26.2 34.4-58 36.2-93.9c2.1-37 2.1-147.8 0-184.8M398.8 388c-7.8 19.6-22.9 34.7-42.6 42.6c-29.5 11.7-99.5 9-132.1 9s-102.7 2.6-132.1-9c-19.6-7.8-34.7-22.9-42.6-42.6c-11.7-29.5-9-99.5-9-132.1s-2.6-102.7 9-132.1c7.8-19.6 22.9-34.7 42.6-42.6c29.5-11.7 99.5-9 132.1-9s102.7-2.6 132.1 9c19.6 7.8 34.7 22.9 42.6 42.6c11.7 29.5 9 99.5 9 132.1s2.7 102.7-9 132.1"
></path> />
</svg> </svg>
); );
} }

View File

@ -16,7 +16,7 @@ export function LinkedInIcon(props: SVGProps<SVGSVGElement>) {
<path <path
fill="currentColor" fill="currentColor"
d="M416 32H31.9C14.3 32 0 46.5 0 64.3v383.4C0 465.5 14.3 480 31.9 480H416c17.6 0 32-14.5 32-32.3V64.3c0-17.8-14.4-32.3-32-32.3M135.4 416H69V202.2h66.5V416zm-33.2-243c-21.3 0-38.5-17.3-38.5-38.5S80.9 96 102.2 96c21.2 0 38.5 17.3 38.5 38.5c0 21.3-17.2 38.5-38.5 38.5m282.1 243h-66.4V312c0-24.8-.5-56.7-34.5-56.7c-34.6 0-39.9 27-39.9 54.9V416h-66.4V202.2h63.7v29.2h.9c8.9-16.8 30.6-34.5 62.9-34.5c67.2 0 79.7 44.3 79.7 101.9z" d="M416 32H31.9C14.3 32 0 46.5 0 64.3v383.4C0 465.5 14.3 480 31.9 480H416c17.6 0 32-14.5 32-32.3V64.3c0-17.8-14.4-32.3-32-32.3M135.4 416H69V202.2h66.5V416zm-33.2-243c-21.3 0-38.5-17.3-38.5-38.5S80.9 96 102.2 96c21.2 0 38.5 17.3 38.5 38.5c0 21.3-17.2 38.5-38.5 38.5m282.1 243h-66.4V312c0-24.8-.5-56.7-34.5-56.7c-34.6 0-39.9 27-39.9 54.9V416h-66.4V202.2h63.7v29.2h.9c8.9-16.8 30.6-34.5 62.9-34.5c67.2 0 79.7 44.3 79.7 101.9z"
></path> />
</svg> </svg>
); );
} }

View File

@ -16,7 +16,7 @@ export function TikTokIcon(props: SVGProps<SVGSVGElement>) {
<path <path
fill="currentColor" fill="currentColor"
d="M448 209.91a210.06 210.06 0 0 1-122.77-39.25v178.72A162.55 162.55 0 1 1 185 188.31v89.89a74.62 74.62 0 1 0 52.23 71.18V0h88a121 121 0 0 0 1.86 22.17A122.18 122.18 0 0 0 381 102.39a121.43 121.43 0 0 0 67 20.14Z" d="M448 209.91a210.06 210.06 0 0 1-122.77-39.25v178.72A162.55 162.55 0 1 1 185 188.31v89.89a74.62 74.62 0 1 0 52.23 71.18V0h88a121 121 0 0 0 1.86 22.17A122.18 122.18 0 0 0 381 102.39a121.43 121.43 0 0 0 67 20.14Z"
></path> />
</svg> </svg>
); );
} }

View File

@ -16,7 +16,7 @@ export function TwitterIcon(props: SVGProps<SVGSVGElement>) {
<path <path
fill="currentColor" fill="currentColor"
d="M459.37 151.716c.325 4.548.325 9.097.325 13.645c0 138.72-105.583 298.558-298.558 298.558c-59.452 0-114.68-17.219-161.137-47.106c8.447.974 16.568 1.299 25.34 1.299c49.055 0 94.213-16.568 130.274-44.832c-46.132-.975-84.792-31.188-98.112-72.772c6.498.974 12.995 1.624 19.818 1.624c9.421 0 18.843-1.3 27.614-3.573c-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319c-28.264-18.843-46.781-51.005-46.781-87.391c0-19.492 5.197-37.36 14.294-52.954c51.655 63.675 129.3 105.258 216.365 109.807c-1.624-7.797-2.599-15.918-2.599-24.04c0-57.828 46.782-104.934 104.934-104.934c30.213 0 57.502 12.67 76.67 33.137c23.715-4.548 46.456-13.32 66.599-25.34c-7.798 24.366-24.366 44.833-46.132 57.827c21.117-2.273 41.584-8.122 60.426-16.243c-14.292 20.791-32.161 39.308-52.628 54.253" d="M459.37 151.716c.325 4.548.325 9.097.325 13.645c0 138.72-105.583 298.558-298.558 298.558c-59.452 0-114.68-17.219-161.137-47.106c8.447.974 16.568 1.299 25.34 1.299c49.055 0 94.213-16.568 130.274-44.832c-46.132-.975-84.792-31.188-98.112-72.772c6.498.974 12.995 1.624 19.818 1.624c9.421 0 18.843-1.3 27.614-3.573c-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319c-28.264-18.843-46.781-51.005-46.781-87.391c0-19.492 5.197-37.36 14.294-52.954c51.655 63.675 129.3 105.258 216.365 109.807c-1.624-7.797-2.599-15.918-2.599-24.04c0-57.828 46.782-104.934 104.934-104.934c30.213 0 57.502 12.67 76.67 33.137c23.715-4.548 46.456-13.32 66.599-25.34c-7.798 24.366-24.366 44.833-46.132 57.827c21.117-2.273 41.584-8.122 60.426-16.243c-14.292 20.791-32.161 39.308-52.628 54.253"
></path> />
</svg> </svg>
); );
} }

View File

@ -39,21 +39,20 @@ export function Footer({ className }: React.HTMLAttributes<HTMLElement>) {
{/* social links */} {/* social links */}
<div className="flex items-center gap-4 py-2"> <div className="flex items-center gap-4 py-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{socialLinks && {socialLinks?.map((link) => (
socialLinks.map((link) => ( <a
<a key={link.title}
key={link.title} href={link.href || '#'}
href={link.href || '#'} target="_blank"
target="_blank" rel="noreferrer"
rel="noreferrer" aria-label={link.title}
aria-label={link.title} className="border border-border inline-flex h-8 w-8 items-center
className="border border-border inline-flex h-8 w-8 items-center
justify-center rounded-full hover:bg-accent hover:text-accent-foreground" justify-center rounded-full hover:bg-accent hover:text-accent-foreground"
> >
<span className="sr-only">{link.title}</span> <span className="sr-only">{link.title}</span>
{link.icon ? link.icon : null} {link.icon ? link.icon : null}
</a> </a>
))} ))}
</div> </div>
</div> </div>
@ -63,33 +62,32 @@ export function Footer({ className }: React.HTMLAttributes<HTMLElement>) {
</div> </div>
{/* footer links */} {/* footer links */}
{footerLinks && {footerLinks?.map((section) => (
footerLinks.map((section) => ( <div
<div key={section.title}
key={section.title} className="col-span-1 md:col-span-1 items-start"
className="col-span-1 md:col-span-1 items-start" >
> <span className="text-sm font-semibold uppercase">
<span className="text-sm font-semibold uppercase"> {section.title}
{section.title} </span>
</span> <ul className="mt-4 list-inside space-y-3">
<ul className="mt-4 list-inside space-y-3"> {section.items?.map(
{section.items?.map( (item) =>
(item) => item.href && (
item.href && ( <li key={item.title}>
<li key={item.title}> <LocaleLink
<LocaleLink href={item.href || '#'}
href={item.href || '#'} target={item.external ? '_blank' : undefined}
target={item.external ? '_blank' : undefined} className="text-sm text-muted-foreground hover:text-primary"
className="text-sm text-muted-foreground hover:text-primary" >
> {item.title}
{item.title} </LocaleLink>
</LocaleLink> </li>
</li> )
) )}
)} </ul>
</ul> </div>
</div> ))}
))}
</div> </div>
</Container> </Container>

View File

@ -177,104 +177,101 @@ function MainMobileMenu({ userLoggedIn, onLinkClicked }: MainMobileMenuProps) {
{/* main menu */} {/* main menu */}
<ul className="w-full px-4"> <ul className="w-full px-4">
{menuLinks && {menuLinks?.map((item) => {
menuLinks.map((item) => { const isActive = item.href
const isActive = item.href ? localePathname.startsWith(item.href)
? localePathname.startsWith(item.href) : item.items?.some(
: item.items?.some( (subItem) =>
(subItem) => subItem.href && localePathname.startsWith(subItem.href)
subItem.href && localePathname.startsWith(subItem.href) );
);
return ( return (
<li key={item.title} className="py-1"> <li key={item.title} className="py-1">
{item.items ? ( {item.items ? (
<Collapsible <Collapsible
open={expanded[item.title.toLowerCase()]} open={expanded[item.title.toLowerCase()]}
onOpenChange={(isOpen) => onOpenChange={(isOpen) =>
setExpanded((prev) => ({ setExpanded((prev) => ({
...prev, ...prev,
[item.title.toLowerCase()]: isOpen, [item.title.toLowerCase()]: isOpen,
})) }))
} }
> >
<CollapsibleTrigger asChild> <CollapsibleTrigger asChild>
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
className={cn( className={cn(
'flex w-full !pl-2 items-center justify-between text-left', 'flex w-full !pl-2 items-center justify-between text-left',
'bg-transparent text-muted-foreground cursor-pointer', 'bg-transparent text-muted-foreground cursor-pointer',
'hover:bg-transparent hover:text-foreground', 'hover:bg-transparent hover:text-foreground',
'focus:bg-transparent focus:text-foreground', 'focus:bg-transparent focus:text-foreground',
isActive && isActive &&
'font-semibold bg-transparent text-foreground' 'font-semibold bg-transparent text-foreground'
)} )}
> >
<span className="text-base">{item.title}</span> <span className="text-base">{item.title}</span>
{expanded[item.title.toLowerCase()] ? ( {expanded[item.title.toLowerCase()] ? (
<ChevronDownIcon className="size-4" /> <ChevronDownIcon className="size-4" />
) : ( ) : (
<ChevronRightIcon className="size-4" /> <ChevronRightIcon className="size-4" />
)} )}
</Button> </Button>
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent className="pl-2"> <CollapsibleContent className="pl-2">
<ul className="mt-2 space-y-2 pl-0"> <ul className="mt-2 space-y-2 pl-0">
{item.items.map((subItem) => { {item.items.map((subItem) => {
const isSubItemActive = const isSubItemActive =
subItem.href && subItem.href &&
localePathname.startsWith(subItem.href); localePathname.startsWith(subItem.href);
return ( return (
<li key={subItem.title}> <li key={subItem.title}>
<LocaleLink <LocaleLink
href={subItem.href || '#'} href={subItem.href || '#'}
target={ target={subItem.external ? '_blank' : undefined}
subItem.external ? '_blank' : undefined rel={
} subItem.external
rel={ ? 'noopener noreferrer'
subItem.external : undefined
? 'noopener noreferrer' }
: undefined className={cn(
} buttonVariants({ variant: 'ghost' }),
'group h-auto w-full justify-start gap-4 p-1 !pl-0 !pr-3',
'bg-transparent text-muted-foreground cursor-pointer',
'hover:bg-transparent hover:text-foreground',
'focus:bg-transparent focus:text-foreground',
isSubItemActive &&
'font-semibold bg-transparent text-foreground'
)}
onClick={onLinkClicked}
>
<div
className={cn( className={cn(
buttonVariants({ variant: 'ghost' }), 'flex size-8 shrink-0 items-center justify-center transition-colors ml-0',
'group h-auto w-full justify-start gap-4 p-1 !pl-0 !pr-3', 'bg-transparent text-muted-foreground',
'bg-transparent text-muted-foreground cursor-pointer', 'group-hover:bg-transparent group-hover:text-foreground',
'hover:bg-transparent hover:text-foreground', 'group-focus:bg-transparent group-focus:text-foreground',
'focus:bg-transparent focus:text-foreground',
isSubItemActive && isSubItemActive &&
'font-semibold bg-transparent text-foreground' 'bg-transparent text-foreground'
)} )}
onClick={onLinkClicked}
> >
<div {subItem.icon ? subItem.icon : null}
</div>
<div className="flex-1">
<span
className={cn( className={cn(
'flex size-8 shrink-0 items-center justify-center transition-colors ml-0', 'text-sm text-muted-foreground',
'bg-transparent text-muted-foreground',
'group-hover:bg-transparent group-hover:text-foreground', 'group-hover:bg-transparent group-hover:text-foreground',
'group-focus:bg-transparent group-focus:text-foreground', 'group-focus:bg-transparent group-focus:text-foreground',
isSubItemActive && isSubItemActive &&
'bg-transparent text-foreground' 'font-semibold bg-transparent text-foreground'
)} )}
> >
{subItem.icon ? subItem.icon : null} {subItem.title}
</div> </span>
<div className="flex-1"> {/* hide description for now */}
<span {/* {subItem.description && (
className={cn(
'text-sm text-muted-foreground',
'group-hover:bg-transparent group-hover:text-foreground',
'group-focus:bg-transparent group-focus:text-foreground',
isSubItemActive &&
'font-semibold bg-transparent text-foreground'
)}
>
{subItem.title}
</span>
{/* hide description for now */}
{/* {subItem.description && (
<p <p
className={cn( className={cn(
'text-xs text-muted-foreground', 'text-xs text-muted-foreground',
@ -287,49 +284,48 @@ function MainMobileMenu({ userLoggedIn, onLinkClicked }: MainMobileMenuProps) {
{subItem.description} {subItem.description}
</p> </p>
)} */} )} */}
</div> </div>
{subItem.external && ( {subItem.external && (
<ArrowUpRightIcon <ArrowUpRightIcon
className={cn( className={cn(
'size-4 shrink-0 text-muted-foreground items-center', 'size-4 shrink-0 text-muted-foreground items-center',
'group-hover:bg-transparent group-hover:text-foreground', 'group-hover:bg-transparent group-hover:text-foreground',
'group-focus:bg-transparent group-focus:text-foreground', 'group-focus:bg-transparent group-focus:text-foreground',
isSubItemActive && isSubItemActive &&
'bg-transparent text-foreground' 'bg-transparent text-foreground'
)} )}
/> />
)} )}
</LocaleLink> </LocaleLink>
</li> </li>
); );
})} })}
</ul> </ul>
</CollapsibleContent> </CollapsibleContent>
</Collapsible> </Collapsible>
) : ( ) : (
<LocaleLink <LocaleLink
href={item.href || '#'} href={item.href || '#'}
target={item.external ? '_blank' : undefined} target={item.external ? '_blank' : undefined}
rel={item.external ? 'noopener noreferrer' : undefined} rel={item.external ? 'noopener noreferrer' : undefined}
className={cn( className={cn(
buttonVariants({ variant: 'ghost' }), buttonVariants({ variant: 'ghost' }),
'w-full !pl-2 justify-start cursor-pointer group', 'w-full !pl-2 justify-start cursor-pointer group',
'bg-transparent text-muted-foreground', 'bg-transparent text-muted-foreground',
'hover:bg-transparent hover:text-foreground', 'hover:bg-transparent hover:text-foreground',
'focus:bg-transparent focus:text-foreground', 'focus:bg-transparent focus:text-foreground',
isActive && isActive && 'font-semibold bg-transparent text-foreground'
'font-semibold bg-transparent text-foreground' )}
)} onClick={onLinkClicked}
onClick={onLinkClicked} >
> <div className="flex items-center w-full pl-0">
<div className="flex items-center w-full pl-0"> <span className="text-base">{item.title}</span>
<span className="text-base">{item.title}</span> </div>
</div> </LocaleLink>
</LocaleLink> )}
)} </li>
</li> );
); })}
})}
</ul> </ul>
{/* bottom buttons */} {/* bottom buttons */}

View File

@ -78,136 +78,132 @@ export function Navbar({ scroll }: NavBarProps) {
<div className="flex-1 flex items-center justify-center space-x-2"> <div className="flex-1 flex items-center justify-center space-x-2">
<NavigationMenu className="relative"> <NavigationMenu className="relative">
<NavigationMenuList className="flex items-center"> <NavigationMenuList className="flex items-center">
{menuLinks && {menuLinks?.map((item, index) =>
menuLinks.map((item, index) => item.items ? (
item.items ? ( <NavigationMenuItem key={index} className="relative">
<NavigationMenuItem key={index} className="relative"> <NavigationMenuTrigger
<NavigationMenuTrigger data-active={
data-active={ item.items.some((subItem) =>
item.items.some((subItem) => subItem.href
subItem.href ? localePathname.startsWith(subItem.href)
? localePathname.startsWith(subItem.href) : false
: false )
) ? 'true'
? 'true' : undefined
: undefined }
} className={customNavigationMenuTriggerStyle}
className={customNavigationMenuTriggerStyle} >
> {item.title}
{item.title} </NavigationMenuTrigger>
</NavigationMenuTrigger> <NavigationMenuContent>
<NavigationMenuContent> <ul className="grid w-[400px] gap-4 p-4 md:w-[500px] md:grid-cols-2 lg:w-[600px]">
<ul className="grid w-[400px] gap-4 p-4 md:w-[500px] md:grid-cols-2 lg:w-[600px]"> {item.items?.map((subItem, subIndex) => {
{item.items && const isSubItemActive =
item.items.map((subItem, subIndex) => { subItem.href &&
const isSubItemActive = localePathname.startsWith(subItem.href);
subItem.href && return (
localePathname.startsWith(subItem.href); <li key={subIndex}>
return ( <NavigationMenuLink asChild>
<li key={subIndex}> <LocaleLink
<NavigationMenuLink asChild> href={subItem.href || '#'}
<LocaleLink target={
href={subItem.href || '#'} subItem.external ? '_blank' : undefined
target={ }
subItem.external rel={
? '_blank' subItem.external
: undefined ? 'noopener noreferrer'
} : undefined
rel={ }
subItem.external className={cn(
? 'noopener noreferrer' 'group flex select-none flex-row items-center gap-4 rounded-md',
: undefined 'p-2 leading-none no-underline outline-hidden transition-colors',
} 'hover:bg-accent hover:text-accent-foreground',
'focus:bg-accent focus:text-accent-foreground',
isSubItemActive &&
'bg-accent text-accent-foreground'
)}
>
<div
className={cn(
'flex size-8 shrink-0 items-center justify-center transition-colors',
'bg-transparent text-muted-foreground',
'group-hover:bg-transparent group-hover:text-foreground',
'group-focus:bg-transparent group-focus:text-foreground',
isSubItemActive &&
'bg-transparent text-foreground'
)}
>
{subItem.icon ? subItem.icon : null}
</div>
<div className="flex-1">
<div
className={cn( className={cn(
'group flex select-none flex-row items-center gap-4 rounded-md', 'text-sm font-medium text-muted-foreground',
'p-2 leading-none no-underline outline-hidden transition-colors', 'group-hover:bg-transparent group-hover:text-foreground',
'hover:bg-accent hover:text-accent-foreground', 'group-focus:bg-transparent group-focus:text-foreground',
'focus:bg-accent focus:text-accent-foreground',
isSubItemActive && isSubItemActive &&
'bg-accent text-accent-foreground' 'bg-transparent text-foreground'
)} )}
> >
{subItem.title}
</div>
{subItem.description && (
<div <div
className={cn( className={cn(
'flex size-8 shrink-0 items-center justify-center transition-colors', 'text-sm text-muted-foreground',
'bg-transparent text-muted-foreground', 'group-hover:bg-transparent group-hover:text-foreground/80',
'group-hover:bg-transparent group-hover:text-foreground', 'group-focus:bg-transparent group-focus:text-foreground/80',
'group-focus:bg-transparent group-focus:text-foreground',
isSubItemActive && isSubItemActive &&
'bg-transparent text-foreground' 'bg-transparent text-foreground/80'
)} )}
> >
{subItem.icon ? subItem.icon : null} {subItem.description}
</div> </div>
<div className="flex-1"> )}
<div </div>
className={cn( {subItem.external && (
'text-sm font-medium text-muted-foreground', <ArrowUpRightIcon
'group-hover:bg-transparent group-hover:text-foreground', className={cn(
'group-focus:bg-transparent group-focus:text-foreground', 'size-4 shrink-0 text-muted-foreground',
isSubItemActive && 'group-hover:bg-transparent group-hover:text-foreground',
'bg-transparent text-foreground' 'group-focus:bg-transparent group-focus:text-foreground',
)} isSubItemActive &&
> 'bg-transparent text-foreground'
{subItem.title}
</div>
{subItem.description && (
<div
className={cn(
'text-sm text-muted-foreground',
'group-hover:bg-transparent group-hover:text-foreground/80',
'group-focus:bg-transparent group-focus:text-foreground/80',
isSubItemActive &&
'bg-transparent text-foreground/80'
)}
>
{subItem.description}
</div>
)}
</div>
{subItem.external && (
<ArrowUpRightIcon
className={cn(
'size-4 shrink-0 text-muted-foreground',
'group-hover:bg-transparent group-hover:text-foreground',
'group-focus:bg-transparent group-focus:text-foreground',
isSubItemActive &&
'bg-transparent text-foreground'
)}
/>
)} )}
</LocaleLink> />
</NavigationMenuLink> )}
</li> </LocaleLink>
); </NavigationMenuLink>
})} </li>
</ul> );
</NavigationMenuContent> })}
</NavigationMenuItem> </ul>
) : ( </NavigationMenuContent>
<NavigationMenuItem key={index}> </NavigationMenuItem>
<NavigationMenuLink ) : (
asChild <NavigationMenuItem key={index}>
active={ <NavigationMenuLink
item.href asChild
? localePathname.startsWith(item.href) active={
: false item.href
? localePathname.startsWith(item.href)
: false
}
className={customNavigationMenuTriggerStyle}
>
<LocaleLink
href={item.href || '#'}
target={item.external ? '_blank' : undefined}
rel={
item.external ? 'noopener noreferrer' : undefined
} }
className={customNavigationMenuTriggerStyle}
> >
<LocaleLink {item.title}
href={item.href || '#'} </LocaleLink>
target={item.external ? '_blank' : undefined} </NavigationMenuLink>
rel={ </NavigationMenuItem>
item.external ? 'noopener noreferrer' : undefined )
} )}
>
{item.title}
</LocaleLink>
</NavigationMenuLink>
</NavigationMenuItem>
)
)}
</NavigationMenuList> </NavigationMenuList>
</NavigationMenu> </NavigationMenu>
</div> </div>

View File

@ -85,22 +85,21 @@ export function UserButtonMobile({ user }: UserButtonProps) {
</div> </div>
<ul className="mb-14 mt-1 w-full text-muted-foreground"> <ul className="mb-14 mt-1 w-full text-muted-foreground">
{avatarLinks && {avatarLinks?.map((item) => (
avatarLinks.map((item) => ( <li
<li key={item.title}
key={item.title} className="rounded-lg text-foreground hover:bg-muted"
className="rounded-lg text-foreground hover:bg-muted" >
<LocaleLink
href={item.href || '#'}
onClick={closeDrawer}
className="flex w-full items-center gap-3 px-2.5 py-2"
> >
<LocaleLink {item.icon ? item.icon : null}
href={item.href || '#'} <p className="text-sm">{item.title}</p>
onClick={closeDrawer} </LocaleLink>
className="flex w-full items-center gap-3 px-2.5 py-2" </li>
> ))}
{item.icon ? item.icon : null}
<p className="text-sm">{item.title}</p>
</LocaleLink>
</li>
))}
<li <li
key="logout" key="logout"

View File

@ -59,7 +59,7 @@ export function CheckoutButton({
}); });
// Redirect to checkout page // Redirect to checkout page
if (result && result.data?.success && result.data.data?.url) { if (result?.data?.success && result.data.data?.url) {
window.location.href = result.data.data?.url; window.location.href = result.data.data?.url;
} else { } else {
console.error('Create checkout session error, result:', result); console.error('Create checkout session error, result:', result);

View File

@ -54,7 +54,7 @@ export function CustomerPortalButton({
}); });
// Redirect to customer portal // Redirect to customer portal
if (result && result.data?.success && result.data.data?.url) { if (result?.data?.success && result.data.data?.url) {
window.location.href = result.data.data?.url; window.location.href = result.data.data?.url;
} else { } else {
console.error('Create customer portal error, result:', result); console.error('Create customer portal error, result:', result);

View File

@ -71,7 +71,7 @@ export function NewsletterFormCard({ className }: NewsletterFormCardProps) {
email: currentUser.email, email: currentUser.email,
}); });
if (statusResult && statusResult.data?.success) { if (statusResult?.data?.success) {
const isCurrentlySubscribed = statusResult.data.subscribed; const isCurrentlySubscribed = statusResult.data.subscribed;
setIsSubscriptionChecked(isCurrentlySubscribed); setIsSubscriptionChecked(isCurrentlySubscribed);
form.setValue('subscribed', isCurrentlySubscribed); form.setValue('subscribed', isCurrentlySubscribed);
@ -122,7 +122,7 @@ export function NewsletterFormCard({ className }: NewsletterFormCardProps) {
email: currentUser.email, email: currentUser.email,
}); });
if (subscribeResult && subscribeResult.data?.success) { if (subscribeResult?.data?.success) {
toast.success(t('newsletter.subscribeSuccess')); toast.success(t('newsletter.subscribeSuccess'));
setIsSubscriptionChecked(true); setIsSubscriptionChecked(true);
form.setValue('subscribed', true); form.setValue('subscribed', true);
@ -140,7 +140,7 @@ export function NewsletterFormCard({ className }: NewsletterFormCardProps) {
email: currentUser.email, email: currentUser.email,
}); });
if (unsubscribeResult && unsubscribeResult.data?.success) { if (unsubscribeResult?.data?.success) {
toast.success(t('newsletter.unsubscribeSuccess')); toast.success(t('newsletter.unsubscribeSuccess'));
setIsSubscriptionChecked(false); setIsSubscriptionChecked(false);
form.setValue('subscribed', false); form.setValue('subscribed', false);

View File

@ -60,11 +60,10 @@ export async function sendEmail(
// This is a template email // This is a template email
const result = await provider.sendTemplate(params); const result = await provider.sendTemplate(params);
return result.success; return result.success;
} else {
// This is a raw email
const result = await provider.sendRawEmail(params);
return result.success;
} }
// This is a raw email
const result = await provider.sendRawEmail(params);
return result.success;
} }
/** /**

View File

@ -71,10 +71,9 @@ export class ResendNewsletterProvider implements NewsletterProvider {
if (createResult.error) { if (createResult.error) {
console.error('Error creating contact', createResult.error); console.error('Error creating contact', createResult.error);
return false; return false;
} else {
console.log('Created new contact', email);
return true;
} }
console.log('Created new contact', email);
return true;
} }
// If the contact already exists, update it // If the contact already exists, update it

View File

@ -1,4 +1,4 @@
import { randomUUID } from 'crypto'; import { randomUUID } from 'node:crypto';
import db from '@/db'; import db from '@/db';
import { payment, session, user } from '@/db/schema'; import { payment, session, user } from '@/db/schema';
import { import {
@ -148,9 +148,8 @@ export class StripeProvider implements PaymentProvider {
if (result.length > 0) { if (result.length > 0) {
return result[0].id; return result[0].id;
} else {
console.warn(`No user found with customerId ${customerId}`);
} }
console.warn(`No user found with customerId ${customerId}`);
return undefined; return undefined;
} catch (error) { } catch (error) {
@ -611,11 +610,10 @@ export class StripeProvider implements PaymentProvider {
`<< Failed to create one-time payment record for user ${userId}` `<< Failed to create one-time payment record for user ${userId}`
); );
return; return;
} else {
console.log(
`<< Created one-time payment record for user ${userId}, price: ${priceId}`
);
} }
console.log(
`<< Created one-time payment record for user ${userId}, price: ${priceId}`
);
} }
/** /**

View File

@ -132,56 +132,55 @@ export const uploadFileFromBrowser = async (
return await response.json(); return await response.json();
} }
// For larger files, use pre-signed URL // For larger files, use pre-signed URL
else {
// First, get a pre-signed URL
const presignedUrlResponse = await fetch(API_STORAGE_PRESIGNED_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
filename: file.name,
contentType: file.type,
folder: folder || '',
}),
});
if (!presignedUrlResponse.ok) { // First, get a pre-signed URL
const error = await presignedUrlResponse.json(); const presignedUrlResponse = await fetch(API_STORAGE_PRESIGNED_URL, {
throw new Error(error.message || 'Failed to get pre-signed URL'); method: 'POST',
} headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
filename: file.name,
contentType: file.type,
folder: folder || '',
}),
});
const { url, key } = await presignedUrlResponse.json(); if (!presignedUrlResponse.ok) {
const error = await presignedUrlResponse.json();
// Then upload directly to the storage provider throw new Error(error.message || 'Failed to get pre-signed URL');
const uploadResponse = await fetch(url, {
method: 'PUT',
headers: {
'Content-Type': file.type,
},
body: file,
});
if (!uploadResponse.ok) {
throw new Error('Failed to upload file using pre-signed URL');
}
// Get the public URL
const fileUrlResponse = await fetch(API_STORAGE_FILE_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ key }),
});
if (!fileUrlResponse.ok) {
const error = await fileUrlResponse.json();
throw new Error(error.message || 'Failed to get file URL');
}
return await fileUrlResponse.json();
} }
const { url, key } = await presignedUrlResponse.json();
// Then upload directly to the storage provider
const uploadResponse = await fetch(url, {
method: 'PUT',
headers: {
'Content-Type': file.type,
},
body: file,
});
if (!uploadResponse.ok) {
throw new Error('Failed to upload file using pre-signed URL');
}
// Get the public URL
const fileUrlResponse = await fetch(API_STORAGE_FILE_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ key }),
});
if (!fileUrlResponse.ok) {
const error = await fileUrlResponse.json();
throw new Error(error.message || 'Failed to get file URL');
}
return await fileUrlResponse.json();
} catch (error) { } catch (error) {
const message = const message =
error instanceof Error error instanceof Error

View File

@ -1,4 +1,4 @@
import { randomUUID } from 'crypto'; import { randomUUID } from 'node:crypto';
import { import {
GetObjectCommand, GetObjectCommand,
PutObjectCommand, PutObjectCommand,