refactor: biome lint part 1

This commit is contained in:
javayhu 2025-04-18 21:47:14 +08:00
parent 5b02b0379f
commit 23cd59bbac
199 changed files with 2417 additions and 1961 deletions

View File

@ -66,6 +66,8 @@
"src/components/magicui/*.tsx",
"src/app/[[]locale]/preview/**",
"src/db/schema.ts",
"src/payment/types.ts",
"src/types/index.d.ts",
"public/sw.js"
]
},

View File

@ -1,11 +1,11 @@
import { DEFAULT_LOCALE, LOCALES } from "@/i18n/routing";
import { defineCollection, defineConfig } from "@content-collections/core";
import path from 'path';
import { DEFAULT_LOCALE, LOCALES } from '@/i18n/routing';
import { defineCollection, defineConfig } from '@content-collections/core';
import {
createDocSchema,
createMetaSchema,
transformMDX,
} from '@fumadocs/content-collections/configuration';
import path from "path";
/**
* 1. Content Collections documentation
@ -53,7 +53,7 @@ export const authors = defineCollection({
slug: z.string(),
name: z.string(),
avatar: z.string(),
locale: z.string().optional().default(DEFAULT_LOCALE)
locale: z.string().optional().default(DEFAULT_LOCALE),
}),
transform: async (data, context) => {
// Get the filename from the path
@ -68,7 +68,7 @@ export const authors = defineCollection({
...data,
locale,
};
}
},
});
/**
@ -88,7 +88,7 @@ export const categories = defineCollection({
slug: z.string(),
name: z.string(),
description: z.string(),
locale: z.string().optional().default(DEFAULT_LOCALE)
locale: z.string().optional().default(DEFAULT_LOCALE),
}),
transform: async (data, context) => {
// Get the filename from the path
@ -101,9 +101,9 @@ export const categories = defineCollection({
return {
...data,
locale
locale,
};
}
},
});
/**
@ -136,7 +136,7 @@ export const posts = defineCollection({
published: z.boolean().default(true),
categories: z.array(z.string()),
author: z.string(),
estimatedTime: z.number().optional() // Reading time in minutes
estimatedTime: z.number().optional(), // Reading time in minutes
}),
transform: async (data, context) => {
// Use Fumadocs transformMDX for consistent MDX processing
@ -156,13 +156,15 @@ export const posts = defineCollection({
.find((a) => a.slug === data.author && a.locale === locale);
// Find categories by matching slug and locale
const blogCategories = data.categories.map(categorySlug => {
const blogCategories = data.categories
.map((categorySlug) => {
const category = context
.documents(categories)
.find(c => c.slug === categorySlug && c.locale === locale);
.find((c) => c.slug === categorySlug && c.locale === locale);
return category;
}).filter(Boolean); // Remove null values
})
.filter(Boolean); // Remove null values
// Create the slug and slugAsParams
const slug = `/blog/${base}`;
@ -182,9 +184,9 @@ export const posts = defineCollection({
slugAsParams,
estimatedTime,
body: transformedData.body,
toc: transformedData.toc
toc: transformedData.toc,
};
}
},
});
/**
@ -210,7 +212,7 @@ export const pages = defineCollection({
title: z.string(),
description: z.string(),
date: z.string().datetime(),
published: z.boolean().default(true)
published: z.boolean().default(true),
}),
transform: async (data, context) => {
// Use Fumadocs transformMDX for consistent MDX processing
@ -234,9 +236,9 @@ export const pages = defineCollection({
slug,
slugAsParams,
body: transformedData.body,
toc: transformedData.toc
toc: transformedData.toc,
};
}
},
});
/**
@ -263,7 +265,7 @@ export const releases = defineCollection({
description: z.string(),
date: z.string().datetime(),
version: z.string(),
published: z.boolean().default(true)
published: z.boolean().default(true),
}),
transform: async (data, context) => {
// Use Fumadocs transformMDX for consistent MDX processing
@ -287,9 +289,9 @@ export const releases = defineCollection({
slug,
slugAsParams,
body: transformedData.body,
toc: transformedData.toc
toc: transformedData.toc,
};
}
},
});
/**
@ -301,7 +303,10 @@ export const releases = defineCollection({
* @param fileName Filename without extension (already has .mdx removed)
* @returns Object with locale and base name
*/
function extractLocaleAndBase(fileName: string): { locale: string; base: string } {
function extractLocaleAndBase(fileName: string): {
locale: string;
base: string;
} {
// Split filename into parts
const parts = fileName.split('.');
@ -319,5 +324,5 @@ function extractLocaleAndBase(fileName: string): { locale: string; base: string
}
export default defineConfig({
collections: [docs, metas, authors, categories, posts, pages, releases]
collections: [docs, metas, authors, categories, posts, pages, releases],
});

4
global.d.ts vendored
View File

@ -1,5 +1,5 @@
import { routing } from '@/i18n/routing';
import messages from './messages/en.json';
import type { routing } from '@/i18n/routing';
import type messages from './messages/en.json';
/**
* next-intl 4.0.0

View File

@ -1,6 +1,6 @@
import type { NextConfig } from "next";
import { withContentCollections } from '@content-collections/next';
import type { NextConfig } from 'next';
import createNextIntlPlugin from 'next-intl/plugin';
import { withContentCollections } from "@content-collections/next";
/**
* https://nextjs.org/docs/app/api-reference/config/next-config-js
@ -12,22 +12,22 @@ const nextConfig: NextConfig = {
// https://nextjs.org/docs/architecture/nextjs-compiler#remove-console
// Remove all console.* calls in production only
compiler: {
removeConsole: process.env.NODE_ENV === "production",
removeConsole: process.env.NODE_ENV === 'production',
},
images: {
remotePatterns: [
{
protocol: "https",
hostname: "avatars.githubusercontent.com",
protocol: 'https',
hostname: 'avatars.githubusercontent.com',
},
{
protocol: "https",
hostname: "lh3.googleusercontent.com",
protocol: 'https',
hostname: 'lh3.googleusercontent.com',
},
{
protocol: "https",
hostname: "randomuser.me",
protocol: 'https',
hostname: 'randomuser.me',
},
{
protocol: 'https',

View File

@ -1,14 +1,14 @@
'use server';
import { getSession } from "@/lib/server";
import { findPlanByPlanId } from "@/lib/price-plan";
import { getUrlWithLocale } from "@/lib/urls/urls";
import { createCheckout } from "@/payment";
import { CreateCheckoutParams } from "@/payment/types";
import { getLocale } from "next-intl/server";
import { findPlanByPlanId } from '@/lib/price-plan';
import { getSession } from '@/lib/server';
import { getUrlWithLocale } from '@/lib/urls/urls';
import { createCheckout } from '@/payment';
import type { CreateCheckoutParams } from '@/payment/types';
import { Routes } from '@/routes';
import { getLocale } from 'next-intl/server';
import { createSafeActionClient } from 'next-safe-action';
import { z } from 'zod';
import { Routes } from "@/routes";
// Create a safe action client
const actionClient = createSafeActionClient();
@ -33,7 +33,9 @@ export const createCheckoutAction = actionClient
// Get the current user session for authorization
const session = await getSession();
if (!session) {
console.warn(`unauthorized request to create checkout session for user ${userId}`);
console.warn(
`unauthorized request to create checkout session for user ${userId}`
);
return {
success: false,
error: 'Unauthorized',
@ -42,7 +44,9 @@ export const createCheckoutAction = actionClient
// Only allow users to create their own checkout session
if (session.user.id !== userId) {
console.warn(`current user ${session.user.id} is not authorized to create checkout session for user ${userId}`);
console.warn(
`current user ${session.user.id} is not authorized to create checkout session for user ${userId}`
);
return {
success: false,
error: 'Not authorized to do this action',
@ -70,7 +74,10 @@ export const createCheckoutAction = actionClient
};
// Create the checkout session with localized URLs
const successUrl = getUrlWithLocale('/settings/billing?session_id={CHECKOUT_SESSION_ID}', locale);
const successUrl = getUrlWithLocale(
'/settings/billing?session_id={CHECKOUT_SESSION_ID}',
locale
);
const cancelUrl = getUrlWithLocale(Routes.Pricing, locale);
const params: CreateCheckoutParams = {
planId,
@ -89,7 +96,7 @@ export const createCheckoutAction = actionClient
data: result,
};
} catch (error) {
console.error("create checkout session error:", error);
console.error('create checkout session error:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Something went wrong',

View File

@ -1,13 +1,13 @@
'use server';
import db from "@/db";
import { user } from "@/db/schema";
import { getSession } from "@/lib/server";
import { getUrlWithLocale } from "@/lib/urls/urls";
import { createCustomerPortal } from "@/payment";
import { CreatePortalParams } from "@/payment/types";
import { eq } from "drizzle-orm";
import { getLocale } from "next-intl/server";
import db from '@/db';
import { user } from '@/db/schema';
import { getSession } from '@/lib/server';
import { getUrlWithLocale } from '@/lib/urls/urls';
import { createCustomerPortal } from '@/payment';
import type { CreatePortalParams } from '@/payment/types';
import { eq } from 'drizzle-orm';
import { getLocale } from 'next-intl/server';
import { createSafeActionClient } from 'next-safe-action';
import { z } from 'zod';
@ -17,7 +17,10 @@ const actionClient = createSafeActionClient();
// Portal schema for validation
const portalSchema = z.object({
userId: z.string().min(1, { message: 'User ID is required' }),
returnUrl: z.string().url({ message: 'Return URL must be a valid URL' }).optional(),
returnUrl: z
.string()
.url({ message: 'Return URL must be a valid URL' })
.optional(),
});
/**
@ -31,7 +34,9 @@ export const createPortalAction = actionClient
// Get the current user session for authorization
const session = await getSession();
if (!session) {
console.warn(`unauthorized request to create portal session for user ${userId}`);
console.warn(
`unauthorized request to create portal session for user ${userId}`
);
return {
success: false,
error: 'Unauthorized',
@ -40,7 +45,9 @@ export const createPortalAction = actionClient
// Only allow users to create their own portal session
if (session.user.id !== userId) {
console.warn(`current user ${session.user.id} is not authorized to create portal session for user ${userId}`);
console.warn(
`current user ${session.user.id} is not authorized to create portal session for user ${userId}`
);
return {
success: false,
error: 'Not authorized to do this action',
@ -67,11 +74,12 @@ export const createPortalAction = actionClient
const locale = await getLocale();
// Create the portal session with localized URL if no custom return URL is provided
const returnUrlWithLocale = returnUrl || getUrlWithLocale('/settings/billing', locale);
const returnUrlWithLocale =
returnUrl || getUrlWithLocale('/settings/billing', locale);
const params: CreatePortalParams = {
customerId: customerResult[0].customerId,
returnUrl: returnUrlWithLocale,
locale
locale,
};
const result = await createCustomerPortal(params);
@ -81,7 +89,7 @@ export const createPortalAction = actionClient
data: result,
};
} catch (error) {
console.error("create customer portal error:", error);
console.error('create customer portal error:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Something went wrong',

View File

@ -1,9 +1,9 @@
'use server';
import { getSession } from "@/lib/server";
import { getSubscriptions } from "@/payment";
import { getSession } from '@/lib/server';
import { getSubscriptions } from '@/payment';
import { createSafeActionClient } from 'next-safe-action';
import { z } from "zod";
import { z } from 'zod';
// Create a safe action client
const actionClient = createSafeActionClient();
@ -27,7 +27,9 @@ export const getActiveSubscriptionAction = actionClient
// Get the current user session for authorization
const session = await getSession();
if (!session) {
console.warn(`unauthorized request to get active subscription for user ${userId}`);
console.warn(
`unauthorized request to get active subscription for user ${userId}`
);
return {
success: false,
error: 'Unauthorized',
@ -36,7 +38,9 @@ export const getActiveSubscriptionAction = actionClient
// Only allow users to check their own status unless they're admins
if (session.user.id !== userId && session.user.role !== 'admin') {
console.warn(`current user ${session.user.id} is not authorized to get active subscription for user ${userId}`);
console.warn(
`current user ${session.user.id} is not authorized to get active subscription for user ${userId}`
);
return {
success: false,
error: 'Not authorized to do this action',
@ -46,7 +50,7 @@ export const getActiveSubscriptionAction = actionClient
try {
// Find the user's most recent active subscription
const subscriptions = await getSubscriptions({
userId: session.user.id
userId: session.user.id,
});
// console.log('get user subscriptions:', subscriptions);
@ -54,8 +58,8 @@ export const getActiveSubscriptionAction = actionClient
// Find the most recent active subscription (if any)
if (subscriptions && subscriptions.length > 0) {
// First try to find an active subscription
const activeSubscription = subscriptions.find(sub =>
sub.status === 'active' || sub.status === 'trialing'
const activeSubscription = subscriptions.find(
(sub) => sub.status === 'active' || sub.status === 'trialing'
);
// If found, use it
@ -63,7 +67,10 @@ export const getActiveSubscriptionAction = actionClient
console.log('find active subscription for userId:', session.user.id);
subscriptionData = activeSubscription;
} else {
console.log('no active subscription found for userId:', session.user.id);
console.log(
'no active subscription found for userId:',
session.user.id
);
}
} else {
console.log('no subscriptions found for userId:', session.user.id);
@ -74,7 +81,7 @@ export const getActiveSubscriptionAction = actionClient
data: subscriptionData,
};
} catch (error) {
console.error("get user subscription data error:", error);
console.error('get user subscription data error:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Something went wrong',

View File

@ -1,13 +1,13 @@
'use server';
import db from "@/db";
import { payment } from "@/db/schema";
import { getSession } from "@/lib/server";
import { getAllPricePlans, findPlanByPriceId } from "@/lib/price-plan";
import { PaymentTypes } from "@/payment/types";
import { and, eq } from "drizzle-orm";
import db from '@/db';
import { payment } from '@/db/schema';
import { findPlanByPriceId, getAllPricePlans } from '@/lib/price-plan';
import { getSession } from '@/lib/server';
import { PaymentTypes } from '@/payment/types';
import { and, eq } from 'drizzle-orm';
import { createSafeActionClient } from 'next-safe-action';
import { z } from "zod";
import { z } from 'zod';
// Create a safe action client
const actionClient = createSafeActionClient();
@ -33,7 +33,9 @@ export const getLifetimeStatusAction = actionClient
// Get the current user session for authorization
const session = await getSession();
if (!session) {
console.warn(`unauthorized request to get lifetime status for user ${userId}`);
console.warn(
`unauthorized request to get lifetime status for user ${userId}`
);
return {
success: false,
error: 'Unauthorized',
@ -42,7 +44,9 @@ export const getLifetimeStatusAction = actionClient
// Only allow users to check their own status unless they're admins
if (session.user.id !== userId && session.user.role !== 'admin') {
console.warn(`current user ${session.user.id} is not authorized to get lifetime status for user ${userId}`);
console.warn(
`current user ${session.user.id} is not authorized to get lifetime status for user ${userId}`
);
return {
success: false,
error: 'Not authorized to do this action',
@ -53,8 +57,8 @@ export const getLifetimeStatusAction = actionClient
// Get lifetime plans
const plans = getAllPricePlans();
const lifetimePlanIds = plans
.filter(plan => plan.isLifetime)
.map(plan => plan.id);
.filter((plan) => plan.isLifetime)
.map((plan) => plan.id);
// Check if there are any lifetime plans defined in the system
if (lifetimePlanIds.length === 0) {
@ -66,7 +70,11 @@ export const getLifetimeStatusAction = actionClient
// Query the database for one-time payments with lifetime plans
const result = await db
.select({ id: payment.id, priceId: payment.priceId, type: payment.type })
.select({
id: payment.id,
priceId: payment.priceId,
type: payment.type,
})
.from(payment)
.where(
and(
@ -77,7 +85,7 @@ export const getLifetimeStatusAction = actionClient
);
// Check if any payment has a lifetime plan
const hasLifetimePayment = result.some(paymentRecord => {
const hasLifetimePayment = result.some((paymentRecord) => {
const plan = findPlanByPriceId(paymentRecord.priceId);
return plan && lifetimePlanIds.includes(plan.id);
});
@ -87,7 +95,7 @@ export const getLifetimeStatusAction = actionClient
isLifetimeMember: hasLifetimePayment,
};
} catch (error) {
console.error("get user lifetime status error:", error);
console.error('get user lifetime status error:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Something went wrong',

View File

@ -15,12 +15,13 @@ const actionClient = createSafeActionClient();
*/
// Contact form schema for validation
const contactFormSchema = z.object({
name: z.string()
name: z
.string()
.min(3, { message: 'Name must be at least 3 characters' })
.max(30, { message: 'Name must not exceed 30 characters' }),
email: z.string()
.email({ message: 'Please enter a valid email address' }),
message: z.string()
email: z.string().email({ message: 'Please enter a valid email address' }),
message: z
.string()
.min(10, { message: 'Message must be at least 10 characters' })
.max(500, { message: 'Message must not exceed 500 characters' }),
});

View File

@ -1,9 +1,9 @@
import GoogleAnalytics from "./google-analytics";
import { UmamiAnalytics } from "./umami-analytics";
import { PlausibleAnalytics } from "./plausible-analytics";
import DataFastAnalytics from "./data-fast-analytics";
import OpenPanelAnalytics from "./open-panel-analytics";
import { SelineAnalytics } from "./seline-analytics";
import DataFastAnalytics from './data-fast-analytics';
import GoogleAnalytics from './google-analytics';
import OpenPanelAnalytics from './open-panel-analytics';
import { PlausibleAnalytics } from './plausible-analytics';
import { SelineAnalytics } from './seline-analytics';
import { UmamiAnalytics } from './umami-analytics';
/**
* Analytics Components all in one
@ -12,7 +12,7 @@ import { SelineAnalytics } from "./seline-analytics";
* 2. only work if the environment variable for the analytics is set
*/
export function Analytics() {
if (process.env.NODE_ENV !== "production") {
if (process.env.NODE_ENV !== 'production') {
return null;
}

View File

@ -1,6 +1,6 @@
"use client";
'use client';
import Script from "next/script";
import Script from 'next/script';
/**
* DataFast Analytics
@ -8,7 +8,7 @@ import Script from "next/script";
* https://datafa.st
*/
export default function DataFastAnalytics() {
if (process.env.NODE_ENV !== "production") {
if (process.env.NODE_ENV !== 'production') {
return null;
}

View File

@ -1,6 +1,6 @@
"use client";
'use client';
import { GoogleAnalytics as NextGoogleAnalytics } from "@next/third-parties/google";
import { GoogleAnalytics as NextGoogleAnalytics } from '@next/third-parties/google';
/**
* Google Analytics
@ -9,7 +9,7 @@ import { GoogleAnalytics as NextGoogleAnalytics } from "@next/third-parties/goog
* https://nextjs.org/docs/app/building-your-application/optimizing/third-party-libraries#google-analytics
*/
export default function GoogleAnalytics() {
if (process.env.NODE_ENV !== "production") {
if (process.env.NODE_ENV !== 'production') {
return null;
}

View File

@ -1,4 +1,4 @@
import { OpenPanelComponent } from "@openpanel/nextjs";
import { OpenPanelComponent } from '@openpanel/nextjs';
/**
* OpenPanel Analytics (https://openpanel.dev)
@ -6,7 +6,7 @@ import { OpenPanelComponent } from "@openpanel/nextjs";
* https://docs.openpanel.dev/docs/sdks/nextjs#options
*/
export default function OpenPanelAnalytics() {
if (process.env.NODE_ENV !== "production") {
if (process.env.NODE_ENV !== 'production') {
return null;
}

View File

@ -1,6 +1,6 @@
"use client";
'use client';
import Script from "next/script";
import Script from 'next/script';
/**
* Plausible Analytics
@ -8,7 +8,7 @@ import Script from "next/script";
* https://plausible.io
*/
export function PlausibleAnalytics() {
if (process.env.NODE_ENV !== "production") {
if (process.env.NODE_ENV !== 'production') {
return null;
}
@ -23,11 +23,6 @@ export function PlausibleAnalytics() {
}
return (
<Script
defer
type="text/javascript"
data-domain={domain}
src={script}
/>
<Script defer type="text/javascript" data-domain={domain} src={script} />
);
}

View File

@ -1,6 +1,6 @@
"use client";
'use client';
import Script from "next/script";
import Script from 'next/script';
/**
* Seline Analytics
@ -10,7 +10,7 @@ import Script from "next/script";
* https://seline.com/docs/stripe
*/
export function SelineAnalytics() {
if (process.env.NODE_ENV !== "production") {
if (process.env.NODE_ENV !== 'production') {
return null;
}

View File

@ -1,6 +1,6 @@
"use client";
'use client';
import Script from "next/script";
import Script from 'next/script';
/**
* Umami Analytics
@ -8,7 +8,7 @@ import Script from "next/script";
* https://umami.is
*/
export function UmamiAnalytics() {
if (process.env.NODE_ENV !== "production") {
if (process.env.NODE_ENV !== 'production') {
return null;
}

View File

@ -14,8 +14,8 @@ import StatsSection from '@/components/blocks/stats/stats';
import TestimonialsSection from '@/components/blocks/testimonials/testimonials';
import { constructMetadata } from '@/lib/metadata';
import { getUrlWithLocale } from '@/lib/urls/urls';
import { Metadata } from 'next';
import { Locale } from 'next-intl';
import type { Metadata } from 'next';
import type { Locale } from 'next-intl';
import { getTranslations } from 'next-intl/server';
/**

View File

@ -4,7 +4,7 @@ import { getPage } from '@/lib/page/get-page';
import { getUrlWithLocale } from '@/lib/urls/urls';
import type { NextPageProps } from '@/types/next-page-props';
import type { Metadata } from 'next';
import { Locale } from 'next-intl';
import type { Locale } from 'next-intl';
import { getTranslations } from 'next-intl/server';
import { notFound } from 'next/navigation';
@ -28,7 +28,7 @@ export async function generateMetadata({
return constructMetadata({
title: page.title + ' | ' + t('title'),
description: page.description,
canonicalUrl: getUrlWithLocale("/cookie", locale),
canonicalUrl: getUrlWithLocale('/cookie', locale),
});
}

View File

@ -1,5 +1,5 @@
import Container from '@/components/layout/container';
import { PropsWithChildren } from 'react';
import type { PropsWithChildren } from 'react';
import '@/styles/mdx.css';

View File

@ -4,7 +4,7 @@ import { getPage } from '@/lib/page/get-page';
import { getUrlWithLocale } from '@/lib/urls/urls';
import type { NextPageProps } from '@/types/next-page-props';
import type { Metadata } from 'next';
import { Locale } from 'next-intl';
import type { Locale } from 'next-intl';
import { getTranslations } from 'next-intl/server';
import { notFound } from 'next/navigation';
@ -28,7 +28,7 @@ export async function generateMetadata({
return constructMetadata({
title: page.title + ' | ' + t('title'),
description: page.description,
canonicalUrl: getUrlWithLocale("/privacy", locale),
canonicalUrl: getUrlWithLocale('/privacy', locale),
});
}

View File

@ -4,7 +4,7 @@ import { getPage } from '@/lib/page/get-page';
import { getUrlWithLocale } from '@/lib/urls/urls';
import type { NextPageProps } from '@/types/next-page-props';
import type { Metadata } from 'next';
import { Locale } from 'next-intl';
import type { Locale } from 'next-intl';
import { getTranslations } from 'next-intl/server';
import { notFound } from 'next/navigation';
@ -28,7 +28,7 @@ export async function generateMetadata({
return constructMetadata({
title: page.title + ' | ' + t('title'),
description: page.description,
canonicalUrl: getUrlWithLocale("/terms", locale),
canonicalUrl: getUrlWithLocale('/terms', locale),
});
}

View File

@ -4,8 +4,8 @@ import { websiteConfig } from '@/config/website';
import { constructMetadata } from '@/lib/metadata';
import { getUrlWithLocale } from '@/lib/urls/urls';
import { MailIcon } from 'lucide-react';
import { Metadata } from 'next';
import { Locale } from 'next-intl';
import type { Metadata } from 'next';
import type { Locale } from 'next-intl';
import { getTranslations } from 'next-intl/server';
export async function generateMetadata({
@ -20,7 +20,7 @@ export async function generateMetadata({
return constructMetadata({
title: pt('title') + ' | ' + t('title'),
description: pt('description'),
canonicalUrl: getUrlWithLocale("/about", locale),
canonicalUrl: getUrlWithLocale('/about', locale),
});
}
@ -49,9 +49,7 @@ export default async function AboutPage() {
</AvatarFallback>
</Avatar>
<div>
<h1 className="text-4xl text-foreground">
{t('authorName')}
</h1>
<h1 className="text-4xl text-foreground">{t('authorName')}</h1>
<p className="text-base text-muted-foreground mt-2">
{t('authorBio')}
</p>
@ -67,7 +65,9 @@ export default async function AboutPage() {
<div className="flex items-center gap-4">
<Button className="rounded-lg cursor-pointer">
<MailIcon className="mr-1 size-4" />
<a href={`mailto:${websiteConfig.mail.from}`}>{t('talkWithMe')}</a>
<a href={`mailto:${websiteConfig.mail.from}`}>
{t('talkWithMe')}
</a>
</Button>
</div>
</div>

View File

@ -4,7 +4,7 @@ import { getReleases } from '@/lib/release/get-releases';
import { getUrlWithLocale } from '@/lib/urls/urls';
import type { NextPageProps } from '@/types/next-page-props';
import type { Metadata } from 'next';
import { Locale } from 'next-intl';
import type { Locale } from 'next-intl';
import { getTranslations } from 'next-intl/server';
import { notFound } from 'next/navigation';
@ -22,7 +22,7 @@ export async function generateMetadata({
return constructMetadata({
title: pt('title') + ' | ' + t('title'),
description: pt('description'),
canonicalUrl: getUrlWithLocale("/changelog", locale),
canonicalUrl: getUrlWithLocale('/changelog', locale),
});
}

View File

@ -1,8 +1,8 @@
import { ContactFormCard } from '@/components/contact/contact-form-card';
import { constructMetadata } from '@/lib/metadata';
import { getUrlWithLocale } from '@/lib/urls/urls';
import { Metadata } from 'next';
import { Locale } from 'next-intl';
import type { Metadata } from 'next';
import type { Locale } from 'next-intl';
import { getTranslations } from 'next-intl/server';
export async function generateMetadata({
@ -17,7 +17,7 @@ export async function generateMetadata({
return constructMetadata({
title: pt('title') + ' | ' + t('title'),
description: pt('description'),
canonicalUrl: getUrlWithLocale("/contact", locale),
canonicalUrl: getUrlWithLocale('/contact', locale),
});
}

View File

@ -1,5 +1,5 @@
import Container from '@/components/layout/container';
import { PropsWithChildren } from 'react';
import type { PropsWithChildren } from 'react';
export default function PageLayout({ children }: PropsWithChildren) {
return (

View File

@ -1,8 +1,8 @@
import { WaitlistFormCard } from '@/components/waitlist/waitlist-form-card';
import { constructMetadata } from '@/lib/metadata';
import { getUrlWithLocale } from '@/lib/urls/urls';
import { Metadata } from 'next';
import { Locale } from 'next-intl';
import type { Metadata } from 'next';
import type { Locale } from 'next-intl';
import { getTranslations } from 'next-intl/server';
export async function generateMetadata({
@ -16,7 +16,7 @@ export async function generateMetadata({
return constructMetadata({
title: pt('title') + ' | ' + t('title'),
description: pt('description'),
canonicalUrl: getUrlWithLocale("/waitlist", locale),
canonicalUrl: getUrlWithLocale('/waitlist', locale),
});
}

View File

@ -1,8 +1,8 @@
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { constructMetadata } from '@/lib/metadata';
import { getUrlWithLocale } from '@/lib/urls/urls';
import { Metadata } from 'next';
import { Locale } from 'next-intl';
import type { Metadata } from 'next';
import type { Locale } from 'next-intl';
import { getTranslations } from 'next-intl/server';
export async function generateMetadata({
@ -17,7 +17,7 @@ export async function generateMetadata({
return constructMetadata({
title: pt('title') + ' | ' + t('title'),
description: pt('description'),
canonicalUrl: getUrlWithLocale("/ai/audio", locale),
canonicalUrl: getUrlWithLocale('/ai/audio', locale),
});
}
@ -44,9 +44,7 @@ export default async function AIAudioPage() {
</Avatar>
<div>
<h1 className="text-4xl text-foreground">
{t('content')}
</h1>
<h1 className="text-4xl text-foreground">{t('content')}</h1>
</div>
</div>
</div>

View File

@ -1,8 +1,8 @@
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { constructMetadata } from '@/lib/metadata';
import { getUrlWithLocale } from '@/lib/urls/urls';
import { Metadata } from 'next';
import { Locale } from 'next-intl';
import type { Metadata } from 'next';
import type { Locale } from 'next-intl';
import { getTranslations } from 'next-intl/server';
export async function generateMetadata({
@ -17,7 +17,7 @@ export async function generateMetadata({
return constructMetadata({
title: pt('title') + ' | ' + t('title'),
description: pt('description'),
canonicalUrl: getUrlWithLocale("/ai/image", locale),
canonicalUrl: getUrlWithLocale('/ai/image', locale),
});
}
@ -44,9 +44,7 @@ export default async function AIImagePage() {
</Avatar>
<div>
<h1 className="text-4xl text-foreground">
{t('content')}
</h1>
<h1 className="text-4xl text-foreground">{t('content')}</h1>
</div>
</div>
</div>

View File

@ -1,5 +1,5 @@
import Container from '@/components/layout/container';
import { PropsWithChildren } from 'react';
import type { PropsWithChildren } from 'react';
export default function PageLayout({ children }: PropsWithChildren) {
return (

View File

@ -1,8 +1,8 @@
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { constructMetadata } from '@/lib/metadata';
import { getUrlWithLocale } from '@/lib/urls/urls';
import { Metadata } from 'next';
import { Locale } from 'next-intl';
import type { Metadata } from 'next';
import type { Locale } from 'next-intl';
import { getTranslations } from 'next-intl/server';
export async function generateMetadata({
@ -17,7 +17,7 @@ export async function generateMetadata({
return constructMetadata({
title: pt('title') + ' | ' + t('title'),
description: pt('description'),
canonicalUrl: getUrlWithLocale("/ai/text", locale),
canonicalUrl: getUrlWithLocale('/ai/text', locale),
});
}
@ -44,9 +44,7 @@ export default async function AITextPage() {
</Avatar>
<div>
<h1 className="text-4xl text-foreground">
{t('content')}
</h1>
<h1 className="text-4xl text-foreground">{t('content')}</h1>
</div>
</div>
</div>

View File

@ -1,8 +1,8 @@
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { constructMetadata } from '@/lib/metadata';
import { getUrlWithLocale } from '@/lib/urls/urls';
import { Metadata } from 'next';
import { Locale } from 'next-intl';
import type { Metadata } from 'next';
import type { Locale } from 'next-intl';
import { getTranslations } from 'next-intl/server';
export async function generateMetadata({
@ -17,7 +17,7 @@ export async function generateMetadata({
return constructMetadata({
title: pt('title') + ' | ' + t('title'),
description: pt('description'),
canonicalUrl: getUrlWithLocale("/ai/video", locale),
canonicalUrl: getUrlWithLocale('/ai/video', locale),
});
}
@ -44,9 +44,7 @@ export default async function AIVideoPage() {
</Avatar>
<div>
<h1 className="text-4xl text-foreground">
{t('content')}
</h1>
<h1 className="text-4xl text-foreground">{t('content')}</h1>
</div>
</div>
</div>

View File

@ -1,6 +1,6 @@
import { categories } from '@/components/nsui/blocks';
import BlocksNav from '@/components/nsui/blocks-nav';
import { PropsWithChildren } from 'react';
import type { PropsWithChildren } from 'react';
/**
* The locale inconsistency issue has been fixed in the BlocksNav component
@ -10,9 +10,7 @@ export default function BlockCategoryLayout({ children }: PropsWithChildren) {
<>
<BlocksNav categories={categories} />
<main>
{children}
</main>
<main>{children}</main>
</>
);
}

View File

@ -2,8 +2,8 @@ import BlockPreview from '@/components/nsui/block-preview';
import { blocks, categories } from '@/components/nsui/blocks';
import { constructMetadata } from '@/lib/metadata';
import { getUrlWithLocale } from '@/lib/urls/urls';
import { Metadata } from 'next';
import { Locale } from 'next-intl';
import type { Metadata } from 'next';
import type { Locale } from 'next-intl';
import { getTranslations } from 'next-intl/server';
import { notFound } from 'next/navigation';
@ -26,7 +26,7 @@ export async function generateMetadata({
return constructMetadata({
title: category + ' | ' + t('title'),
description: t('description'),
canonicalUrl: getUrlWithLocale("/blocks/${category}", locale),
canonicalUrl: getUrlWithLocale('/blocks/${category}', locale),
});
}

View File

@ -4,10 +4,10 @@ import CustomPagination from '@/components/shared/pagination';
import { websiteConfig } from '@/config/website';
import { constructMetadata } from '@/lib/metadata';
import { getUrlWithLocale } from '@/lib/urls/urls';
import { NextPageProps } from '@/types/next-page-props';
import type { NextPageProps } from '@/types/next-page-props';
import { allCategories, allPosts } from 'content-collections';
import type { Metadata } from 'next';
import { Locale } from 'next-intl';
import type { Locale } from 'next-intl';
import { getTranslations } from 'next-intl/server';
export async function generateMetadata({
@ -34,7 +34,7 @@ export async function generateMetadata({
return constructMetadata({
title: `${category.name} | ${t('title')}`,
description: category.description,
canonicalUrl: getUrlWithLocale("/blog/category/${slug}", locale),
canonicalUrl: getUrlWithLocale('/blog/category/${slug}', locale),
});
}

View File

@ -1,9 +1,9 @@
import { BlogCategoryFilter } from '@/components/blog/blog-category-filter';
import Container from '@/components/layout/container';
import { NextPageProps } from '@/types/next-page-props';
import type { NextPageProps } from '@/types/next-page-props';
import { allCategories } from 'content-collections';
import { getTranslations } from 'next-intl/server';
import { PropsWithChildren } from 'react';
import type { PropsWithChildren } from 'react';
interface BlogListLayoutProps extends PropsWithChildren, NextPageProps {}

View File

@ -4,10 +4,10 @@ import CustomPagination from '@/components/shared/pagination';
import { websiteConfig } from '@/config/website';
import { constructMetadata } from '@/lib/metadata';
import { getUrlWithLocale } from '@/lib/urls/urls';
import { NextPageProps } from '@/types/next-page-props';
import type { NextPageProps } from '@/types/next-page-props';
import { allPosts } from 'content-collections';
import { Metadata } from 'next';
import { Locale } from 'next-intl';
import type { Metadata } from 'next';
import type { Locale } from 'next-intl';
import { getTranslations } from 'next-intl/server';
export async function generateMetadata({
@ -21,7 +21,7 @@ export async function generateMetadata({
return constructMetadata({
title: pt('title') + ' | ' + t('title'),
description: pt('description'),
canonicalUrl: getUrlWithLocale("/blog", locale),
canonicalUrl: getUrlWithLocale('/blog', locale),
});
}

View File

@ -1,5 +1,5 @@
import Container from '@/components/layout/container';
import { PropsWithChildren } from 'react';
import type { PropsWithChildren } from 'react';
export default function BlogPostLayout({ children }: PropsWithChildren) {
return (

View File

@ -6,17 +6,17 @@ import { CustomMDXContent } from '@/components/shared/custom-mdx-content';
import { websiteConfig } from '@/config/website';
import { LocaleLink } from '@/i18n/navigation';
import { getTableOfContents } from '@/lib/blog/toc';
import { formatDate } from '@/lib/formatter';
import { constructMetadata } from '@/lib/metadata';
import { getUrlWithLocale } from '@/lib/urls/urls';
import type { NextPageProps } from '@/types/next-page-props';
import { allPosts, Post } from 'content-collections';
import { type Post, allPosts } from 'content-collections';
import { CalendarIcon, ClockIcon, FileTextIcon } from 'lucide-react';
import type { Metadata } from 'next';
import { Locale } from 'next-intl';
import type { Locale } from 'next-intl';
import { getTranslations } from 'next-intl/server';
import Image from 'next/image';
import { notFound } from 'next/navigation';
import { formatDate } from '@/lib/formatter';
import '@/styles/mdx.css';
@ -85,7 +85,7 @@ export async function generateMetadata({
const post = await getBlogPostFromParams({
params: Promise.resolve({ slug, locale }),
searchParams: Promise.resolve({})
searchParams: Promise.resolve({}),
});
if (!post) {
console.warn(
@ -194,9 +194,7 @@ export default async function BlogPostPage(props: NextPageProps) {
/>
)}
</div>
<span className="line-clamp-1">
{post.author?.name}
</span>
<span className="line-clamp-1">{post.author?.name}</span>
</div>
</div>

View File

@ -1,6 +1,6 @@
import { Footer } from '@/components/layout/footer';
import { Navbar } from '@/components/layout/navbar';
import { ReactNode } from 'react';
import type { ReactNode } from 'react';
export default function MarketingLayout({ children }: { children: ReactNode }) {
return (

View File

@ -1,7 +1,7 @@
import { constructMetadata } from '@/lib/metadata';
import { getUrlWithLocale } from '@/lib/urls/urls';
import { Metadata } from 'next';
import { Locale } from 'next-intl';
import type { Metadata } from 'next';
import type { Locale } from 'next-intl';
import { getTranslations } from 'next-intl/server';
export async function generateMetadata({

View File

@ -4,7 +4,7 @@ import { DataTable } from '@/components/dashboard/data-table';
import { SectionCards } from '@/components/dashboard/section-cards';
import { useTranslations } from 'next-intl';
import data from "./data.json";
import data from './data.json';
/**
* Admin users page

View File

@ -4,7 +4,7 @@ import { DataTable } from '@/components/dashboard/data-table';
import { SectionCards } from '@/components/dashboard/section-cards';
import { useTranslations } from 'next-intl';
import data from "./data.json";
import data from './data.json';
/**
* Dashboard page

View File

@ -1,9 +1,6 @@
import { DashboardSidebar } from '@/components/dashboard/dashboard-sidebar';
import {
SidebarInset,
SidebarProvider
} from '@/components/ui/sidebar';
import { PropsWithChildren } from 'react';
import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar';
import type { PropsWithChildren } from 'react';
/**
* inspired by dashboard-01
@ -14,16 +11,14 @@ export default function DashboardLayout({ children }: PropsWithChildren) {
<SidebarProvider
style={
{
"--sidebar-width": "calc(var(--spacing) * 72)",
"--header-height": "calc(var(--spacing) * 12)",
'--sidebar-width': 'calc(var(--spacing) * 72)',
'--header-height': 'calc(var(--spacing) * 12)',
} as React.CSSProperties
}
>
<DashboardSidebar variant="inset" />
<SidebarInset>
{children}
</SidebarInset>
<SidebarInset>{children}</SidebarInset>
</SidebarProvider>
);
}

View File

@ -5,9 +5,7 @@ interface BillingLayoutProps {
children: React.ReactNode;
}
export default async function BillingLayout({
children,
}: BillingLayoutProps) {
export default async function BillingLayout({ children }: BillingLayoutProps) {
const t = await getTranslations('Dashboard.settings');
const breadcrumbs = [

View File

@ -1,7 +1,5 @@
import BillingCard from '@/components/settings/billing/billing-card';
export default function BillingPage() {
return (
<BillingCard />
);
return <BillingCard />;
}

View File

@ -5,9 +5,7 @@ interface ProfileLayoutProps {
children: React.ReactNode;
}
export default async function ProfileLayout({
children,
}: ProfileLayoutProps) {
export default async function ProfileLayout({ children }: ProfileLayoutProps) {
const t = await getTranslations('Dashboard.settings');
const breadcrumbs = [

View File

@ -1,8 +1,8 @@
import { ErrorCard } from '@/components/auth/error-card';
import { constructMetadata } from '@/lib/metadata';
import { getUrlWithLocale } from '@/lib/urls/urls';
import { Metadata } from 'next';
import { Locale } from 'next-intl';
import type { Metadata } from 'next';
import type { Locale } from 'next-intl';
import { getTranslations } from 'next-intl/server';
export async function generateMetadata({

View File

@ -1,8 +1,8 @@
import { ForgotPasswordForm } from '@/components/auth/forgot-password-form';
import { constructMetadata } from '@/lib/metadata';
import { getUrlWithLocale } from '@/lib/urls/urls';
import { Metadata } from 'next';
import { Locale } from 'next-intl';
import type { Metadata } from 'next';
import type { Locale } from 'next-intl';
import { getTranslations } from 'next-intl/server';
export async function generateMetadata({
@ -12,12 +12,15 @@ export async function generateMetadata({
}): Promise<Metadata | undefined> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'Metadata' });
const pt = await getTranslations({locale, namespace: 'AuthPage.forgotPassword'});
const pt = await getTranslations({
locale,
namespace: 'AuthPage.forgotPassword',
});
return constructMetadata({
title: pt('title') + ' | ' + t('title'),
description: t('description'),
canonicalUrl: getUrlWithLocale("/auth/forgot-password", locale),
canonicalUrl: getUrlWithLocale('/auth/forgot-password', locale),
});
}

View File

@ -3,8 +3,8 @@ import { LocaleLink } from '@/i18n/navigation';
import { constructMetadata } from '@/lib/metadata';
import { getUrlWithLocale } from '@/lib/urls/urls';
import { Routes } from '@/routes';
import { Metadata } from 'next';
import { Locale } from 'next-intl';
import type { Metadata } from 'next';
import type { Locale } from 'next-intl';
import { getTranslations } from 'next-intl/server';
export async function generateMetadata({
@ -19,7 +19,7 @@ export async function generateMetadata({
return constructMetadata({
title: pt('title') + ' | ' + t('title'),
description: t('description'),
canonicalUrl: getUrlWithLocale("/auth/login", locale),
canonicalUrl: getUrlWithLocale('/auth/login', locale),
});
}
@ -47,4 +47,4 @@ export default async function LoginPage() {
</div>
</div>
);
};
}

View File

@ -3,8 +3,8 @@ import { LocaleLink } from '@/i18n/navigation';
import { constructMetadata } from '@/lib/metadata';
import { getUrlWithLocale } from '@/lib/urls/urls';
import { Routes } from '@/routes';
import { Metadata } from 'next';
import { Locale } from 'next-intl';
import type { Metadata } from 'next';
import type { Locale } from 'next-intl';
import { getTranslations } from 'next-intl/server';
export async function generateMetadata({

View File

@ -1,8 +1,8 @@
import { ResetPasswordForm } from '@/components/auth/reset-password-form';
import { constructMetadata } from '@/lib/metadata';
import { getUrlWithLocale } from '@/lib/urls/urls';
import { Metadata } from 'next';
import { Locale } from 'next-intl';
import type { Metadata } from 'next';
import type { Locale } from 'next-intl';
import { getTranslations } from 'next-intl/server';
export async function generateMetadata({
@ -12,12 +12,15 @@ export async function generateMetadata({
}): Promise<Metadata | undefined> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'Metadata' });
const pt = await getTranslations({locale, namespace: 'AuthPage.resetPassword'});
const pt = await getTranslations({
locale,
namespace: 'AuthPage.resetPassword',
});
return constructMetadata({
title: pt('title') + ' | ' + t('title'),
description: t('description'),
canonicalUrl: getUrlWithLocale("/auth/reset-password", locale),
canonicalUrl: getUrlWithLocale('/auth/reset-password', locale),
});
}

View File

@ -1,31 +1,38 @@
import * as Preview from '@/components/docs';
import { CustomMDXContent } from '@/components/shared/custom-mdx-content';
import { HoverCard, HoverCardContent, HoverCardTrigger, } from '@/components/ui/hover-card';
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from '@/components/ui/hover-card';
import { LOCALES } from '@/i18n/routing';
import { source } from '@/lib/docs/source';
import Link from 'fumadocs-core/link';
import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/page';
import {
DocsBody,
DocsDescription,
DocsPage,
DocsTitle,
} from 'fumadocs-ui/page';
import type { Metadata } from 'next';
import { Locale } from 'next-intl';
import type { Locale } from 'next-intl';
import { notFound } from 'next/navigation';
import { ReactNode } from 'react';
import type { ReactNode } from 'react';
export function generateStaticParams() {
const locales = LOCALES;
const slugParams = source.generateParams();
const params = locales.flatMap(locale =>
slugParams.map(param => ({
const params = locales.flatMap((locale) =>
slugParams.map((param) => ({
locale,
slug: param.slug
slug: param.slug,
}))
);
return params;
}
export async function generateMetadata({
params,
}: DocPageProps) {
export async function generateMetadata({ params }: DocPageProps) {
const { slug, locale } = await params;
const language = locale as string;
const page = source.getPage(slug, language);
@ -54,7 +61,7 @@ export const revalidate = false;
interface DocPageProps {
params: Promise<{
slug?: string[];
locale: Locale
locale: Locale;
}>;
}
@ -64,9 +71,7 @@ interface DocPageProps {
* ref:
* https://github.com/fuma-nama/fumadocs/blob/dev/apps/docs/app/docs/%5B...slug%5D/page.tsx
*/
export default async function DocPage({
params,
}: DocPageProps) {
export default async function DocPage({ params }: DocPageProps) {
const { slug, locale } = await params;
const language = locale as string;
const page = source.getPage(slug, language);
@ -79,18 +84,15 @@ export default async function DocPage({
const preview = page.data.preview;
return (
<DocsPage toc={page.data.toc}
<DocsPage
toc={page.data.toc}
full={page.data.full}
tableOfContent={{
style: "clerk",
style: 'clerk',
}}
>
<DocsTitle>
{page.data.title}
</DocsTitle>
<DocsDescription>
{page.data.description}
</DocsDescription>
<DocsTitle>{page.data.title}</DocsTitle>
<DocsDescription>{page.data.description}</DocsDescription>
<DocsBody>
{/* Preview Rendered Component */}
{preview ? <PreviewRenderer preview={preview} /> : null}

View File

@ -5,11 +5,11 @@ import { websiteConfig } from '@/config/website';
import { docsI18nConfig } from '@/lib/docs/i18n';
import { source } from '@/lib/docs/source';
import { getUrlWithLocale } from '@/lib/urls/urls';
import { I18nProvider, Translations } from 'fumadocs-ui/i18n';
import { I18nProvider, type Translations } from 'fumadocs-ui/i18n';
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
import { BookIcon, HomeIcon } from 'lucide-react';
import { Locale } from 'next-intl';
import type { Locale } from 'next-intl';
import { getTranslations } from 'next-intl/server';
import type { ReactNode } from 'react';
@ -17,10 +17,12 @@ import '@/styles/mdx.css';
// available languages that will be displayed on UI
// make sure `locale` is consistent with your i18n config
const locales = Object.entries(websiteConfig.i18n.locales).map(([locale, data]) => ({
const locales = Object.entries(websiteConfig.i18n.locales).map(
([locale, data]) => ({
name: data.name,
locale,
}));
})
);
interface DocsLayoutProps {
children: ReactNode;
@ -41,7 +43,10 @@ interface DocsLayoutProps {
* ref:
* https://github.com/fuma-nama/fumadocs/blob/dev/apps/docs/content/docs/ui/meta.json
*/
export default async function DocsRootLayout({ children, params }: DocsLayoutProps) {
export default async function DocsRootLayout({
children,
params,
}: DocsLayoutProps) {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'DocsPage' });
@ -81,28 +86,24 @@ export default async function DocsRootLayout({ children, params }: DocsLayoutPro
...(websiteConfig.metadata.social?.twitter
? [
{
type: "icon" as const,
type: 'icon' as const,
icon: <XTwitterIcon />,
text: "X",
text: 'X',
url: websiteConfig.metadata.social.twitter,
secondary: true,
}
},
]
: [])
: []),
],
themeSwitch: {
enabled: true,
mode: 'light-dark-system',
component: <ModeSwitcher />
component: <ModeSwitcher />,
},
};
return (
<I18nProvider
locales={locales}
locale={locale}
translations={translations}
>
<I18nProvider locales={locales} locale={locale} translations={translations}>
<DocsLayout tree={source.pageTree[locale]} {...docsOptions}>
{children}
</DocsLayout>

View File

@ -1,15 +1,20 @@
import { fontBricolageGrotesque, fontNotoSans, fontNotoSansMono, fontNotoSerif } from '@/assets/fonts';
import {
fontBricolageGrotesque,
fontNotoSans,
fontNotoSansMono,
fontNotoSerif,
} from '@/assets/fonts';
import { routing } from '@/i18n/routing';
import { cn } from '@/lib/utils';
import { hasLocale, Locale, NextIntlClientProvider } from 'next-intl';
import { type Locale, NextIntlClientProvider, hasLocale } from 'next-intl';
import { notFound } from 'next/navigation';
import { ReactNode } from 'react';
import type { ReactNode } from 'react';
import { Toaster } from 'sonner';
import { Providers } from './providers';
import '@/styles/globals.css';
import { TailwindIndicator } from '@/components/layout/tailwind-indicator';
import { Analytics } from '@/analytics/analytics';
import { TailwindIndicator } from '@/components/layout/tailwind-indicator';
interface LocaleLayoutProps {
children: ReactNode;

View File

@ -6,7 +6,7 @@ import { TooltipProvider } from '@/components/ui/tooltip';
import { websiteConfig } from '@/config/website';
import { RootProvider } from 'fumadocs-ui/provider';
import { ThemeProvider, useTheme } from 'next-themes';
import { PropsWithChildren } from 'react';
import type { PropsWithChildren } from 'react';
/**
* Providers
@ -21,7 +21,7 @@ import { PropsWithChildren } from 'react';
*/
export function Providers({ children }: PropsWithChildren) {
const theme = useTheme();
const defaultMode = websiteConfig.metadata.mode?.defaultMode ?? "system";
const defaultMode = websiteConfig.metadata.mode?.defaultMode ?? 'system';
return (
<ThemeProvider
@ -33,9 +33,7 @@ export function Providers({ children }: PropsWithChildren) {
<ActiveThemeProvider>
<RootProvider theme={theme}>
<TooltipProvider>
<PaymentProvider>
{children}
</PaymentProvider>
<PaymentProvider>{children}</PaymentProvider>
</TooltipProvider>
</RootProvider>
</ActiveThemeProvider>

View File

@ -1,7 +1,7 @@
import { docsI18nConfig } from '@/lib/docs/i18n';
import { source } from '@/lib/docs/source';
import { createTokenizer } from '@orama/tokenizers/mandarin';
import { createI18nSearchAPI } from 'fumadocs-core/search/server';
import { docsI18nConfig } from '@/lib/docs/i18n';
/**
* Fumadocs i18n search configuration
@ -26,7 +26,7 @@ const searchAPI = createI18nSearchAPI('advanced', {
id: page.url,
url: page.url,
locale: language,
})),
}))
),
// Configure special language tokenizers and search options
@ -73,7 +73,10 @@ export const GET = async (request: Request) => {
console.log('search, referer pathname:', refererUrl.pathname);
const refererPathParts = refererUrl.pathname.split('/').filter(Boolean);
console.log('search, referer path parts:', refererPathParts);
if (refererPathParts.length > 0 && docsI18nConfig.languages.includes(refererPathParts[0])) {
if (
refererPathParts.length > 0 &&
docsI18nConfig.languages.includes(refererPathParts[0])
) {
locale = refererPathParts[0];
console.log(`search, detected locale from referer: ${locale}`);
}

View File

@ -1,7 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { StorageError } from '@/storage/types';
import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { type NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
try {
@ -68,10 +68,7 @@ export async function POST(request: NextRequest) {
console.error('Error getting file URL:', error);
if (error instanceof StorageError) {
return NextResponse.json(
{ error: error.message },
{ status: 500 }
);
return NextResponse.json({ error: error.message }, { status: 500 });
}
return NextResponse.json(

View File

@ -1,7 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { getPresignedUploadUrl } from '@/storage';
import { randomUUID } from 'crypto';
import { getPresignedUploadUrl } from '@/storage';
import { StorageError } from '@/storage/types';
import { type NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
try {
@ -47,10 +47,7 @@ export async function POST(request: NextRequest) {
console.error('Error generating pre-signed URL:', error);
if (error instanceof StorageError) {
return NextResponse.json(
{ error: error.message },
{ status: 500 }
);
return NextResponse.json({ error: error.message }, { status: 500 });
}
return NextResponse.json(

View File

@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from 'next/server';
import { uploadFile } from '@/storage';
import { StorageError } from '@/storage/types';
import { type NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
try {
@ -9,10 +9,7 @@ export async function POST(request: NextRequest) {
const folder = formData.get('folder') as string | null;
if (!file) {
return NextResponse.json(
{ error: 'No file provided' },
{ status: 400 }
);
return NextResponse.json({ error: 'No file provided' }, { status: 400 });
}
// Validate file size (max 10MB)
@ -49,10 +46,7 @@ export async function POST(request: NextRequest) {
console.error('Error uploading file:', error);
if (error instanceof StorageError) {
return NextResponse.json(
{ error: error.message },
{ status: 500 }
);
return NextResponse.json({ error: error.message }, { status: 500 });
}
return NextResponse.json(

View File

@ -1,5 +1,5 @@
import { handleWebhookEvent } from '@/payment';
import { NextRequest, NextResponse } from 'next/server';
import { type NextRequest, NextResponse } from 'next/server';
/**
* Stripe webhook handler

View File

@ -1,4 +1,4 @@
import { ReactNode } from 'react';
import type { ReactNode } from 'react';
interface Props {
children: ReactNode;

View File

@ -1,5 +1,5 @@
import { defaultMessages } from '@/i18n/messages';
import { type MetadataRoute } from 'next';
import type { MetadataRoute } from 'next';
/**
* Generates the Web App Manifest for the application

View File

@ -15,7 +15,7 @@ export default function GlobalNotFound() {
return (
<html lang="en">
<body>
<Error statusCode={404} />;
<Error statusCode={404} />
</body>
</html>
);

View File

@ -1,4 +1,4 @@
import { MetadataRoute } from 'next';
import type { MetadataRoute } from 'next';
import { getBaseUrl } from '../lib/urls/urls';
export default function robots(): MetadataRoute.Robots {

View File

@ -2,8 +2,8 @@ import { getLocalePathname } from '@/i18n/navigation';
import { routing } from '@/i18n/routing';
import { source } from '@/lib/docs/source';
import { allCategories, allPosts } from 'content-collections';
import { MetadataRoute } from 'next';
import { Locale } from 'next-intl';
import type { MetadataRoute } from 'next';
import type { Locale } from 'next-intl';
import { getBaseUrl } from '../lib/urls/urls';
type Href = Parameters<typeof getLocalePathname>[0]['href'];
@ -37,45 +37,53 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const sitemapList: MetadataRoute.Sitemap = []; // final result
// add static routes
sitemapList.push(...staticRoutes.flatMap((route) => {
sitemapList.push(
...staticRoutes.flatMap((route) => {
return routing.locales.map((locale) => ({
url: getUrl(route, locale),
lastModified: new Date(),
priority: 1,
changeFrequency: 'weekly' as const
}));
changeFrequency: 'weekly' as const,
}));
})
);
// add categories
sitemapList.push(...allCategories.flatMap((category: { slug: string }) =>
sitemapList.push(
...allCategories.flatMap((category: { slug: string }) =>
routing.locales.map((locale) => ({
url: getUrl(`/blog/category/${category.slug}`, locale),
lastModified: new Date(),
priority: 0.8,
changeFrequency: 'weekly' as const
changeFrequency: 'weekly' as const,
}))
));
)
);
// add posts
sitemapList.push(...allPosts.flatMap((post: { slugAsParams: string }) =>
sitemapList.push(
...allPosts.flatMap((post: { slugAsParams: string }) =>
routing.locales.map((locale) => ({
url: getUrl(`/blog/${post.slugAsParams}`, locale),
lastModified: new Date(),
priority: 0.8,
changeFrequency: 'weekly' as const
changeFrequency: 'weekly' as const,
}))
));
)
);
// add docs
const docsParams = source.generateParams();
sitemapList.push(...docsParams.flatMap(param =>
sitemapList.push(
...docsParams.flatMap((param) =>
routing.locales.map((locale) => ({
url: getUrl(`/docs/${param.slug.join('/')}`, locale),
lastModified: new Date(),
priority: 0.8,
changeFrequency: 'weekly' as const
changeFrequency: 'weekly' as const,
}))
));
)
);
return sitemapList;
}

View File

@ -1,4 +1,9 @@
import { Bricolage_Grotesque, Noto_Sans, Noto_Sans_Mono, Noto_Serif } from 'next/font/google';
import {
Bricolage_Grotesque,
Noto_Sans,
Noto_Sans_Mono,
Noto_Serif,
} from 'next/font/google';
/**
* This file shows how to customize the font by using local font or google font

View File

@ -35,9 +35,7 @@ export const AuthCard = ({
</LocaleLink>
<CardDescription>{headerLabel}</CardDescription>
</CardHeader>
<CardContent>
{children}
</CardContent>
<CardContent>{children}</CardContent>
<CardFooter>
<BottomLink label={bottomButtonLabel} href={bottomButtonHref} />
</CardFooter>

View File

@ -8,17 +8,12 @@ interface DividerWithTextProps {
/**
* A horizontal divider with text in the middle
*/
export const DividerWithText = ({
text,
className,
}: DividerWithTextProps) => {
export const DividerWithText = ({ text, className }: DividerWithTextProps) => {
return (
<div className={cn('relative flex items-center', className)}>
<div className="grow border-t border-border"></div>
<span className="shrink mx-4 text-sm text-muted-foreground">
{text}
</span>
<div className="grow border-t border-border"></div>
<div className="grow border-t border-border" />
<span className="shrink mx-4 text-sm text-muted-foreground">{text}</span>
<div className="grow border-t border-border" />
</div>
);
};

View File

@ -118,9 +118,7 @@ export const ForgotPasswordForm = ({ className }: { className?: string }) => {
type="submit"
className="w-full cursor-pointer"
>
{isPending && (
<Loader2Icon className="mr-2 size-4 animate-spin" />
)}
{isPending && <Loader2Icon className="mr-2 size-4 animate-spin" />}
<span>{t('send')}</span>
</Button>
</form>

View File

@ -32,14 +32,20 @@ export interface LoginFormProps {
callbackUrl?: string;
}
export const LoginForm = ({ className, callbackUrl: propCallbackUrl }: LoginFormProps) => {
export const LoginForm = ({
className,
callbackUrl: propCallbackUrl,
}: LoginFormProps) => {
const t = useTranslations('AuthPage.login');
const searchParams = useSearchParams();
const urlError = searchParams.get('error');
const paramCallbackUrl = searchParams.get('callbackUrl');
// Use prop callback URL or param callback URL if provided, otherwise use the default login redirect
const locale = useLocale();
const defaultCallbackUrl = getUrlWithLocaleInCallbackUrl(DEFAULT_LOGIN_REDIRECT, locale);
const defaultCallbackUrl = getUrlWithLocaleInCallbackUrl(
DEFAULT_LOGIN_REDIRECT,
locale
);
const callbackUrl = propCallbackUrl || paramCallbackUrl || defaultCallbackUrl;
console.log('login form, callbackUrl', callbackUrl);
@ -158,7 +164,7 @@ export const LoginForm = ({ className, callbackUrl: propCallbackUrl }: LoginForm
{...field}
disabled={isPending}
placeholder="******"
type={showPassword ? "text" : "password"}
type={showPassword ? 'text' : 'password'}
className="pr-10"
/>
<Button
@ -193,9 +199,7 @@ export const LoginForm = ({ className, callbackUrl: propCallbackUrl }: LoginForm
type="submit"
className="w-full flex items-center justify-center gap-2 cursor-pointer"
>
{isPending && (
<Loader2Icon className="mr-2 size-4 animate-spin" />
)}
{isPending && <Loader2Icon className="mr-2 size-4 animate-spin" />}
<span>{t('signIn')}</span>
</Button>
</form>

View File

@ -27,8 +27,6 @@ export const LoginWrapper = ({
callbackUrl,
}: LoginWrapperProps) => {
const router = useLocaleRouter();
const pathname = useLocalePathname();
const searchParams = useSearchParams();
const [isModalOpen, setIsModalOpen] = useState(false);
const handleLogin = () => {
@ -40,11 +38,6 @@ export const LoginWrapper = ({
router.push(loginPath);
};
// Close the modal on route change
useEffect(() => {
setIsModalOpen(false);
}, [pathname, searchParams]);
if (mode === 'modal') {
return (
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>

View File

@ -29,13 +29,18 @@ interface RegisterFormProps {
callbackUrl?: string;
}
export const RegisterForm = ({ callbackUrl: propCallbackUrl }: RegisterFormProps) => {
export const RegisterForm = ({
callbackUrl: propCallbackUrl,
}: RegisterFormProps) => {
const t = useTranslations('AuthPage.register');
const searchParams = useSearchParams();
const paramCallbackUrl = searchParams.get('callbackUrl');
// Use prop callback URL or param callback URL if provided, otherwise use the default login redirect
const locale = useLocale();
const defaultCallbackUrl = getUrlWithLocaleInCallbackUrl(DEFAULT_LOGIN_REDIRECT, locale);
const defaultCallbackUrl = getUrlWithLocaleInCallbackUrl(
DEFAULT_LOGIN_REDIRECT,
locale
);
const callbackUrl = propCallbackUrl || paramCallbackUrl || defaultCallbackUrl;
console.log('register form, callbackUrl', callbackUrl);
@ -159,7 +164,7 @@ export const RegisterForm = ({ callbackUrl: propCallbackUrl }: RegisterFormProps
{...field}
disabled={isPending}
placeholder="******"
type={showPassword ? "text" : "password"}
type={showPassword ? 'text' : 'password'}
className="pr-10"
/>
<Button
@ -194,9 +199,7 @@ export const RegisterForm = ({ callbackUrl: propCallbackUrl }: RegisterFormProps
type="submit"
className="cursor-pointer w-full flex items-center justify-center gap-2"
>
{isPending && (
<Loader2Icon className="mr-2 size-4 animate-spin" />
)}
{isPending && <Loader2Icon className="mr-2 size-4 animate-spin" />}
<span>{t('signUp')}</span>
</Button>
</form>

View File

@ -117,7 +117,7 @@ export const ResetPasswordForm = () => {
{...field}
disabled={isPending}
placeholder="******"
type={showPassword ? "text" : "password"}
type={showPassword ? 'text' : 'password'}
className="pr-10"
/>
<Button
@ -152,9 +152,7 @@ export const ResetPasswordForm = () => {
type="submit"
className="w-full cursor-pointer"
>
{isPending && (
<Loader2Icon className="mr-2 size-4 animate-spin" />
)}
{isPending && <Loader2Icon className="mr-2 size-4 animate-spin" />}
<span>{t('reset')}</span>
</Button>
</form>

View File

@ -19,13 +19,18 @@ interface SocialLoginButtonProps {
/**
* social login buttons
*/
export const SocialLoginButton = ({ callbackUrl: propCallbackUrl }: SocialLoginButtonProps) => {
export const SocialLoginButton = ({
callbackUrl: propCallbackUrl,
}: SocialLoginButtonProps) => {
const t = useTranslations('AuthPage.login');
const searchParams = useSearchParams();
const paramCallbackUrl = searchParams.get('callbackUrl');
// Use prop callback URL or param callback URL if provided, otherwise use the default login redirect
const locale = useLocale();
const defaultCallbackUrl = getUrlWithLocaleInCallbackUrl(DEFAULT_LOGIN_REDIRECT, locale);
const defaultCallbackUrl = getUrlWithLocaleInCallbackUrl(
DEFAULT_LOGIN_REDIRECT,
locale
);
const callbackUrl = propCallbackUrl || paramCallbackUrl || defaultCallbackUrl;
const [isLoading, setIsLoading] = useState<'google' | 'github' | null>(null);
console.log('social login button, callbackUrl', callbackUrl);

View File

@ -12,9 +12,7 @@ export default function CallToActionSection() {
<h2 className="text-balance text-4xl font-semibold lg:text-5xl">
{t('title')}
</h2>
<p className="mt-4">
{t('description')}
</p>
<p className="mt-4">{t('description')}</p>
<div className="mt-12 flex flex-wrap justify-center gap-4">
<Button asChild size="lg">

View File

@ -6,7 +6,7 @@ import {
AccordionItem,
AccordionTrigger,
} from '@/components/ui/accordion';
import { IconName } from 'lucide-react/dynamic';
import type { IconName } from 'lucide-react/dynamic';
import { useLocale, useTranslations } from 'next-intl';
type FAQItem = {

View File

@ -1,5 +1,6 @@
'use client';
import { BorderBeam } from '@/components/magicui/border-beam';
import {
Accordion,
AccordionContent,
@ -12,11 +13,10 @@ import {
Fingerprint,
IdCard,
} from 'lucide-react';
import { AnimatePresence, motion } from 'motion/react';
import { useTranslations } from 'next-intl';
import Image from 'next/image';
import { useState } from 'react';
import { motion, AnimatePresence } from 'motion/react';
import { BorderBeam } from '@/components/magicui/border-beam';
import { useTranslations } from 'next-intl';
/**
* https://nsui.irung.me/features
@ -48,15 +48,13 @@ export default function Features2Section() {
return (
<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="relative z-10 mx-auto max-w-2xl space-y-6 text-center">
<h2 className="text-balance text-4xl lg:text-5xl font-semibold">
{t('title')}
</h2>
<p>
{t('description')}
</p>
<p>{t('description')}</p>
</div>
<div className="grid gap-12 sm:px-12 md:grid-cols-2 lg:grid-cols-12 md:gap-12 lg:gap-24 lg:px-0">

View File

@ -1,5 +1,6 @@
'use client';
import { BorderBeam } from '@/components/magicui/border-beam';
import {
Accordion,
AccordionContent,
@ -12,11 +13,10 @@ import {
Fingerprint,
IdCard,
} from 'lucide-react';
import { AnimatePresence, motion } from 'motion/react';
import { useTranslations } from 'next-intl';
import Image from 'next/image';
import { useState } from 'react';
import { motion, AnimatePresence } from 'motion/react';
import { BorderBeam } from '@/components/magicui/border-beam';
import { useTranslations } from 'next-intl';
/**
* https://nsui.irung.me/features
@ -54,13 +54,10 @@ export default function Features2Section() {
<h2 className="text-balance text-4xl lg:text-5xl font-semibold">
{t('title')}
</h2>
<p>
{t('description')}
</p>
<p>{t('description')}</p>
</div>
<div className="grid gap-12 sm:px-12 md:grid-cols-2 lg:grid-cols-12 md:gap-12 lg:gap-24 lg:px-0">
<div className="bg-background w-full relative flex overflow-hidden rounded-2xl border p-2 md:h-auto lg:col-span-7">
<div className="aspect-76/59 bg-background relative w-full rounded-2xl">
<AnimatePresence mode="wait">

View File

@ -1,4 +1,9 @@
import { ActivityIcon, DraftingCompassIcon, MailIcon, ZapIcon } from 'lucide-react';
import {
ActivityIcon,
DraftingCompassIcon,
MailIcon,
ZapIcon,
} from 'lucide-react';
import { useTranslations } from 'next-intl';
import Image from 'next/image';
@ -15,12 +20,8 @@ export default function Features3Section() {
<div className="grid items-center gap-12 md:grid-cols-2 md:gap-12 lg:grid-cols-5 lg:gap-24">
<div className="lg:col-span-2">
<div className="md:pr-6 lg:pr-0">
<h2 className="text-4xl font-semibold">
{t('title')}
</h2>
<p className="mt-6">
{t('description')}
</p>
<h2 className="text-4xl font-semibold">{t('title')}</h2>
<p className="mt-6">{t('description')}</p>
</div>
<ul className="mt-8 divide-y border-y *:flex *:items-center *:gap-3 *:py-3">

View File

@ -1,4 +1,9 @@
import { ActivityIcon, DraftingCompassIcon, MailIcon, ZapIcon } from 'lucide-react';
import {
ActivityIcon,
DraftingCompassIcon,
MailIcon,
ZapIcon,
} from 'lucide-react';
import { useTranslations } from 'next-intl';
import Image from 'next/image';
@ -36,12 +41,8 @@ export default function Features4Section() {
<div className="lg:col-span-2">
<div className="md:pr-6 lg:pr-0">
<h2 className="text-4xl font-semibold">
{t('title')}
</h2>
<p className="mt-6">
{t('description')}
</p>
<h2 className="text-4xl font-semibold">{t('title')}</h2>
<p className="mt-6">{t('description')}</p>
</div>
<ul className="mt-8 divide-y border-y *:flex *:items-center *:gap-3 *:py-3">

View File

@ -1,4 +1,11 @@
import { CpuIcon, FingerprintIcon, PencilIcon, Settings2Icon, SparklesIcon, ZapIcon } from 'lucide-react';
import {
CpuIcon,
FingerprintIcon,
PencilIcon,
Settings2Icon,
SparklesIcon,
ZapIcon,
} from 'lucide-react';
import { useTranslations } from 'next-intl';
/**
@ -15,16 +22,16 @@ export default function Features5Section() {
<h2 className="text-balance text-4xl lg:text-5xl font-semibold">
{t('title')}
</h2>
<p className="mt-4">
{t('description')}
</p>
<p className="mt-4">{t('description')}</p>
</div>
<div className="relative mx-auto grid divide-x divide-y border *:p-8 sm:grid-cols-2 lg:grid-cols-3">
<div className="space-y-2">
<div className="flex items-center gap-2">
<ZapIcon className="size-4" />
<h3 className="text-base font-medium">{t('items.item-1.title')}</h3>
<h3 className="text-base font-medium">
{t('items.item-1.title')}
</h3>
</div>
<p className="text-sm text-muted-foreground mt-4">
{t('items.item-1.description')}
@ -33,7 +40,9 @@ export default function Features5Section() {
<div className="space-y-2">
<div className="flex items-center gap-2">
<CpuIcon className="size-4" />
<h3 className="text-base font-medium">{t('items.item-2.title')}</h3>
<h3 className="text-base font-medium">
{t('items.item-2.title')}
</h3>
</div>
<p className="text-sm text-muted-foreground mt-4">
{t('items.item-2.description')}
@ -43,7 +52,9 @@ export default function Features5Section() {
<div className="flex items-center gap-2">
<FingerprintIcon className="size-4" />
<h3 className="text-base font-medium">{t('items.item-3.title')}</h3>
<h3 className="text-base font-medium">
{t('items.item-3.title')}
</h3>
</div>
<p className="text-sm text-muted-foreground mt-4">
{t('items.item-3.description')}
@ -53,7 +64,9 @@ export default function Features5Section() {
<div className="flex items-center gap-2">
<PencilIcon className="size-4" />
<h3 className="text-base font-medium">{t('items.item-4.title')}</h3>
<h3 className="text-base font-medium">
{t('items.item-4.title')}
</h3>
</div>
<p className="text-sm text-muted-foreground mt-4">
{t('items.item-4.description')}
@ -63,7 +76,9 @@ export default function Features5Section() {
<div className="flex items-center gap-2">
<Settings2Icon className="size-4" />
<h3 className="text-base font-medium">{t('items.item-5.title')}</h3>
<h3 className="text-base font-medium">
{t('items.item-5.title')}
</h3>
</div>
<p className="text-sm text-muted-foreground mt-4">
{t('items.item-5.description')}
@ -73,7 +88,9 @@ export default function Features5Section() {
<div className="flex items-center gap-2">
<SparklesIcon className="size-4" />
<h3 className="text-base font-medium">{t('items.item-6.title')}</h3>
<h3 className="text-base font-medium">
{t('items.item-6.title')}
</h3>
</div>
<p className="text-sm text-muted-foreground mt-4">
{t('items.item-6.description')}

View File

@ -119,9 +119,7 @@ export default function HeroSection() {
className="rounded-xl px-5 text-base"
>
<LocaleLink href={linkPrimary}>
<span className="text-nowrap">
{t('primary')}
</span>
<span className="text-nowrap">{t('primary')}</span>
</LocaleLink>
</Button>
</div>
@ -133,9 +131,7 @@ export default function HeroSection() {
className="h-10.5 rounded-xl px-5"
>
<LocaleLink href={linkSecondary}>
<span className="text-nowrap">
{t('secondary')}
</span>
<span className="text-nowrap">{t('secondary')}</span>
</LocaleLink>
</Button>
</AnimatedGroup>

View File

@ -11,7 +11,7 @@ import { Card } from '@/components/ui/card';
import { LocaleLink } from '@/i18n/navigation';
import { ChevronRight } from 'lucide-react';
import { useTranslations } from 'next-intl';
import * as React from 'react';
import type * as React from 'react';
export default function IntegrationSection() {
const t = useTranslations('HomePage.integration');
@ -24,9 +24,7 @@ export default function IntegrationSection() {
<h2 className="text-balance text-3xl font-semibold md:text-4xl">
{t('title')}
</h2>
<p className="text-muted-foreground mt-6">
{t('description')}
</p>
<p className="text-muted-foreground mt-6">{t('description')}</p>
</div>
<div className="mt-12 grid gap-3 sm:grid-cols-2 lg:grid-cols-3">

View File

@ -59,9 +59,7 @@ export default function Integration2Section() {
<h2 className="text-balance text-3xl font-semibold md:text-4xl">
{t('title')}
</h2>
<p className="text-muted-foreground">
{t('description')}
</p>
<p className="text-muted-foreground">{t('description')}</p>
<div className="mt-12 flex flex-wrap justify-start gap-4">
<Button asChild size="lg">

View File

@ -1,4 +1,4 @@
import { useTranslations } from "next-intl";
import { useTranslations } from 'next-intl';
export default function LogoCloudSection() {
const t = useTranslations('HomePage.logocloud');
@ -6,9 +6,7 @@ export default function LogoCloudSection() {
return (
<section className="py-16">
<div className="mx-auto max-w-5xl px-6">
<h2 className="text-center text-xl font-medium">
{t('title')}
</h2>
<h2 className="text-center text-xl font-medium">{t('title')}</h2>
<div className="mx-auto mt-20 flex max-w-4xl flex-wrap items-center justify-center gap-x-12 gap-y-8 sm:gap-x-16 sm:gap-y-12">
<img
className="h-4 w-fit dark:invert"
@ -85,4 +83,3 @@ export default function LogoCloudSection() {
</section>
);
}

View File

@ -1,5 +1,5 @@
import { PricingTable } from "@/components/pricing/pricing-table";
import { useTranslations } from "next-intl";
import { PricingTable } from '@/components/pricing/pricing-table';
import { useTranslations } from 'next-intl';
export default function PricingSection() {
const t = useTranslations('HomePage.pricing');
@ -11,9 +11,7 @@ export default function PricingSection() {
<h2 className="text-balance text-4xl lg:text-5xl font-semibold">
{t('title')}
</h2>
<p className="mt-4">
{t('description')}
</p>
<p className="mt-4">{t('description')}</p>
</div>
<PricingTable />

View File

@ -1,4 +1,4 @@
import { useTranslations } from "next-intl";
import { useTranslations } from 'next-intl';
export default function StatsSection() {
const t = useTranslations('HomePage.stats');
@ -7,12 +7,8 @@ export default function StatsSection() {
<section className="py-12 md:py-20 w-full bg-muted dark:bg-background">
<div className="mx-auto max-w-5xl space-y-8 px-6 md:space-y-16">
<div className="relative z-10 mx-auto max-w-xl space-y-6 text-center">
<h2 className="text-4xl font-medium lg:text-5xl">
{t('title')}
</h2>
<p>
{t('description')}
</p>
<h2 className="text-4xl font-medium lg:text-5xl">{t('title')}</h2>
<p>{t('description')}</p>
</div>
<div className="grid gap-12 divide-y-0 *:text-center md:grid-cols-3 md:gap-2 md:divide-x">

View File

@ -95,7 +95,7 @@ export default function TestimonialsSection() {
role: t('items.item-12.role'),
image: t('items.item-12.image'),
quote: t('items.item-12.quote'),
}
},
];
const testimonialChunks = chunkArray(
@ -111,9 +111,7 @@ export default function TestimonialsSection() {
<h2 className="text-title text-4xl lg:text-5xl font-semibold">
{t('title')}
</h2>
<p className="text-body mt-6">
{t('description')}
</p>
<p className="text-body mt-6">{t('description')}</p>
</div>
<div className="mt-8 grid gap-3 sm:grid-cols-2 md:mt-12 lg:grid-cols-3">
{testimonialChunks.map((chunk, chunkIndex) => (

View File

@ -1,8 +1,8 @@
import { Skeleton } from '@/components/ui/skeleton';
import { PLACEHOLDER_IMAGE } from '@/lib/constants';
import { LocaleLink } from '@/i18n/navigation';
import { PLACEHOLDER_IMAGE } from '@/lib/constants';
import { formatDate } from '@/lib/formatter';
import { Post } from 'content-collections';
import type { Post } from 'content-collections';
import Image from 'next/image';
interface BlogCardProps {

View File

@ -1,5 +1,5 @@
import Container from '@/components/layout/container';
import { Category } from 'content-collections';
import type { Category } from 'content-collections';
import { BlogCategoryListDesktop } from './blog-category-list-desktop';
import { BlogCategoryListMobile } from './blog-category-list-mobile';

View File

@ -1,11 +1,11 @@
'use client';
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
import { cn } from '@/lib/utils';
import { Category } from 'content-collections';
import { LocaleLink } from '@/i18n/navigation';
import { useParams } from 'next/navigation';
import { cn } from '@/lib/utils';
import type { Category } from 'content-collections';
import { useTranslations } from 'next-intl';
import { useParams } from 'next/navigation';
export type BlogCategoryListDesktopProps = {
categoryList: Category[];
@ -52,7 +52,10 @@ export function BlogCategoryListDesktop({
)}
aria-label={`Toggle blog category of ${category.name}`}
>
<LocaleLink href={`/blog/category/${category.slug}`} className="px-4">
<LocaleLink
href={`/blog/category/${category.slug}`}
className="px-4"
>
<h2>{category.name}</h2>
</LocaleLink>
</ToggleGroupItem>

View File

@ -9,7 +9,7 @@ import {
DrawerTitle,
DrawerTrigger,
} from '@/components/ui/drawer';
import { Category } from 'content-collections';
import type { Category } from 'content-collections';
import { LayoutListIcon } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { useParams } from 'next/navigation';

View File

@ -1,6 +1,6 @@
import BlogCard, { BlogCardSkeleton } from '@/components/blog/blog-card';
import { websiteConfig } from '@/config/website';
import { Post } from 'content-collections';
import type { Post } from 'content-collections';
interface BlogGridProps {
posts: Post[];

View File

@ -1,4 +1,4 @@
"use client";
'use client';
import { sendMessageAction } from '@/actions/send-message';
import { FormError } from '@/components/shared/form-error';
@ -9,7 +9,7 @@ import {
CardDescription,
CardFooter,
CardHeader,
CardTitle
CardTitle,
} from '@/components/ui/card';
import {
Form,
@ -17,13 +17,13 @@ import {
FormField,
FormItem,
FormLabel,
FormMessage
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { zodResolver } from '@hookform/resolvers/zod';
import { useTranslations } from 'next-intl';
import { useTransition, useState } from 'react';
import { useState, useTransition } from 'react';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
import { z } from 'zod';
@ -39,13 +39,8 @@ export function ContactFormCard() {
// Create a schema for contact form validation
const formSchema = z.object({
name: z
.string()
.min(3, t('nameMinLength'))
.max(30, t('nameMaxLength')),
email: z
.string()
.email(t('emailValidation')),
name: z.string().min(3, t('nameMinLength')).max(30, t('nameMaxLength')),
email: z.string().email(t('emailValidation')),
message: z
.string()
.min(10, t('messageMinLength'))
@ -93,12 +88,8 @@ export function ContactFormCard() {
return (
<Card className="mx-auto max-w-lg overflow-hidden pt-6 pb-0">
<CardHeader>
<CardTitle className="text-lg font-semibold">
{t('title')}
</CardTitle>
<CardDescription>
{t('description')}
</CardDescription>
<CardTitle className="text-lg font-semibold">{t('title')}</CardTitle>
<CardDescription>{t('description')}</CardDescription>
</CardHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col">
@ -110,10 +101,7 @@ export function ContactFormCard() {
<FormItem>
<FormLabel>{t('name')}</FormLabel>
<FormControl>
<Input
placeholder={t('name')}
{...field}
/>
<Input placeholder={t('name')} {...field} />
</FormControl>
<FormMessage />
</FormItem>
@ -127,11 +115,7 @@ export function ContactFormCard() {
<FormItem>
<FormLabel>{t('email')}</FormLabel>
<FormControl>
<Input
type="email"
placeholder={t('email')}
{...field}
/>
<Input type="email" placeholder={t('email')} {...field} />
</FormControl>
<FormMessage />
</FormItem>
@ -145,11 +129,7 @@ export function ContactFormCard() {
<FormItem>
<FormLabel>{t('message')}</FormLabel>
<FormControl>
<Textarea
placeholder={t('message')}
rows={3}
{...field}
/>
<Textarea placeholder={t('message')} rows={3} {...field} />
</FormControl>
<FormMessage />
</FormItem>

View File

@ -1,7 +1,5 @@
"use client";
'use client';
import * as React from "react";
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts";
import {
Card,
CardAction,
@ -9,159 +7,158 @@ import {
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
} from '@/components/ui/card';
import {
ChartConfig,
type ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart";
} from '@/components/ui/chart';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
ToggleGroup,
ToggleGroupItem,
} from "@/components/ui/toggle-group";
import { useIsMobile } from "@/hooks/use-mobile";
} from '@/components/ui/select';
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
import { useIsMobile } from '@/hooks/use-mobile';
import * as React from 'react';
import { Area, AreaChart, CartesianGrid, XAxis } from 'recharts';
export const description = "An interactive area chart"
export const description = 'An interactive area chart';
const chartData = [
{ date: "2024-04-01", desktop: 222, mobile: 150 },
{ date: "2024-04-02", desktop: 97, mobile: 180 },
{ date: "2024-04-03", desktop: 167, mobile: 120 },
{ date: "2024-04-04", desktop: 242, mobile: 260 },
{ date: "2024-04-05", desktop: 373, mobile: 290 },
{ date: "2024-04-06", desktop: 301, mobile: 340 },
{ date: "2024-04-07", desktop: 245, mobile: 180 },
{ date: "2024-04-08", desktop: 409, mobile: 320 },
{ date: "2024-04-09", desktop: 59, mobile: 110 },
{ date: "2024-04-10", desktop: 261, mobile: 190 },
{ date: "2024-04-11", desktop: 327, mobile: 350 },
{ date: "2024-04-12", desktop: 292, mobile: 210 },
{ date: "2024-04-13", desktop: 342, mobile: 380 },
{ date: "2024-04-14", desktop: 137, mobile: 220 },
{ date: "2024-04-15", desktop: 120, mobile: 170 },
{ date: "2024-04-16", desktop: 138, mobile: 190 },
{ date: "2024-04-17", desktop: 446, mobile: 360 },
{ date: "2024-04-18", desktop: 364, mobile: 410 },
{ date: "2024-04-19", desktop: 243, mobile: 180 },
{ date: "2024-04-20", desktop: 89, mobile: 150 },
{ date: "2024-04-21", desktop: 137, mobile: 200 },
{ date: "2024-04-22", desktop: 224, mobile: 170 },
{ date: "2024-04-23", desktop: 138, mobile: 230 },
{ date: "2024-04-24", desktop: 387, mobile: 290 },
{ date: "2024-04-25", desktop: 215, mobile: 250 },
{ date: "2024-04-26", desktop: 75, mobile: 130 },
{ date: "2024-04-27", desktop: 383, mobile: 420 },
{ date: "2024-04-28", desktop: 122, mobile: 180 },
{ date: "2024-04-29", desktop: 315, mobile: 240 },
{ date: "2024-04-30", desktop: 454, mobile: 380 },
{ date: "2024-05-01", desktop: 165, mobile: 220 },
{ date: "2024-05-02", desktop: 293, mobile: 310 },
{ date: "2024-05-03", desktop: 247, mobile: 190 },
{ date: "2024-05-04", desktop: 385, mobile: 420 },
{ date: "2024-05-05", desktop: 481, mobile: 390 },
{ date: "2024-05-06", desktop: 498, mobile: 520 },
{ date: "2024-05-07", desktop: 388, mobile: 300 },
{ date: "2024-05-08", desktop: 149, mobile: 210 },
{ date: "2024-05-09", desktop: 227, mobile: 180 },
{ date: "2024-05-10", desktop: 293, mobile: 330 },
{ date: "2024-05-11", desktop: 335, mobile: 270 },
{ date: "2024-05-12", desktop: 197, mobile: 240 },
{ date: "2024-05-13", desktop: 197, mobile: 160 },
{ date: "2024-05-14", desktop: 448, mobile: 490 },
{ date: "2024-05-15", desktop: 473, mobile: 380 },
{ date: "2024-05-16", desktop: 338, mobile: 400 },
{ date: "2024-05-17", desktop: 499, mobile: 420 },
{ date: "2024-05-18", desktop: 315, mobile: 350 },
{ date: "2024-05-19", desktop: 235, mobile: 180 },
{ date: "2024-05-20", desktop: 177, mobile: 230 },
{ date: "2024-05-21", desktop: 82, mobile: 140 },
{ date: "2024-05-22", desktop: 81, mobile: 120 },
{ date: "2024-05-23", desktop: 252, mobile: 290 },
{ date: "2024-05-24", desktop: 294, mobile: 220 },
{ date: "2024-05-25", desktop: 201, mobile: 250 },
{ date: "2024-05-26", desktop: 213, mobile: 170 },
{ date: "2024-05-27", desktop: 420, mobile: 460 },
{ date: "2024-05-28", desktop: 233, mobile: 190 },
{ date: "2024-05-29", desktop: 78, mobile: 130 },
{ date: "2024-05-30", desktop: 340, mobile: 280 },
{ date: "2024-05-31", desktop: 178, mobile: 230 },
{ date: "2024-06-01", desktop: 178, mobile: 200 },
{ date: "2024-06-02", desktop: 470, mobile: 410 },
{ date: "2024-06-03", desktop: 103, mobile: 160 },
{ date: "2024-06-04", desktop: 439, mobile: 380 },
{ date: "2024-06-05", desktop: 88, mobile: 140 },
{ date: "2024-06-06", desktop: 294, mobile: 250 },
{ date: "2024-06-07", desktop: 323, mobile: 370 },
{ date: "2024-06-08", desktop: 385, mobile: 320 },
{ date: "2024-06-09", desktop: 438, mobile: 480 },
{ date: "2024-06-10", desktop: 155, mobile: 200 },
{ date: "2024-06-11", desktop: 92, mobile: 150 },
{ date: "2024-06-12", desktop: 492, mobile: 420 },
{ date: "2024-06-13", desktop: 81, mobile: 130 },
{ date: "2024-06-14", desktop: 426, mobile: 380 },
{ date: "2024-06-15", desktop: 307, mobile: 350 },
{ date: "2024-06-16", desktop: 371, mobile: 310 },
{ date: "2024-06-17", desktop: 475, mobile: 520 },
{ date: "2024-06-18", desktop: 107, mobile: 170 },
{ date: "2024-06-19", desktop: 341, mobile: 290 },
{ date: "2024-06-20", desktop: 408, mobile: 450 },
{ date: "2024-06-21", desktop: 169, mobile: 210 },
{ date: "2024-06-22", desktop: 317, mobile: 270 },
{ date: "2024-06-23", desktop: 480, mobile: 530 },
{ date: "2024-06-24", desktop: 132, mobile: 180 },
{ date: "2024-06-25", desktop: 141, mobile: 190 },
{ date: "2024-06-26", desktop: 434, mobile: 380 },
{ date: "2024-06-27", desktop: 448, mobile: 490 },
{ date: "2024-06-28", desktop: 149, mobile: 200 },
{ date: "2024-06-29", desktop: 103, mobile: 160 },
{ date: "2024-06-30", desktop: 446, mobile: 400 },
]
{ date: '2024-04-01', desktop: 222, mobile: 150 },
{ date: '2024-04-02', desktop: 97, mobile: 180 },
{ date: '2024-04-03', desktop: 167, mobile: 120 },
{ date: '2024-04-04', desktop: 242, mobile: 260 },
{ date: '2024-04-05', desktop: 373, mobile: 290 },
{ date: '2024-04-06', desktop: 301, mobile: 340 },
{ date: '2024-04-07', desktop: 245, mobile: 180 },
{ date: '2024-04-08', desktop: 409, mobile: 320 },
{ date: '2024-04-09', desktop: 59, mobile: 110 },
{ date: '2024-04-10', desktop: 261, mobile: 190 },
{ date: '2024-04-11', desktop: 327, mobile: 350 },
{ date: '2024-04-12', desktop: 292, mobile: 210 },
{ date: '2024-04-13', desktop: 342, mobile: 380 },
{ date: '2024-04-14', desktop: 137, mobile: 220 },
{ date: '2024-04-15', desktop: 120, mobile: 170 },
{ date: '2024-04-16', desktop: 138, mobile: 190 },
{ date: '2024-04-17', desktop: 446, mobile: 360 },
{ date: '2024-04-18', desktop: 364, mobile: 410 },
{ date: '2024-04-19', desktop: 243, mobile: 180 },
{ date: '2024-04-20', desktop: 89, mobile: 150 },
{ date: '2024-04-21', desktop: 137, mobile: 200 },
{ date: '2024-04-22', desktop: 224, mobile: 170 },
{ date: '2024-04-23', desktop: 138, mobile: 230 },
{ date: '2024-04-24', desktop: 387, mobile: 290 },
{ date: '2024-04-25', desktop: 215, mobile: 250 },
{ date: '2024-04-26', desktop: 75, mobile: 130 },
{ date: '2024-04-27', desktop: 383, mobile: 420 },
{ date: '2024-04-28', desktop: 122, mobile: 180 },
{ date: '2024-04-29', desktop: 315, mobile: 240 },
{ date: '2024-04-30', desktop: 454, mobile: 380 },
{ date: '2024-05-01', desktop: 165, mobile: 220 },
{ date: '2024-05-02', desktop: 293, mobile: 310 },
{ date: '2024-05-03', desktop: 247, mobile: 190 },
{ date: '2024-05-04', desktop: 385, mobile: 420 },
{ date: '2024-05-05', desktop: 481, mobile: 390 },
{ date: '2024-05-06', desktop: 498, mobile: 520 },
{ date: '2024-05-07', desktop: 388, mobile: 300 },
{ date: '2024-05-08', desktop: 149, mobile: 210 },
{ date: '2024-05-09', desktop: 227, mobile: 180 },
{ date: '2024-05-10', desktop: 293, mobile: 330 },
{ date: '2024-05-11', desktop: 335, mobile: 270 },
{ date: '2024-05-12', desktop: 197, mobile: 240 },
{ date: '2024-05-13', desktop: 197, mobile: 160 },
{ date: '2024-05-14', desktop: 448, mobile: 490 },
{ date: '2024-05-15', desktop: 473, mobile: 380 },
{ date: '2024-05-16', desktop: 338, mobile: 400 },
{ date: '2024-05-17', desktop: 499, mobile: 420 },
{ date: '2024-05-18', desktop: 315, mobile: 350 },
{ date: '2024-05-19', desktop: 235, mobile: 180 },
{ date: '2024-05-20', desktop: 177, mobile: 230 },
{ date: '2024-05-21', desktop: 82, mobile: 140 },
{ date: '2024-05-22', desktop: 81, mobile: 120 },
{ date: '2024-05-23', desktop: 252, mobile: 290 },
{ date: '2024-05-24', desktop: 294, mobile: 220 },
{ date: '2024-05-25', desktop: 201, mobile: 250 },
{ date: '2024-05-26', desktop: 213, mobile: 170 },
{ date: '2024-05-27', desktop: 420, mobile: 460 },
{ date: '2024-05-28', desktop: 233, mobile: 190 },
{ date: '2024-05-29', desktop: 78, mobile: 130 },
{ date: '2024-05-30', desktop: 340, mobile: 280 },
{ date: '2024-05-31', desktop: 178, mobile: 230 },
{ date: '2024-06-01', desktop: 178, mobile: 200 },
{ date: '2024-06-02', desktop: 470, mobile: 410 },
{ date: '2024-06-03', desktop: 103, mobile: 160 },
{ date: '2024-06-04', desktop: 439, mobile: 380 },
{ date: '2024-06-05', desktop: 88, mobile: 140 },
{ date: '2024-06-06', desktop: 294, mobile: 250 },
{ date: '2024-06-07', desktop: 323, mobile: 370 },
{ date: '2024-06-08', desktop: 385, mobile: 320 },
{ date: '2024-06-09', desktop: 438, mobile: 480 },
{ date: '2024-06-10', desktop: 155, mobile: 200 },
{ date: '2024-06-11', desktop: 92, mobile: 150 },
{ date: '2024-06-12', desktop: 492, mobile: 420 },
{ date: '2024-06-13', desktop: 81, mobile: 130 },
{ date: '2024-06-14', desktop: 426, mobile: 380 },
{ date: '2024-06-15', desktop: 307, mobile: 350 },
{ date: '2024-06-16', desktop: 371, mobile: 310 },
{ date: '2024-06-17', desktop: 475, mobile: 520 },
{ date: '2024-06-18', desktop: 107, mobile: 170 },
{ date: '2024-06-19', desktop: 341, mobile: 290 },
{ date: '2024-06-20', desktop: 408, mobile: 450 },
{ date: '2024-06-21', desktop: 169, mobile: 210 },
{ date: '2024-06-22', desktop: 317, mobile: 270 },
{ date: '2024-06-23', desktop: 480, mobile: 530 },
{ date: '2024-06-24', desktop: 132, mobile: 180 },
{ date: '2024-06-25', desktop: 141, mobile: 190 },
{ date: '2024-06-26', desktop: 434, mobile: 380 },
{ date: '2024-06-27', desktop: 448, mobile: 490 },
{ date: '2024-06-28', desktop: 149, mobile: 200 },
{ date: '2024-06-29', desktop: 103, mobile: 160 },
{ date: '2024-06-30', desktop: 446, mobile: 400 },
];
const chartConfig = {
visitors: {
label: "Visitors",
label: 'Visitors',
},
desktop: {
label: "Desktop",
color: "var(--primary)",
label: 'Desktop',
color: 'var(--primary)',
},
mobile: {
label: "Mobile",
color: "var(--primary)",
label: 'Mobile',
color: 'var(--primary)',
},
} satisfies ChartConfig
} satisfies ChartConfig;
export function ChartAreaInteractive() {
const isMobile = useIsMobile()
const [timeRange, setTimeRange] = React.useState("90d")
const isMobile = useIsMobile();
const [timeRange, setTimeRange] = React.useState('90d');
React.useEffect(() => {
if (isMobile) {
setTimeRange("7d")
setTimeRange('7d');
}
}, [isMobile])
}, [isMobile]);
const filteredData = chartData.filter((item) => {
const date = new Date(item.date)
const referenceDate = new Date("2024-06-30")
let daysToSubtract = 90
if (timeRange === "30d") {
daysToSubtract = 30
} else if (timeRange === "7d") {
daysToSubtract = 7
const date = new Date(item.date);
const referenceDate = new Date('2024-06-30');
let daysToSubtract = 90;
if (timeRange === '30d') {
daysToSubtract = 30;
} else if (timeRange === '7d') {
daysToSubtract = 7;
}
const startDate = new Date(referenceDate)
startDate.setDate(startDate.getDate() - daysToSubtract)
return date >= startDate
})
const startDate = new Date(referenceDate);
startDate.setDate(startDate.getDate() - daysToSubtract);
return date >= startDate;
});
return (
<Card className="@container/card">
@ -247,11 +244,11 @@ export function ChartAreaInteractive() {
tickMargin={8}
minTickGap={32}
tickFormatter={(value) => {
const date = new Date(value)
return date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
})
const date = new Date(value);
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
});
}}
/>
<ChartTooltip
@ -260,10 +257,10 @@ export function ChartAreaInteractive() {
content={
<ChartTooltipContent
labelFormatter={(value) => {
return new Date(value).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
})
return new Date(value).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
});
}}
indicator="dot"
/>
@ -287,5 +284,5 @@ export function ChartAreaInteractive() {
</ChartContainer>
</CardContent>
</Card>
)
);
}

View File

@ -7,7 +7,7 @@ import {
} from '@/components/ui/breadcrumb';
import { Separator } from '@/components/ui/separator';
import { SidebarTrigger } from '@/components/ui/sidebar';
import React, { ReactNode } from 'react';
import React, { type ReactNode } from 'react';
import LocaleSwitcher from '../layout/locale-switcher';
import { ModeSwitcher } from '../layout/mode-switcher';
import { ThemeSelector } from '../layout/theme-selector';
@ -25,7 +25,10 @@ interface DashboardHeaderProps {
/**
* Dashboard header
*/
export function DashboardHeader({ breadcrumbs, actions }: DashboardHeaderProps) {
export function DashboardHeader({
breadcrumbs,
actions,
}: DashboardHeaderProps) {
return (
<header className="flex h-(--header-height) shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)">
<div className="flex w-full items-center gap-1 px-4 lg:gap-2 lg:px-6">
@ -40,11 +43,16 @@ export function DashboardHeader({ breadcrumbs, actions }: DashboardHeaderProps)
{breadcrumbs.map((item, index) => (
<React.Fragment key={`breadcrumb-${index}`}>
{index > 0 && (
<BreadcrumbSeparator key={`sep-${index}`} className="hidden md:block" />
<BreadcrumbSeparator
key={`sep-${index}`}
className="hidden md:block"
/>
)}
<BreadcrumbItem
key={`item-${index}`}
className={index < breadcrumbs.length - 1 ? "hidden md:block" : ""}
className={
index < breadcrumbs.length - 1 ? 'hidden md:block' : ''
}
>
{item.isCurrentPage ? (
<BreadcrumbPage>{item.label}</BreadcrumbPage>

View File

@ -9,21 +9,23 @@ import {
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem
SidebarMenuItem,
} from '@/components/ui/sidebar';
import { getSidebarLinks } from '@/config/sidebar-config';
import { authClient } from '@/lib/auth-client';
import { LocaleLink } from '@/i18n/navigation';
import { authClient } from '@/lib/auth-client';
import { Routes } from '@/routes';
import { useTranslations } from 'next-intl';
import * as React from 'react';
import type * as React from 'react';
import { Logo } from '../layout/logo';
import { UpgradeCard } from './upgrade-card';
/**
* Dashboard sidebar
*/
export function DashboardSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
export function DashboardSidebar({
...props
}: React.ComponentProps<typeof Sidebar>) {
const t = useTranslations();
const sidebarLinks = getSidebarLinks();
const { data: session, isPending } = authClient.useSession();

View File

@ -1,25 +1,24 @@
"use client"
'use client';
import * as React from "react"
import {
DndContext,
type DragEndEvent,
KeyboardSensor,
MouseSensor,
TouchSensor,
type UniqueIdentifier,
closestCenter,
useSensor,
useSensors,
type DragEndEvent,
type UniqueIdentifier,
} from "@dnd-kit/core"
import { restrictToVerticalAxis } from "@dnd-kit/modifiers"
} from '@dnd-kit/core';
import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
import {
SortableContext,
arrayMove,
useSortable,
verticalListSortingStrategy,
} from "@dnd-kit/sortable"
import { CSS } from "@dnd-kit/utilities"
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import {
IconChevronDown,
IconChevronLeft,
@ -33,13 +32,13 @@ import {
IconLoader,
IconPlus,
IconTrendingUp,
} from "@tabler/icons-react"
} from '@tabler/icons-react';
import {
ColumnDef,
ColumnFiltersState,
Row,
SortingState,
VisibilityState,
type ColumnDef,
type ColumnFiltersState,
type Row,
type SortingState,
type VisibilityState,
flexRender,
getCoreRowModel,
getFacetedRowModel,
@ -48,21 +47,21 @@ import {
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table"
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts"
import { toast } from "sonner"
import { z } from "zod"
} from '@tanstack/react-table';
import * as React from 'react';
import { Area, AreaChart, CartesianGrid, XAxis } from 'recharts';
import { toast } from 'sonner';
import { z } from 'zod';
import { useIsMobile } from "@/hooks/use-mobile"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
ChartConfig,
type ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart"
import { Checkbox } from "@/components/ui/checkbox"
} from '@/components/ui/chart';
import { Checkbox } from '@/components/ui/checkbox';
import {
Drawer,
DrawerClose,
@ -72,7 +71,7 @@ import {
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer"
} from '@/components/ui/drawer';
import {
DropdownMenu,
DropdownMenuCheckboxItem,
@ -80,17 +79,17 @@ import {
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
} from '@/components/ui/dropdown-menu';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { Separator } from "@/components/ui/separator"
} from '@/components/ui/select';
import { Separator } from '@/components/ui/separator';
import {
Table,
TableBody,
@ -98,13 +97,9 @@ import {
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/components/ui/tabs"
} from '@/components/ui/table';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { useIsMobile } from '@/hooks/use-mobile';
export const schema = z.object({
id: z.number(),
@ -114,13 +109,13 @@ export const schema = z.object({
target: z.string(),
limit: z.string(),
reviewer: z.string(),
})
});
// Create a separate component for the drag handle
function DragHandle({ id }: { id: number }) {
const { attributes, listeners } = useSortable({
id,
})
});
return (
<Button
@ -133,23 +128,23 @@ function DragHandle({ id }: { id: number }) {
<IconGripVertical className="text-muted-foreground size-3" />
<span className="sr-only">Drag to reorder</span>
</Button>
)
);
}
const columns: ColumnDef<z.infer<typeof schema>>[] = [
{
id: "drag",
id: 'drag',
header: () => null,
cell: ({ row }) => <DragHandle id={row.original.id} />,
},
{
id: "select",
id: 'select',
header: ({ table }) => (
<div className="flex items-center justify-center">
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
(table.getIsSomePageRowsSelected() && 'indeterminate')
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
@ -169,16 +164,16 @@ const columns: ColumnDef<z.infer<typeof schema>>[] = [
enableHiding: false,
},
{
accessorKey: "header",
header: "Header",
accessorKey: 'header',
header: 'Header',
cell: ({ row }) => {
return <TableCellViewer item={row.original} />
return <TableCellViewer item={row.original} />;
},
enableHiding: false,
},
{
accessorKey: "type",
header: "Section Type",
accessorKey: 'type',
header: 'Section Type',
cell: ({ row }) => (
<div className="w-32">
<Badge variant="outline" className="text-muted-foreground px-1.5">
@ -188,11 +183,11 @@ const columns: ColumnDef<z.infer<typeof schema>>[] = [
),
},
{
accessorKey: "status",
header: "Status",
accessorKey: 'status',
header: 'Status',
cell: ({ row }) => (
<Badge variant="outline" className="text-muted-foreground px-1.5">
{row.original.status === "Done" ? (
{row.original.status === 'Done' ? (
<IconCircleCheckFilled className="fill-green-500 dark:fill-green-400" />
) : (
<IconLoader />
@ -202,17 +197,17 @@ const columns: ColumnDef<z.infer<typeof schema>>[] = [
),
},
{
accessorKey: "target",
accessorKey: 'target',
header: () => <div className="w-full text-right">Target</div>,
cell: ({ row }) => (
<form
onSubmit={(e) => {
e.preventDefault()
e.preventDefault();
toast.promise(new Promise((resolve) => setTimeout(resolve, 1000)), {
loading: `Saving ${row.original.header}`,
success: "Done",
error: "Error",
})
success: 'Done',
error: 'Error',
});
}}
>
<Label htmlFor={`${row.original.id}-target`} className="sr-only">
@ -227,17 +222,17 @@ const columns: ColumnDef<z.infer<typeof schema>>[] = [
),
},
{
accessorKey: "limit",
accessorKey: 'limit',
header: () => <div className="w-full text-right">Limit</div>,
cell: ({ row }) => (
<form
onSubmit={(e) => {
e.preventDefault()
e.preventDefault();
toast.promise(new Promise((resolve) => setTimeout(resolve, 1000)), {
loading: `Saving ${row.original.header}`,
success: "Done",
error: "Error",
})
success: 'Done',
error: 'Error',
});
}}
>
<Label htmlFor={`${row.original.id}-limit`} className="sr-only">
@ -252,13 +247,13 @@ const columns: ColumnDef<z.infer<typeof schema>>[] = [
),
},
{
accessorKey: "reviewer",
header: "Reviewer",
accessorKey: 'reviewer',
header: 'Reviewer',
cell: ({ row }) => {
const isAssigned = row.original.reviewer !== "Assign reviewer"
const isAssigned = row.original.reviewer !== 'Assign reviewer';
if (isAssigned) {
return row.original.reviewer
return row.original.reviewer;
}
return (
@ -282,11 +277,11 @@ const columns: ColumnDef<z.infer<typeof schema>>[] = [
</SelectContent>
</Select>
</>
)
);
},
},
{
id: "actions",
id: 'actions',
cell: () => (
<DropdownMenu>
<DropdownMenuTrigger asChild>
@ -309,16 +304,16 @@ const columns: ColumnDef<z.infer<typeof schema>>[] = [
</DropdownMenu>
),
},
]
];
function DraggableRow({ row }: { row: Row<z.infer<typeof schema>> }) {
const { transform, transition, setNodeRef, isDragging } = useSortable({
id: row.original.id,
})
});
return (
<TableRow
data-state={row.getIsSelected() && "selected"}
data-state={row.getIsSelected() && 'selected'}
data-dragging={isDragging}
ref={setNodeRef}
className="relative z-0 data-[dragging=true]:z-10 data-[dragging=true]:opacity-80"
@ -333,37 +328,37 @@ function DraggableRow({ row }: { row: Row<z.infer<typeof schema>> }) {
</TableCell>
))}
</TableRow>
)
);
}
export function DataTable({
data: initialData,
}: {
data: z.infer<typeof schema>[]
data: z.infer<typeof schema>[];
}) {
const [data, setData] = React.useState(() => initialData)
const [rowSelection, setRowSelection] = React.useState({})
const [data, setData] = React.useState(() => initialData);
const [rowSelection, setRowSelection] = React.useState({});
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({})
React.useState<VisibilityState>({});
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[]
)
const [sorting, setSorting] = React.useState<SortingState>([])
);
const [sorting, setSorting] = React.useState<SortingState>([]);
const [pagination, setPagination] = React.useState({
pageIndex: 0,
pageSize: 10,
})
const sortableId = React.useId()
});
const sortableId = React.useId();
const sensors = useSensors(
useSensor(MouseSensor, {}),
useSensor(TouchSensor, {}),
useSensor(KeyboardSensor, {})
)
);
const dataIds = React.useMemo<UniqueIdentifier[]>(
() => data?.map(({ id }) => id) || [],
[data]
)
);
const table = useReactTable({
data,
@ -388,16 +383,16 @@ export function DataTable({
getSortedRowModel: getSortedRowModel(),
getFacetedRowModel: getFacetedRowModel(),
getFacetedUniqueValues: getFacetedUniqueValues(),
})
});
function handleDragEnd(event: DragEndEvent) {
const { active, over } = event
const { active, over } = event;
if (active && over && active.id !== over.id) {
setData((data) => {
const oldIndex = dataIds.indexOf(active.id)
const newIndex = dataIds.indexOf(over.id)
return arrayMove(data, oldIndex, newIndex)
})
const oldIndex = dataIds.indexOf(active.id);
const newIndex = dataIds.indexOf(over.id);
return arrayMove(data, oldIndex, newIndex);
});
}
}
@ -450,7 +445,7 @@ export function DataTable({
.getAllColumns()
.filter(
(column) =>
typeof column.accessorFn !== "undefined" &&
typeof column.accessorFn !== 'undefined' &&
column.getCanHide()
)
.map((column) => {
@ -465,7 +460,7 @@ export function DataTable({
>
{column.id}
</DropdownMenuCheckboxItem>
)
);
})}
</DropdownMenuContent>
</DropdownMenu>
@ -501,7 +496,7 @@ export function DataTable({
header.getContext()
)}
</TableHead>
)
);
})}
</TableRow>
))}
@ -532,7 +527,7 @@ export function DataTable({
</div>
<div className="flex items-center justify-between px-4">
<div className="text-muted-foreground hidden flex-1 text-sm lg:flex">
{table.getFilteredSelectedRowModel().rows.length} of{" "}
{table.getFilteredSelectedRowModel().rows.length} of{' '}
{table.getFilteredRowModel().rows.length} row(s) selected.
</div>
<div className="flex w-full items-center gap-8 lg:w-fit">
@ -543,7 +538,7 @@ export function DataTable({
<Select
value={`${table.getState().pagination.pageSize}`}
onValueChange={(value) => {
table.setPageSize(Number(value))
table.setPageSize(Number(value));
}}
>
<SelectTrigger size="sm" className="w-20" id="rows-per-page">
@ -561,7 +556,7 @@ export function DataTable({
</Select>
</div>
<div className="flex w-fit items-center justify-center text-sm font-medium">
Page {table.getState().pagination.pageIndex + 1} of{" "}
Page {table.getState().pagination.pageIndex + 1} of{' '}
{table.getPageCount()}
</div>
<div className="ml-auto flex items-center gap-2 lg:ml-0">
@ -624,34 +619,34 @@ export function DataTable({
<div className="aspect-video w-full flex-1 rounded-lg border border-dashed"></div>
</TabsContent>
</Tabs>
)
);
}
const chartData = [
{ month: "January", desktop: 186, mobile: 80 },
{ month: "February", desktop: 305, mobile: 200 },
{ month: "March", desktop: 237, mobile: 120 },
{ month: "April", desktop: 73, mobile: 190 },
{ month: "May", desktop: 209, mobile: 130 },
{ month: "June", desktop: 214, mobile: 140 },
]
{ month: 'January', desktop: 186, mobile: 80 },
{ month: 'February', desktop: 305, mobile: 200 },
{ month: 'March', desktop: 237, mobile: 120 },
{ month: 'April', desktop: 73, mobile: 190 },
{ month: 'May', desktop: 209, mobile: 130 },
{ month: 'June', desktop: 214, mobile: 140 },
];
const chartConfig = {
desktop: {
label: "Desktop",
color: "var(--primary)",
label: 'Desktop',
color: 'var(--primary)',
},
mobile: {
label: "Mobile",
color: "var(--primary)",
label: 'Mobile',
color: 'var(--primary)',
},
} satisfies ChartConfig
} satisfies ChartConfig;
function TableCellViewer({ item }: { item: z.infer<typeof schema> }) {
const isMobile = useIsMobile()
const isMobile = useIsMobile();
return (
<Drawer direction={isMobile ? "bottom" : "right"}>
<Drawer direction={isMobile ? 'bottom' : 'right'}>
<DrawerTrigger asChild>
<Button variant="link" className="text-foreground w-fit px-0 text-left">
{item.header}
@ -710,7 +705,7 @@ function TableCellViewer({ item }: { item: z.infer<typeof schema> }) {
<Separator />
<div className="grid gap-2">
<div className="flex gap-2 leading-none font-medium">
Trending up by 5.2% this month{" "}
Trending up by 5.2% this month{' '}
<IconTrendingUp className="size-4" />
</div>
<div className="text-muted-foreground">
@ -803,5 +798,5 @@ function TableCellViewer({ item }: { item: z.infer<typeof schema> }) {
</DrawerFooter>
</DrawerContent>
</Drawer>
)
);
}

View File

@ -1,6 +1,6 @@
import { IconTrendingDown, IconTrendingUp } from "@tabler/icons-react"
import { IconTrendingDown, IconTrendingUp } from '@tabler/icons-react';
import { Badge } from "@/components/ui/badge"
import { Badge } from '@/components/ui/badge';
import {
Card,
CardAction,
@ -8,7 +8,7 @@ import {
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
} from '@/components/ui/card';
export function SectionCards() {
return (
@ -98,5 +98,5 @@ export function SectionCards() {
</CardFooter>
</Card>
</div>
)
);
}

View File

@ -1,4 +1,4 @@
"use client";
'use client';
import {
SidebarGroup,
@ -7,14 +7,14 @@ import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar";
import { LocaleLink, useLocalePathname } from "@/i18n/navigation";
import { NestedMenuItem } from "@/types";
} from '@/components/ui/sidebar';
import { LocaleLink, useLocalePathname } from '@/i18n/navigation';
import type { NestedMenuItem } from '@/types';
/**
* Main navigation for the dashboard sidebar
*/
export function SidebarMain({ items, }: { items: NestedMenuItem[] }) {
export function SidebarMain({ items }: { items: NestedMenuItem[] }) {
const pathname = useLocalePathname();
// Function to check if a path is active
@ -34,7 +34,10 @@ export function SidebarMain({ items, }: { items: NestedMenuItem[] }) {
<SidebarMenu>
{item.items.map((subItem) => (
<SidebarMenuItem key={subItem.title}>
<SidebarMenuButton asChild isActive={isActive(subItem.href)}>
<SidebarMenuButton
asChild
isActive={isActive(subItem.href)}
>
<LocaleLink href={subItem.href || ''}>
{subItem.icon ? subItem.icon : null}
<span className="truncate font-medium text-sm">
@ -68,5 +71,5 @@ export function SidebarMain({ items, }: { items: NestedMenuItem[] }) {
)
)}
</>
)
);
}

Some files were not shown because too many files have changed in this diff Show More