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"
]
},
@ -76,4 +78,4 @@
"semicolons": "always"
}
}
}
}

View File

@ -1,18 +1,18 @@
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
* https://www.content-collections.dev/docs/quickstart/next
* https://www.content-collections.dev/docs/configuration
* https://www.content-collections.dev/docs/transform#join-collections
*
*
* 2. Use Content Collections for Fumadocs
* https://fumadocs.vercel.app/docs/headless/content-collections
*/
@ -38,11 +38,11 @@ const metas = defineCollection({
/**
* Blog Author collection
*
*
* Authors are identified by their slug across all languages
* New format: content/author/authorname.{locale}.mdx
* Example: content/author/mksaas.mdx (default locale) and content/author/mksaas.zh.mdx (Chinese)
*
*
* For author, slug is slugAsParams
*/
export const authors = defineCollection({
@ -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,16 +68,16 @@ export const authors = defineCollection({
...data,
locale,
};
}
},
});
/**
* Blog Category collection
*
*
* Categories are identified by their slug across all languages
* New format: content/category/categoryname.{locale}.mdx
* Example: content/category/tutorial.mdx (default locale) and content/category/tutorial.zh.mdx (Chinese)
*
*
* For category, slug is slugAsParams
*/
export const categories = defineCollection({
@ -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,24 +101,24 @@ export const categories = defineCollection({
return {
...data,
locale
locale,
};
}
},
});
/**
* Blog Post collection
*
*
* New format: content/blog/post-slug.{locale}.mdx
*
*
* slug: /blog/first-post, used in URL or sitemap
* slugAsParams: first-post, used in route params
*
*
* 1. For a blog post at content/blog/first-post.mdx (default locale):
* locale: en
* slug: /blog/first-post
* slugAsParams: first-post
*
*
* 2. For a blog post at content/blog/first-post.zh.mdx (Chinese locale):
* locale: zh
* slug: /blog/first-post
@ -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,18 +156,20 @@ 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 category = context
.documents(categories)
.find(c => c.slug === categorySlug && c.locale === locale);
const blogCategories = data.categories
.map((categorySlug) => {
const category = context
.documents(categories)
.find((c) => c.slug === categorySlug && c.locale === locale);
return category;
}).filter(Boolean); // Remove null values
return category;
})
.filter(Boolean); // Remove null values
// Create the slug and slugAsParams
const slug = `/blog/${base}`;
const slugAsParams = base;
// Calculate estimated reading time
const wordCount = data.content.split(/\s+/).length;
const wordsPerMinute = 200; // average reading speed: 200 words per minute
@ -182,21 +184,21 @@ export const posts = defineCollection({
slugAsParams,
estimatedTime,
body: transformedData.body,
toc: transformedData.toc
toc: transformedData.toc,
};
}
},
});
/**
* Pages collection for policy pages like privacy-policy, terms-of-service, etc.
*
*
* New format: content/pages/page-slug.{locale}.mdx
*
*
* 1. For a page at content/pages/privacy-policy.mdx (default locale):
* locale: en
* slug: /pages/privacy-policy
* slugAsParams: privacy-policy
*
*
* 2. For a page at content/pages/privacy-policy.zh.mdx (Chinese locale):
* locale: zh
* slug: /pages/privacy-policy
@ -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,21 +236,21 @@ export const pages = defineCollection({
slug,
slugAsParams,
body: transformedData.body,
toc: transformedData.toc
toc: transformedData.toc,
};
}
},
});
/**
* Releases collection for changelog
*
*
* New format: content/release/version-slug.{locale}.mdx
*
*
* 1. For a release at content/release/v1-0-0.mdx (default locale):
* locale: en
* slug: /release/v1-0-0
* slugAsParams: v1-0-0
*
*
* 2. For a release at content/release/v1-0-0.zh.mdx (Chinese locale):
* locale: zh
* slug: /release/v1-0-0
@ -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,
};
}
},
});
/**
@ -297,11 +299,14 @@ export const releases = defineCollection({
* Handles filename formats:
* - name -> locale: DEFAULT_LOCALE, base: name
* - name.zh -> locale: zh, base: name
*
*
* @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],
});

6
global.d.ts vendored
View File

@ -1,9 +1,9 @@
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
*
*
* https://github.com/amannn/next-intl/blob/main/examples/example-app-router/global.d.ts
*/
declare module 'next-intl' {

View File

@ -848,4 +848,4 @@
"description": "MkSaaS lets you make AI SaaS in days, simply and effortlessly",
"content": "Working in progress"
}
}
}

View File

@ -671,7 +671,7 @@
},
"item-2": {
"title": "产品特色功能二",
"description": "请在这里详细描述您的产品特色功能二,尽可能详细,使其更吸引用户"
"description": "请在这里详细描述您的产品特色功能二,尽可能详细,使其更吸引用户"
},
"item-3": {
"title": "产品特色功能三",
@ -849,4 +849,4 @@
"description": "MkSaaS 让您在几天内轻松构建您的 AI SaaS简单且毫不费力",
"content": "正在开发中"
}
}
}

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',
@ -39,14 +39,14 @@ const nextConfig: NextConfig = {
/**
* You can specify the path to the request config file or use the default one (@/i18n/request.ts)
*
*
* https://next-intl.dev/docs/getting-started/app-router/with-i18n-routing#next-config
*/
const withNextIntl = createNextIntlPlugin();
/**
* withContentCollections must be the outermost plugin
*
*
* https://www.content-collections.dev/docs/quickstart/next
*/
export default withContentCollections(withNextIntl(nextConfig));

View File

@ -31,4 +31,4 @@ export const checkNewsletterStatusAction = actionClient
error: error instanceof Error ? error.message : 'Something went wrong',
};
}
});
});

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,10 +96,10 @@ 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(),
});
/**
@ -27,11 +30,13 @@ export const createPortalAction = actionClient
.schema(portalSchema)
.action(async ({ parsedInput }) => {
const { userId, returnUrl } = parsedInput;
// 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,10 +89,10 @@ 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();
@ -15,8 +15,8 @@ const schema = z.object({
/**
* Get active subscription data
*
* If the user has multiple subscriptions,
*
* If the user has multiple subscriptions,
* it returns the most recent active or trialing one
*/
export const getActiveSubscriptionAction = actionClient
@ -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,10 +81,10 @@ 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();
@ -19,8 +19,8 @@ const schema = z.object({
/**
* Get user lifetime membership status directly from the database
*
* NOTICE: If you first add lifetime plan and then delete it,
*
* NOTICE: If you first add lifetime plan and then delete it,
* the user with lifetime plan should be considered as a lifetime member as well,
* in order to do this, you have to update the logic to check the lifetime status,
* for example, just check the planId is `lifetime` or not.
@ -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,10 +95,10 @@ 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

@ -51,4 +51,4 @@ export const subscribeNewsletterAction = actionClient
error: error instanceof Error ? error.message : 'Something went wrong',
};
}
});
});

View File

@ -24,7 +24,7 @@ export const unsubscribeNewsletterAction = actionClient
error: 'Unauthorized',
};
}
try {
const unsubscribed = await unsubscribe(email);
@ -46,4 +46,4 @@ export const unsubscribeNewsletterAction = actionClient
error: error instanceof Error ? error.message : 'Something went wrong',
};
}
});
});

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,16 +1,16 @@
"use client";
'use client';
import Script from "next/script";
import Script from 'next/script';
/**
* Seline Analytics
*
*
* https://seline.com
* https://seline.com/docs/install-seline
* 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;
}
@ -30,4 +30,4 @@ export function UmamiAnalytics() {
src={script}
/>
);
}
}

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';
@ -13,7 +13,7 @@ export async function generateMetadata({
}: {
params: Promise<{ locale: Locale }>;
}): Promise<Metadata | undefined> {
const {locale} = await params;
const { locale } = await params;
const page = await getPage('cookie-policy', locale);
if (!page) {
@ -23,12 +23,12 @@ export async function generateMetadata({
return {};
}
const t = await getTranslations({locale, namespace: 'Metadata'});
const t = await getTranslations({ locale, namespace: 'Metadata' });
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';
@ -13,7 +13,7 @@ export async function generateMetadata({
}: {
params: Promise<{ locale: Locale }>;
}): Promise<Metadata | undefined> {
const {locale} = await params;
const { locale } = await params;
const page = await getPage('privacy-policy', locale);
if (!page) {
@ -23,12 +23,12 @@ export async function generateMetadata({
return {};
}
const t = await getTranslations({locale, namespace: 'Metadata'});
const t = await getTranslations({ locale, namespace: 'Metadata' });
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';
@ -13,7 +13,7 @@ export async function generateMetadata({
}: {
params: Promise<{ locale: Locale }>;
}): Promise<Metadata | undefined> {
const {locale} = await params;
const { locale } = await params;
const page = await getPage('terms-of-service', locale);
if (!page) {
@ -23,12 +23,12 @@ export async function generateMetadata({
return {};
}
const t = await getTranslations({locale, namespace: 'Metadata'});
const t = await getTranslations({ locale, namespace: 'Metadata' });
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';
@ -15,14 +15,14 @@ export async function generateMetadata({
}: {
params: Promise<{ locale: Locale }>;
}): Promise<Metadata | undefined> {
const {locale} = await params;
const t = await getTranslations({locale, namespace: 'Metadata'});
const pt = await getTranslations({locale, namespace: 'ChangelogPage'});
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'Metadata' });
const pt = await getTranslations({ locale, namespace: 'ChangelogPage' });
return constructMetadata({
title: pt('title') + ' | ' + t('title'),
description: pt('description'),
canonicalUrl: getUrlWithLocale("/changelog", locale),
canonicalUrl: getUrlWithLocale('/changelog', locale),
});
}

View File

@ -1,10 +1,10 @@
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({
params,
}: {
@ -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({
@ -10,13 +10,13 @@ export async function generateMetadata({
}: {
params: Promise<{ locale: Locale }>;
}): Promise<Metadata | undefined> {
const {locale} = await params;
const t = await getTranslations({locale, namespace: 'Metadata'});
const pt = await getTranslations({locale, namespace: 'WaitlistPage'});
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'Metadata' });
const pt = await getTranslations({ locale, namespace: 'WaitlistPage' });
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({
@ -29,12 +29,12 @@ export async function generateMetadata({
return {};
}
const t = await getTranslations({locale, namespace: 'Metadata'});
const t = await getTranslations({ locale, namespace: 'Metadata' });
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,11 +1,11 @@
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 { }
interface BlogListLayoutProps extends PropsWithChildren, NextPageProps {}
export default async function BlogListLayout({
children,

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({
@ -15,13 +15,13 @@ export async function generateMetadata({
}: {
params: Promise<{ locale: Locale }>;
}): Promise<Metadata | undefined> {
const {locale} = await params;
const t = await getTranslations({locale, namespace: 'Metadata'});
const pt = await getTranslations({locale, namespace: 'BlogPage'});
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'Metadata' });
const pt = await getTranslations({ locale, namespace: 'BlogPage' });
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,11 +4,11 @@ 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
*
*
* NOTICE: This is a demo page for the admin, no real data is used,
* we will show real data in the future
*/

View File

@ -4,11 +4,11 @@ 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
*
*
* NOTICE: This is a demo page for the dashboard, no real data is used,
* we will show real data in the future
*/

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({
@ -10,10 +10,10 @@ export async function generateMetadata({
}: {
params: Promise<{ locale: Locale }>;
}): Promise<Metadata | undefined> {
const {locale} = await params;
const t = await getTranslations({locale, namespace: 'Metadata'});
const pt = await getTranslations({locale, namespace: 'AuthPage.error'});
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'Metadata' });
const pt = await getTranslations({ locale, namespace: 'AuthPage.error' });
return constructMetadata({
title: pt('title') + ' | ' + t('title'),
description: t('description'),

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({
@ -10,14 +10,17 @@ export async function generateMetadata({
}: {
params: Promise<{ locale: Locale }>;
}): Promise<Metadata | undefined> {
const {locale} = await params;
const t = await getTranslations({locale, namespace: 'Metadata'});
const pt = await getTranslations({locale, namespace: 'AuthPage.forgotPassword'});
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'Metadata' });
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({
@ -12,14 +12,14 @@ export async function generateMetadata({
}: {
params: Promise<{ locale: Locale }>;
}): Promise<Metadata | undefined> {
const {locale} = await params;
const t = await getTranslations({locale, namespace: 'Metadata'});
const pt = await getTranslations({locale, namespace: 'AuthPage.login'});
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'Metadata' });
const pt = await getTranslations({ locale, namespace: 'AuthPage.login' });
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({
@ -10,14 +10,17 @@ export async function generateMetadata({
}: {
params: Promise<{ locale: Locale }>;
}): Promise<Metadata | undefined> {
const {locale} = await params;
const t = await getTranslations({locale, namespace: 'Metadata'});
const pt = await getTranslations({locale, namespace: 'AuthPage.resetPassword'});
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'Metadata' });
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,19 +61,17 @@ export const revalidate = false;
interface DocPageProps {
params: Promise<{
slug?: string[];
locale: Locale
locale: Locale;
}>;
}
/**
* Doc Page
*
*
* 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}
@ -99,7 +101,7 @@ export default async function DocPage({
<CustomMDXContent
code={page.data.body}
customComponents={{
a: ({ href, ...props }: { href?: string;[key: string]: any }) => {
a: ({ href, ...props }: { href?: string; [key: string]: any }) => {
const found = source.getPageByHref(href ?? '', {
dir: page.file.dirname,
});

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]) => ({
name: data.name,
locale,
}));
const locales = Object.entries(websiteConfig.i18n.locales).map(
([locale, data]) => ({
name: data.name,
locale,
})
);
interface DocsLayoutProps {
children: ReactNode;
@ -31,17 +33,20 @@ interface DocsLayoutProps {
* 1. Configure navigation
* https://fumadocs.vercel.app/docs/ui/navigation/links
* https://fumadocs.vercel.app/docs/ui/navigation/sidebar
*
*
* ref:
* https://github.com/fuma-nama/fumadocs/blob/dev/apps/docs/app/layout.config.tsx
*
*
* 2. Organizing Pages
* https://fumadocs.vercel.app/docs/ui/page-conventions
*
*
* 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' });
@ -80,29 +85,25 @@ export default async function DocsRootLayout({ children, params }: DocsLayoutPro
},
...(websiteConfig.metadata.social?.twitter
? [
{
type: "icon" as const,
icon: <XTwitterIcon />,
text: "X",
url: websiteConfig.metadata.social.twitter,
secondary: true,
}
]
: [])
{
type: 'icon' as const,
icon: <XTwitterIcon />,
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,13 +6,13 @@ 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
*
*
* This component is used to wrap the app in the providers.
*
*
* - ThemeProvider: Provides the theme to the app.
* - ActiveThemeProvider: Provides the active theme to the app.
* - RootProvider: Provides the root provider for Fumadocs UI.
@ -21,8 +21,8 @@ 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
attribute="class"
@ -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,14 +1,14 @@
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
*
*
* 1. For internationalization, use createI18nSearchAPI:
* https://fumadocs.vercel.app/docs/headless/search/orama#internationalization
*
*
* 2. For special languages like Chinese, configure custom tokenizers:
* https://fumadocs.vercel.app/docs/headless/search/orama#special-languages
* https://docs.orama.com/open-source/supported-languages/using-chinese-with-orama
@ -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(
@ -79,4 +76,4 @@ export async function POST(request: NextRequest) {
{ status: 500 }
);
}
}
}

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(
@ -58,4 +55,4 @@ export async function POST(request: NextRequest) {
{ status: 500 }
);
}
}
}

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(
@ -69,4 +63,4 @@ export const config = {
sizeLimit: '10mb',
},
},
};
};

View File

@ -1,10 +1,10 @@
import { handleWebhookEvent } from '@/payment';
import { NextRequest, NextResponse } from 'next/server';
import { type NextRequest, NextResponse } from 'next/server';
/**
* Stripe webhook handler
* This endpoint receives webhook events from Stripe and processes them
*
*
* @param req The incoming request
* @returns NextResponse
*/

View File

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

View File

@ -1,21 +1,21 @@
import { defaultMessages } from '@/i18n/messages';
import { type MetadataRoute } from 'next';
import type { MetadataRoute } from 'next';
/**
* Generates the Web App Manifest for the application
*
*
* generated file name: manifest.webmanifest
*
* ref: https://github.com/amannn/next-intl/blob/main/examples/example-app-router/src/app/manifest.ts
*
* The manifest.json provides metadata used when the web app is installed on a
* user's mobile device or desktop. See https://web.dev/add-manifest/
*
* Since the manifest file needs to be placed in the root of the app folder (outside the [locale] dynamic segment),
*
* Since the manifest file needs to be placed in the root of the app folder (outside the [locale] dynamic segment),
* you need to provide a locale explicitly since next-intl cant infer it from the pathname.
*
*
* Solution: use the default messages (get from the default locale)
*
*
* https://next-intl.dev/docs/environments/actions-metadata-route-handlers#manifest
*
* @returns {MetadataRoute.Manifest} The manifest configuration object

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'];
@ -29,7 +29,7 @@ const staticRoutes = [
/**
* Generate a sitemap for the website
*
*
* https://nextjs.org/docs/app/api-reference/functions/generate-sitemaps
* https://github.com/javayhu/cnblocks/blob/main/app/sitemap.ts
*/
@ -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) => {
return routing.locales.map((locale) => ({
url: getUrl(route, locale),
lastModified: new Date(),
priority: 1,
changeFrequency: 'weekly' as const
}));
}));
sitemapList.push(
...staticRoutes.flatMap((route) => {
return routing.locales.map((locale) => ({
url: getUrl(route, locale),
lastModified: new Date(),
priority: 1,
changeFrequency: 'weekly' as const,
}));
})
);
// add categories
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
}))
));
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,
}))
)
);
// add posts
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
}))
));
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,
}))
)
);
// add docs
const docsParams = source.generateParams();
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
}))
));
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,
}))
)
);
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,10 +1,15 @@
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';
/**
* Features4Section is Features3Section with a different layout
*
*
* https://nsui.irung.me/features
* pnpm dlx shadcn@canary add https://nsui.irung.me/r/features-5.json
*/
@ -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')}
@ -83,4 +100,4 @@ export default function Features5Section() {
</div>
</section>
);
}
}

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,88 +1,85 @@
import { useTranslations } from "next-intl";
import { useTranslations } from 'next-intl';
export default function LogoCloudSection() {
const t = useTranslations('HomePage.logocloud');
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>
<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"
src="/svg/nextjs_logo_light.svg"
alt="Nextjs Logo"
height="20"
width="auto"
/>
<img
className="h-4 w-fit dark:invert"
src="/svg/tailwindcss.svg"
alt="Tailwind CSS Logo"
height="16"
width="auto"
/>
<img
className="h-6 w-fit dark:invert"
src="/svg/resend-wordmark-black.svg"
alt="Resend Logo"
height="28"
width="auto"
/>
<img
className="h-5 w-fit dark:invert"
src="/svg/vercel.svg"
alt="Vercel Logo"
height="20"
width="auto"
/>
<img
className="h-4 w-fit dark:invert"
src="/svg/github.svg"
alt="GitHub Logo"
height="16"
width="auto"
/>
<img
className="h-5 w-fit dark:invert"
src="/svg/cursor_wordmark_light.svg"
alt="Cursor Logo"
height="20"
width="auto"
/>
<img
className="h-5 w-fit dark:invert"
src="/svg/lemonsqueezy.svg"
alt="Lemon Squeezy Logo"
height="16"
width="auto"
/>
<img
className="h-6 w-fit dark:invert"
src="/svg/openai.svg"
alt="OpenAI Logo"
height="24"
width="auto"
/>
<img
className="h-4 w-fit dark:invert"
src="/svg/zapier.svg"
alt="Zapier Logo"
height="20"
width="auto"
/>
<img
className="h-4 w-fit dark:invert"
src="/svg/nvidia.svg"
alt="NVIDIA Logo"
height="20"
width="auto"
/>
</div>
const t = useTranslations('HomePage.logocloud');
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>
<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"
src="/svg/nextjs_logo_light.svg"
alt="Nextjs Logo"
height="20"
width="auto"
/>
<img
className="h-4 w-fit dark:invert"
src="/svg/tailwindcss.svg"
alt="Tailwind CSS Logo"
height="16"
width="auto"
/>
<img
className="h-6 w-fit dark:invert"
src="/svg/resend-wordmark-black.svg"
alt="Resend Logo"
height="28"
width="auto"
/>
<img
className="h-5 w-fit dark:invert"
src="/svg/vercel.svg"
alt="Vercel Logo"
height="20"
width="auto"
/>
<img
className="h-4 w-fit dark:invert"
src="/svg/github.svg"
alt="GitHub Logo"
height="16"
width="auto"
/>
<img
className="h-5 w-fit dark:invert"
src="/svg/cursor_wordmark_light.svg"
alt="Cursor Logo"
height="20"
width="auto"
/>
<img
className="h-5 w-fit dark:invert"
src="/svg/lemonsqueezy.svg"
alt="Lemon Squeezy Logo"
height="16"
width="auto"
/>
<img
className="h-6 w-fit dark:invert"
src="/svg/openai.svg"
alt="OpenAI Logo"
height="24"
width="auto"
/>
<img
className="h-4 w-fit dark:invert"
src="/svg/zapier.svg"
alt="Zapier Logo"
height="20"
width="auto"
/>
<img
className="h-4 w-fit dark:invert"
src="/svg/nvidia.svg"
alt="NVIDIA Logo"
height="20"
width="auto"
/>
</div>
</section>
);
}
</div>
</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,11 +11,9 @@ 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 />
</div>
</section>

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'))
@ -70,10 +65,10 @@ export function ContactFormCard() {
startTransition(async () => {
try {
setError('');
// Submit form data using the contact server action
const result = await sendMessageAction(values);
if (result && result.data?.success) {
toast.success(t('success'));
form.reset();
@ -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>
@ -159,8 +139,8 @@ export function ContactFormCard() {
<FormError message={error} />
</CardContent>
<CardFooter className="mt-6 px-6 py-4 flex justify-between items-center bg-muted rounded-none">
<Button
type="submit"
<Button
type="submit"
disabled={isPending}
className="cursor-pointer"
>
@ -171,4 +151,4 @@ export function ContactFormCard() {
</Form>
</Card>
);
}
}

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>
)
);
}

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