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/components/magicui/*.tsx",
"src/app/[[]locale]/preview/**", "src/app/[[]locale]/preview/**",
"src/db/schema.ts", "src/db/schema.ts",
"src/payment/types.ts",
"src/types/index.d.ts",
"public/sw.js" "public/sw.js"
] ]
}, },

View File

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

4
global.d.ts vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
"use client"; 'use client';
import Script from "next/script"; import Script from 'next/script';
/** /**
* DataFast Analytics * DataFast Analytics
@ -8,7 +8,7 @@ import Script from "next/script";
* https://datafa.st * https://datafa.st
*/ */
export default function DataFastAnalytics() { export default function DataFastAnalytics() {
if (process.env.NODE_ENV !== "production") { if (process.env.NODE_ENV !== 'production') {
return null; 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 * 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 * https://nextjs.org/docs/app/building-your-application/optimizing/third-party-libraries#google-analytics
*/ */
export default function GoogleAnalytics() { export default function GoogleAnalytics() {
if (process.env.NODE_ENV !== "production") { if (process.env.NODE_ENV !== 'production') {
return null; return null;
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -14,8 +14,8 @@ import StatsSection from '@/components/blocks/stats/stats';
import TestimonialsSection from '@/components/blocks/testimonials/testimonials'; import TestimonialsSection from '@/components/blocks/testimonials/testimonials';
import { constructMetadata } from '@/lib/metadata'; import { constructMetadata } from '@/lib/metadata';
import { getUrlWithLocale } from '@/lib/urls/urls'; import { getUrlWithLocale } from '@/lib/urls/urls';
import { Metadata } from 'next'; import type { Metadata } from 'next';
import { Locale } from 'next-intl'; import type { Locale } from 'next-intl';
import { getTranslations } from 'next-intl/server'; 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 { getUrlWithLocale } from '@/lib/urls/urls';
import type { NextPageProps } from '@/types/next-page-props'; import type { NextPageProps } from '@/types/next-page-props';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import { Locale } from 'next-intl'; import type { Locale } from 'next-intl';
import { getTranslations } from 'next-intl/server'; import { getTranslations } from 'next-intl/server';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
@ -13,7 +13,7 @@ export async function generateMetadata({
}: { }: {
params: Promise<{ locale: Locale }>; params: Promise<{ locale: Locale }>;
}): Promise<Metadata | undefined> { }): Promise<Metadata | undefined> {
const {locale} = await params; const { locale } = await params;
const page = await getPage('cookie-policy', locale); const page = await getPage('cookie-policy', locale);
if (!page) { if (!page) {
@ -23,12 +23,12 @@ export async function generateMetadata({
return {}; return {};
} }
const t = await getTranslations({locale, namespace: 'Metadata'}); const t = await getTranslations({ locale, namespace: 'Metadata' });
return constructMetadata({ return constructMetadata({
title: page.title + ' | ' + t('title'), title: page.title + ' | ' + t('title'),
description: page.description, description: page.description,
canonicalUrl: getUrlWithLocale("/cookie", locale), canonicalUrl: getUrlWithLocale('/cookie', locale),
}); });
} }

View File

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

View File

@ -4,7 +4,7 @@ import { getPage } from '@/lib/page/get-page';
import { getUrlWithLocale } from '@/lib/urls/urls'; import { getUrlWithLocale } from '@/lib/urls/urls';
import type { NextPageProps } from '@/types/next-page-props'; import type { NextPageProps } from '@/types/next-page-props';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import { Locale } from 'next-intl'; import type { Locale } from 'next-intl';
import { getTranslations } from 'next-intl/server'; import { getTranslations } from 'next-intl/server';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
@ -13,7 +13,7 @@ export async function generateMetadata({
}: { }: {
params: Promise<{ locale: Locale }>; params: Promise<{ locale: Locale }>;
}): Promise<Metadata | undefined> { }): Promise<Metadata | undefined> {
const {locale} = await params; const { locale } = await params;
const page = await getPage('privacy-policy', locale); const page = await getPage('privacy-policy', locale);
if (!page) { if (!page) {
@ -23,12 +23,12 @@ export async function generateMetadata({
return {}; return {};
} }
const t = await getTranslations({locale, namespace: 'Metadata'}); const t = await getTranslations({ locale, namespace: 'Metadata' });
return constructMetadata({ return constructMetadata({
title: page.title + ' | ' + t('title'), title: page.title + ' | ' + t('title'),
description: page.description, 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 { getUrlWithLocale } from '@/lib/urls/urls';
import type { NextPageProps } from '@/types/next-page-props'; import type { NextPageProps } from '@/types/next-page-props';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import { Locale } from 'next-intl'; import type { Locale } from 'next-intl';
import { getTranslations } from 'next-intl/server'; import { getTranslations } from 'next-intl/server';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
@ -13,7 +13,7 @@ export async function generateMetadata({
}: { }: {
params: Promise<{ locale: Locale }>; params: Promise<{ locale: Locale }>;
}): Promise<Metadata | undefined> { }): Promise<Metadata | undefined> {
const {locale} = await params; const { locale } = await params;
const page = await getPage('terms-of-service', locale); const page = await getPage('terms-of-service', locale);
if (!page) { if (!page) {
@ -23,12 +23,12 @@ export async function generateMetadata({
return {}; return {};
} }
const t = await getTranslations({locale, namespace: 'Metadata'}); const t = await getTranslations({ locale, namespace: 'Metadata' });
return constructMetadata({ return constructMetadata({
title: page.title + ' | ' + t('title'), title: page.title + ' | ' + t('title'),
description: page.description, 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 { constructMetadata } from '@/lib/metadata';
import { getUrlWithLocale } from '@/lib/urls/urls'; import { getUrlWithLocale } from '@/lib/urls/urls';
import { MailIcon } from 'lucide-react'; import { MailIcon } from 'lucide-react';
import { Metadata } from 'next'; import type { Metadata } from 'next';
import { Locale } from 'next-intl'; import type { Locale } from 'next-intl';
import { getTranslations } from 'next-intl/server'; import { getTranslations } from 'next-intl/server';
export async function generateMetadata({ export async function generateMetadata({
@ -20,7 +20,7 @@ export async function generateMetadata({
return constructMetadata({ return constructMetadata({
title: pt('title') + ' | ' + t('title'), title: pt('title') + ' | ' + t('title'),
description: pt('description'), description: pt('description'),
canonicalUrl: getUrlWithLocale("/about", locale), canonicalUrl: getUrlWithLocale('/about', locale),
}); });
} }
@ -49,9 +49,7 @@ export default async function AboutPage() {
</AvatarFallback> </AvatarFallback>
</Avatar> </Avatar>
<div> <div>
<h1 className="text-4xl text-foreground"> <h1 className="text-4xl text-foreground">{t('authorName')}</h1>
{t('authorName')}
</h1>
<p className="text-base text-muted-foreground mt-2"> <p className="text-base text-muted-foreground mt-2">
{t('authorBio')} {t('authorBio')}
</p> </p>
@ -67,7 +65,9 @@ export default async function AboutPage() {
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<Button className="rounded-lg cursor-pointer"> <Button className="rounded-lg cursor-pointer">
<MailIcon className="mr-1 size-4" /> <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> </Button>
</div> </div>
</div> </div>

View File

@ -4,7 +4,7 @@ import { getReleases } from '@/lib/release/get-releases';
import { getUrlWithLocale } from '@/lib/urls/urls'; import { getUrlWithLocale } from '@/lib/urls/urls';
import type { NextPageProps } from '@/types/next-page-props'; import type { NextPageProps } from '@/types/next-page-props';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import { Locale } from 'next-intl'; import type { Locale } from 'next-intl';
import { getTranslations } from 'next-intl/server'; import { getTranslations } from 'next-intl/server';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
@ -15,14 +15,14 @@ export async function generateMetadata({
}: { }: {
params: Promise<{ locale: Locale }>; params: Promise<{ locale: Locale }>;
}): Promise<Metadata | undefined> { }): Promise<Metadata | undefined> {
const {locale} = await params; const { locale } = await params;
const t = await getTranslations({locale, namespace: 'Metadata'}); const t = await getTranslations({ locale, namespace: 'Metadata' });
const pt = await getTranslations({locale, namespace: 'ChangelogPage'}); const pt = await getTranslations({ locale, namespace: 'ChangelogPage' });
return constructMetadata({ return constructMetadata({
title: pt('title') + ' | ' + t('title'), title: pt('title') + ' | ' + t('title'),
description: pt('description'), description: pt('description'),
canonicalUrl: getUrlWithLocale("/changelog", locale), canonicalUrl: getUrlWithLocale('/changelog', locale),
}); });
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,8 +2,8 @@ import BlockPreview from '@/components/nsui/block-preview';
import { blocks, categories } from '@/components/nsui/blocks'; import { blocks, categories } from '@/components/nsui/blocks';
import { constructMetadata } from '@/lib/metadata'; import { constructMetadata } from '@/lib/metadata';
import { getUrlWithLocale } from '@/lib/urls/urls'; import { getUrlWithLocale } from '@/lib/urls/urls';
import { Metadata } from 'next'; import type { Metadata } from 'next';
import { Locale } from 'next-intl'; import type { Locale } from 'next-intl';
import { getTranslations } from 'next-intl/server'; import { getTranslations } from 'next-intl/server';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
@ -26,7 +26,7 @@ export async function generateMetadata({
return constructMetadata({ return constructMetadata({
title: category + ' | ' + t('title'), title: category + ' | ' + t('title'),
description: t('description'), 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 { websiteConfig } from '@/config/website';
import { constructMetadata } from '@/lib/metadata'; import { constructMetadata } from '@/lib/metadata';
import { getUrlWithLocale } from '@/lib/urls/urls'; 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 { allCategories, allPosts } from 'content-collections';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import { Locale } from 'next-intl'; import type { Locale } from 'next-intl';
import { getTranslations } from 'next-intl/server'; import { getTranslations } from 'next-intl/server';
export async function generateMetadata({ export async function generateMetadata({
@ -29,12 +29,12 @@ export async function generateMetadata({
return {}; return {};
} }
const t = await getTranslations({locale, namespace: 'Metadata'}); const t = await getTranslations({ locale, namespace: 'Metadata' });
return constructMetadata({ return constructMetadata({
title: `${category.name} | ${t('title')}`, title: `${category.name} | ${t('title')}`,
description: category.description, 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 { BlogCategoryFilter } from '@/components/blog/blog-category-filter';
import Container from '@/components/layout/container'; 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 { allCategories } from 'content-collections';
import { getTranslations } from 'next-intl/server'; 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({ export default async function BlogListLayout({
children, children,

View File

@ -4,10 +4,10 @@ import CustomPagination from '@/components/shared/pagination';
import { websiteConfig } from '@/config/website'; import { websiteConfig } from '@/config/website';
import { constructMetadata } from '@/lib/metadata'; import { constructMetadata } from '@/lib/metadata';
import { getUrlWithLocale } from '@/lib/urls/urls'; 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 { allPosts } from 'content-collections';
import { Metadata } from 'next'; import type { Metadata } from 'next';
import { Locale } from 'next-intl'; import type { Locale } from 'next-intl';
import { getTranslations } from 'next-intl/server'; import { getTranslations } from 'next-intl/server';
export async function generateMetadata({ export async function generateMetadata({
@ -15,13 +15,13 @@ export async function generateMetadata({
}: { }: {
params: Promise<{ locale: Locale }>; params: Promise<{ locale: Locale }>;
}): Promise<Metadata | undefined> { }): Promise<Metadata | undefined> {
const {locale} = await params; const { locale } = await params;
const t = await getTranslations({locale, namespace: 'Metadata'}); const t = await getTranslations({ locale, namespace: 'Metadata' });
const pt = await getTranslations({locale, namespace: 'BlogPage'}); const pt = await getTranslations({ locale, namespace: 'BlogPage' });
return constructMetadata({ return constructMetadata({
title: pt('title') + ' | ' + t('title'), title: pt('title') + ' | ' + t('title'),
description: pt('description'), description: pt('description'),
canonicalUrl: getUrlWithLocale("/blog", locale), canonicalUrl: getUrlWithLocale('/blog', locale),
}); });
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,8 @@
import { ErrorCard } from '@/components/auth/error-card'; import { ErrorCard } from '@/components/auth/error-card';
import { constructMetadata } from '@/lib/metadata'; import { constructMetadata } from '@/lib/metadata';
import { getUrlWithLocale } from '@/lib/urls/urls'; import { getUrlWithLocale } from '@/lib/urls/urls';
import { Metadata } from 'next'; import type { Metadata } from 'next';
import { Locale } from 'next-intl'; import type { Locale } from 'next-intl';
import { getTranslations } from 'next-intl/server'; import { getTranslations } from 'next-intl/server';
export async function generateMetadata({ export async function generateMetadata({
@ -10,9 +10,9 @@ export async function generateMetadata({
}: { }: {
params: Promise<{ locale: Locale }>; params: Promise<{ locale: Locale }>;
}): Promise<Metadata | undefined> { }): Promise<Metadata | undefined> {
const {locale} = await params; const { locale } = await params;
const t = await getTranslations({locale, namespace: 'Metadata'}); const t = await getTranslations({ locale, namespace: 'Metadata' });
const pt = await getTranslations({locale, namespace: 'AuthPage.error'}); const pt = await getTranslations({ locale, namespace: 'AuthPage.error' });
return constructMetadata({ return constructMetadata({
title: pt('title') + ' | ' + t('title'), title: pt('title') + ' | ' + t('title'),

View File

@ -1,8 +1,8 @@
import { ForgotPasswordForm } from '@/components/auth/forgot-password-form'; import { ForgotPasswordForm } from '@/components/auth/forgot-password-form';
import { constructMetadata } from '@/lib/metadata'; import { constructMetadata } from '@/lib/metadata';
import { getUrlWithLocale } from '@/lib/urls/urls'; import { getUrlWithLocale } from '@/lib/urls/urls';
import { Metadata } from 'next'; import type { Metadata } from 'next';
import { Locale } from 'next-intl'; import type { Locale } from 'next-intl';
import { getTranslations } from 'next-intl/server'; import { getTranslations } from 'next-intl/server';
export async function generateMetadata({ export async function generateMetadata({
@ -10,14 +10,17 @@ export async function generateMetadata({
}: { }: {
params: Promise<{ locale: Locale }>; params: Promise<{ locale: Locale }>;
}): Promise<Metadata | undefined> { }): Promise<Metadata | undefined> {
const {locale} = await params; const { locale } = await params;
const t = await getTranslations({locale, namespace: 'Metadata'}); const t = await getTranslations({ locale, namespace: 'Metadata' });
const pt = await getTranslations({locale, namespace: 'AuthPage.forgotPassword'}); const pt = await getTranslations({
locale,
namespace: 'AuthPage.forgotPassword',
});
return constructMetadata({ return constructMetadata({
title: pt('title') + ' | ' + t('title'), title: pt('title') + ' | ' + t('title'),
description: t('description'), 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 { constructMetadata } from '@/lib/metadata';
import { getUrlWithLocale } from '@/lib/urls/urls'; import { getUrlWithLocale } from '@/lib/urls/urls';
import { Routes } from '@/routes'; import { Routes } from '@/routes';
import { Metadata } from 'next'; import type { Metadata } from 'next';
import { Locale } from 'next-intl'; import type { Locale } from 'next-intl';
import { getTranslations } from 'next-intl/server'; import { getTranslations } from 'next-intl/server';
export async function generateMetadata({ export async function generateMetadata({
@ -12,14 +12,14 @@ export async function generateMetadata({
}: { }: {
params: Promise<{ locale: Locale }>; params: Promise<{ locale: Locale }>;
}): Promise<Metadata | undefined> { }): Promise<Metadata | undefined> {
const {locale} = await params; const { locale } = await params;
const t = await getTranslations({locale, namespace: 'Metadata'}); const t = await getTranslations({ locale, namespace: 'Metadata' });
const pt = await getTranslations({locale, namespace: 'AuthPage.login'}); const pt = await getTranslations({ locale, namespace: 'AuthPage.login' });
return constructMetadata({ return constructMetadata({
title: pt('title') + ' | ' + t('title'), title: pt('title') + ' | ' + t('title'),
description: t('description'), description: t('description'),
canonicalUrl: getUrlWithLocale("/auth/login", locale), canonicalUrl: getUrlWithLocale('/auth/login', locale),
}); });
} }
@ -47,4 +47,4 @@ export default async function LoginPage() {
</div> </div>
</div> </div>
); );
}; }

View File

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

View File

@ -1,8 +1,8 @@
import { ResetPasswordForm } from '@/components/auth/reset-password-form'; import { ResetPasswordForm } from '@/components/auth/reset-password-form';
import { constructMetadata } from '@/lib/metadata'; import { constructMetadata } from '@/lib/metadata';
import { getUrlWithLocale } from '@/lib/urls/urls'; import { getUrlWithLocale } from '@/lib/urls/urls';
import { Metadata } from 'next'; import type { Metadata } from 'next';
import { Locale } from 'next-intl'; import type { Locale } from 'next-intl';
import { getTranslations } from 'next-intl/server'; import { getTranslations } from 'next-intl/server';
export async function generateMetadata({ export async function generateMetadata({
@ -10,14 +10,17 @@ export async function generateMetadata({
}: { }: {
params: Promise<{ locale: Locale }>; params: Promise<{ locale: Locale }>;
}): Promise<Metadata | undefined> { }): Promise<Metadata | undefined> {
const {locale} = await params; const { locale } = await params;
const t = await getTranslations({locale, namespace: 'Metadata'}); const t = await getTranslations({ locale, namespace: 'Metadata' });
const pt = await getTranslations({locale, namespace: 'AuthPage.resetPassword'}); const pt = await getTranslations({
locale,
namespace: 'AuthPage.resetPassword',
});
return constructMetadata({ return constructMetadata({
title: pt('title') + ' | ' + t('title'), title: pt('title') + ' | ' + t('title'),
description: t('description'), 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 * as Preview from '@/components/docs';
import { CustomMDXContent } from '@/components/shared/custom-mdx-content'; 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 { LOCALES } from '@/i18n/routing';
import { source } from '@/lib/docs/source'; import { source } from '@/lib/docs/source';
import Link from 'fumadocs-core/link'; 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 type { Metadata } from 'next';
import { Locale } from 'next-intl'; import type { Locale } from 'next-intl';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import { ReactNode } from 'react'; import type { ReactNode } from 'react';
export function generateStaticParams() { export function generateStaticParams() {
const locales = LOCALES; const locales = LOCALES;
const slugParams = source.generateParams(); const slugParams = source.generateParams();
const params = locales.flatMap(locale => const params = locales.flatMap((locale) =>
slugParams.map(param => ({ slugParams.map((param) => ({
locale, locale,
slug: param.slug slug: param.slug,
})) }))
); );
return params; return params;
} }
export async function generateMetadata({ export async function generateMetadata({ params }: DocPageProps) {
params,
}: DocPageProps) {
const { slug, locale } = await params; const { slug, locale } = await params;
const language = locale as string; const language = locale as string;
const page = source.getPage(slug, language); const page = source.getPage(slug, language);
@ -54,7 +61,7 @@ export const revalidate = false;
interface DocPageProps { interface DocPageProps {
params: Promise<{ params: Promise<{
slug?: string[]; slug?: string[];
locale: Locale locale: Locale;
}>; }>;
} }
@ -64,9 +71,7 @@ interface DocPageProps {
* ref: * ref:
* https://github.com/fuma-nama/fumadocs/blob/dev/apps/docs/app/docs/%5B...slug%5D/page.tsx * https://github.com/fuma-nama/fumadocs/blob/dev/apps/docs/app/docs/%5B...slug%5D/page.tsx
*/ */
export default async function DocPage({ export default async function DocPage({ params }: DocPageProps) {
params,
}: DocPageProps) {
const { slug, locale } = await params; const { slug, locale } = await params;
const language = locale as string; const language = locale as string;
const page = source.getPage(slug, language); const page = source.getPage(slug, language);
@ -79,18 +84,15 @@ export default async function DocPage({
const preview = page.data.preview; const preview = page.data.preview;
return ( return (
<DocsPage toc={page.data.toc} <DocsPage
toc={page.data.toc}
full={page.data.full} full={page.data.full}
tableOfContent={{ tableOfContent={{
style: "clerk", style: 'clerk',
}} }}
> >
<DocsTitle> <DocsTitle>{page.data.title}</DocsTitle>
{page.data.title} <DocsDescription>{page.data.description}</DocsDescription>
</DocsTitle>
<DocsDescription>
{page.data.description}
</DocsDescription>
<DocsBody> <DocsBody>
{/* Preview Rendered Component */} {/* Preview Rendered Component */}
{preview ? <PreviewRenderer preview={preview} /> : null} {preview ? <PreviewRenderer preview={preview} /> : null}
@ -99,7 +101,7 @@ export default async function DocPage({
<CustomMDXContent <CustomMDXContent
code={page.data.body} code={page.data.body}
customComponents={{ customComponents={{
a: ({ href, ...props }: { href?: string;[key: string]: any }) => { a: ({ href, ...props }: { href?: string; [key: string]: any }) => {
const found = source.getPageByHref(href ?? '', { const found = source.getPageByHref(href ?? '', {
dir: page.file.dirname, dir: page.file.dirname,
}); });

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import { docsI18nConfig } from '@/lib/docs/i18n';
import { source } from '@/lib/docs/source'; import { source } from '@/lib/docs/source';
import { createTokenizer } from '@orama/tokenizers/mandarin'; import { createTokenizer } from '@orama/tokenizers/mandarin';
import { createI18nSearchAPI } from 'fumadocs-core/search/server'; import { createI18nSearchAPI } from 'fumadocs-core/search/server';
import { docsI18nConfig } from '@/lib/docs/i18n';
/** /**
* Fumadocs i18n search configuration * Fumadocs i18n search configuration
@ -26,7 +26,7 @@ const searchAPI = createI18nSearchAPI('advanced', {
id: page.url, id: page.url,
url: page.url, url: page.url,
locale: language, locale: language,
})), }))
), ),
// Configure special language tokenizers and search options // Configure special language tokenizers and search options
@ -73,7 +73,10 @@ export const GET = async (request: Request) => {
console.log('search, referer pathname:', refererUrl.pathname); console.log('search, referer pathname:', refererUrl.pathname);
const refererPathParts = refererUrl.pathname.split('/').filter(Boolean); const refererPathParts = refererUrl.pathname.split('/').filter(Boolean);
console.log('search, referer path parts:', refererPathParts); 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]; locale = refererPathParts[0];
console.log(`search, detected locale from referer: ${locale}`); 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 { 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) { export async function POST(request: NextRequest) {
try { try {
@ -68,10 +68,7 @@ export async function POST(request: NextRequest) {
console.error('Error getting file URL:', error); console.error('Error getting file URL:', error);
if (error instanceof StorageError) { if (error instanceof StorageError) {
return NextResponse.json( return NextResponse.json({ error: error.message }, { status: 500 });
{ error: error.message },
{ status: 500 }
);
} }
return NextResponse.json( return NextResponse.json(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -19,13 +19,18 @@ interface SocialLoginButtonProps {
/** /**
* social login buttons * social login buttons
*/ */
export const SocialLoginButton = ({ callbackUrl: propCallbackUrl }: SocialLoginButtonProps) => { export const SocialLoginButton = ({
callbackUrl: propCallbackUrl,
}: SocialLoginButtonProps) => {
const t = useTranslations('AuthPage.login'); const t = useTranslations('AuthPage.login');
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const paramCallbackUrl = searchParams.get('callbackUrl'); const paramCallbackUrl = searchParams.get('callbackUrl');
// Use prop callback URL or param callback URL if provided, otherwise use the default login redirect // Use prop callback URL or param callback URL if provided, otherwise use the default login redirect
const locale = useLocale(); const locale = useLocale();
const defaultCallbackUrl = getUrlWithLocaleInCallbackUrl(DEFAULT_LOGIN_REDIRECT, locale); const defaultCallbackUrl = getUrlWithLocaleInCallbackUrl(
DEFAULT_LOGIN_REDIRECT,
locale
);
const callbackUrl = propCallbackUrl || paramCallbackUrl || defaultCallbackUrl; const callbackUrl = propCallbackUrl || paramCallbackUrl || defaultCallbackUrl;
const [isLoading, setIsLoading] = useState<'google' | 'github' | null>(null); const [isLoading, setIsLoading] = useState<'google' | 'github' | null>(null);
console.log('social login button, callbackUrl', callbackUrl); 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"> <h2 className="text-balance text-4xl font-semibold lg:text-5xl">
{t('title')} {t('title')}
</h2> </h2>
<p className="mt-4"> <p className="mt-4">{t('description')}</p>
{t('description')}
</p>
<div className="mt-12 flex flex-wrap justify-center gap-4"> <div className="mt-12 flex flex-wrap justify-center gap-4">
<Button asChild size="lg"> <Button asChild size="lg">

View File

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

View File

@ -1,5 +1,6 @@
'use client'; 'use client';
import { BorderBeam } from '@/components/magicui/border-beam';
import { import {
Accordion, Accordion,
AccordionContent, AccordionContent,
@ -12,11 +13,10 @@ import {
Fingerprint, Fingerprint,
IdCard, IdCard,
} from 'lucide-react'; } from 'lucide-react';
import { AnimatePresence, motion } from 'motion/react';
import { useTranslations } from 'next-intl';
import Image from 'next/image'; import Image from 'next/image';
import { useState } from 'react'; 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 * https://nsui.irung.me/features
@ -48,15 +48,13 @@ export default function Features2Section() {
return ( return (
<section className="py-16"> <section className="py-16">
<div className="bg-linear-to-b absolute inset-0 -z-10 sm:inset-6 sm:rounded-b-3xl dark:block dark:to-[color-mix(in_oklab,var(--color-zinc-900)_75%,var(--color-background))]"></div> <div className="bg-linear-to-b absolute inset-0 -z-10 sm:inset-6 sm:rounded-b-3xl dark:block dark:to-[color-mix(in_oklab,var(--color-zinc-900)_75%,var(--color-background))]" />
<div className="mx-auto max-w-6xl space-y-8 px-6 md:space-y-16 lg:space-y-20 dark:[--color-border:color-mix(in_oklab,var(--color-white)_10%,transparent)]"> <div className="mx-auto max-w-6xl space-y-8 px-6 md:space-y-16 lg:space-y-20 dark:[--color-border:color-mix(in_oklab,var(--color-white)_10%,transparent)]">
<div className="relative z-10 mx-auto max-w-2xl space-y-6 text-center"> <div className="relative z-10 mx-auto max-w-2xl space-y-6 text-center">
<h2 className="text-balance text-4xl lg:text-5xl font-semibold"> <h2 className="text-balance text-4xl lg:text-5xl font-semibold">
{t('title')} {t('title')}
</h2> </h2>
<p> <p>{t('description')}</p>
{t('description')}
</p>
</div> </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="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'; 'use client';
import { BorderBeam } from '@/components/magicui/border-beam';
import { import {
Accordion, Accordion,
AccordionContent, AccordionContent,
@ -12,11 +13,10 @@ import {
Fingerprint, Fingerprint,
IdCard, IdCard,
} from 'lucide-react'; } from 'lucide-react';
import { AnimatePresence, motion } from 'motion/react';
import { useTranslations } from 'next-intl';
import Image from 'next/image'; import Image from 'next/image';
import { useState } from 'react'; 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 * https://nsui.irung.me/features
@ -54,13 +54,10 @@ export default function Features2Section() {
<h2 className="text-balance text-4xl lg:text-5xl font-semibold"> <h2 className="text-balance text-4xl lg:text-5xl font-semibold">
{t('title')} {t('title')}
</h2> </h2>
<p> <p>{t('description')}</p>
{t('description')}
</p>
</div> </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="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="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"> <div className="aspect-76/59 bg-background relative w-full rounded-2xl">
<AnimatePresence mode="wait"> <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 { useTranslations } from 'next-intl';
import Image from 'next/image'; 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="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="lg:col-span-2">
<div className="md:pr-6 lg:pr-0"> <div className="md:pr-6 lg:pr-0">
<h2 className="text-4xl font-semibold"> <h2 className="text-4xl font-semibold">{t('title')}</h2>
{t('title')} <p className="mt-6">{t('description')}</p>
</h2>
<p className="mt-6">
{t('description')}
</p>
</div> </div>
<ul className="mt-8 divide-y border-y *:flex *:items-center *:gap-3 *:py-3"> <ul className="mt-8 divide-y border-y *:flex *:items-center *:gap-3 *:py-3">

View File

@ -1,4 +1,9 @@
import { ActivityIcon, DraftingCompassIcon, MailIcon, ZapIcon } from 'lucide-react'; import {
ActivityIcon,
DraftingCompassIcon,
MailIcon,
ZapIcon,
} from 'lucide-react';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import Image from 'next/image'; import Image from 'next/image';
@ -36,12 +41,8 @@ export default function Features4Section() {
<div className="lg:col-span-2"> <div className="lg:col-span-2">
<div className="md:pr-6 lg:pr-0"> <div className="md:pr-6 lg:pr-0">
<h2 className="text-4xl font-semibold"> <h2 className="text-4xl font-semibold">{t('title')}</h2>
{t('title')} <p className="mt-6">{t('description')}</p>
</h2>
<p className="mt-6">
{t('description')}
</p>
</div> </div>
<ul className="mt-8 divide-y border-y *:flex *:items-center *:gap-3 *:py-3"> <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'; import { useTranslations } from 'next-intl';
/** /**
@ -15,16 +22,16 @@ export default function Features5Section() {
<h2 className="text-balance text-4xl lg:text-5xl font-semibold"> <h2 className="text-balance text-4xl lg:text-5xl font-semibold">
{t('title')} {t('title')}
</h2> </h2>
<p className="mt-4"> <p className="mt-4">{t('description')}</p>
{t('description')}
</p>
</div> </div>
<div className="relative mx-auto grid divide-x divide-y border *:p-8 sm:grid-cols-2 lg:grid-cols-3"> <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="space-y-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<ZapIcon className="size-4" /> <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> </div>
<p className="text-sm text-muted-foreground mt-4"> <p className="text-sm text-muted-foreground mt-4">
{t('items.item-1.description')} {t('items.item-1.description')}
@ -33,7 +40,9 @@ export default function Features5Section() {
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<CpuIcon className="size-4" /> <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> </div>
<p className="text-sm text-muted-foreground mt-4"> <p className="text-sm text-muted-foreground mt-4">
{t('items.item-2.description')} {t('items.item-2.description')}
@ -43,7 +52,9 @@ export default function Features5Section() {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FingerprintIcon className="size-4" /> <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> </div>
<p className="text-sm text-muted-foreground mt-4"> <p className="text-sm text-muted-foreground mt-4">
{t('items.item-3.description')} {t('items.item-3.description')}
@ -53,7 +64,9 @@ export default function Features5Section() {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<PencilIcon className="size-4" /> <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> </div>
<p className="text-sm text-muted-foreground mt-4"> <p className="text-sm text-muted-foreground mt-4">
{t('items.item-4.description')} {t('items.item-4.description')}
@ -63,7 +76,9 @@ export default function Features5Section() {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Settings2Icon className="size-4" /> <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> </div>
<p className="text-sm text-muted-foreground mt-4"> <p className="text-sm text-muted-foreground mt-4">
{t('items.item-5.description')} {t('items.item-5.description')}
@ -73,7 +88,9 @@ export default function Features5Section() {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<SparklesIcon className="size-4" /> <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> </div>
<p className="text-sm text-muted-foreground mt-4"> <p className="text-sm text-muted-foreground mt-4">
{t('items.item-6.description')} {t('items.item-6.description')}

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import { useTranslations } from "next-intl"; import { useTranslations } from 'next-intl';
export default function LogoCloudSection() { export default function LogoCloudSection() {
const t = useTranslations('HomePage.logocloud'); const t = useTranslations('HomePage.logocloud');
@ -6,9 +6,7 @@ export default function LogoCloudSection() {
return ( return (
<section className="py-16"> <section className="py-16">
<div className="mx-auto max-w-5xl px-6"> <div className="mx-auto max-w-5xl px-6">
<h2 className="text-center text-xl font-medium"> <h2 className="text-center text-xl font-medium">{t('title')}</h2>
{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"> <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 <img
className="h-4 w-fit dark:invert" className="h-4 w-fit dark:invert"
@ -84,5 +82,4 @@ export default function LogoCloudSection() {
</div> </div>
</section> </section>
); );
} }

View File

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

View File

@ -1,4 +1,4 @@
import { useTranslations } from "next-intl"; import { useTranslations } from 'next-intl';
export default function StatsSection() { export default function StatsSection() {
const t = useTranslations('HomePage.stats'); 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"> <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="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"> <div className="relative z-10 mx-auto max-w-xl space-y-6 text-center">
<h2 className="text-4xl font-medium lg:text-5xl"> <h2 className="text-4xl font-medium lg:text-5xl">{t('title')}</h2>
{t('title')} <p>{t('description')}</p>
</h2>
<p>
{t('description')}
</p>
</div> </div>
<div className="grid gap-12 divide-y-0 *:text-center md:grid-cols-3 md:gap-2 md:divide-x"> <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'), role: t('items.item-12.role'),
image: t('items.item-12.image'), image: t('items.item-12.image'),
quote: t('items.item-12.quote'), quote: t('items.item-12.quote'),
} },
]; ];
const testimonialChunks = chunkArray( const testimonialChunks = chunkArray(
@ -111,9 +111,7 @@ export default function TestimonialsSection() {
<h2 className="text-title text-4xl lg:text-5xl font-semibold"> <h2 className="text-title text-4xl lg:text-5xl font-semibold">
{t('title')} {t('title')}
</h2> </h2>
<p className="text-body mt-6"> <p className="text-body mt-6">{t('description')}</p>
{t('description')}
</p>
</div> </div>
<div className="mt-8 grid gap-3 sm:grid-cols-2 md:mt-12 lg:grid-cols-3"> <div className="mt-8 grid gap-3 sm:grid-cols-2 md:mt-12 lg:grid-cols-3">
{testimonialChunks.map((chunk, chunkIndex) => ( {testimonialChunks.map((chunk, chunkIndex) => (

View File

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

View File

@ -1,5 +1,5 @@
import Container from '@/components/layout/container'; 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 { BlogCategoryListDesktop } from './blog-category-list-desktop';
import { BlogCategoryListMobile } from './blog-category-list-mobile'; import { BlogCategoryListMobile } from './blog-category-list-mobile';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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