refactor: biome lint part 1
This commit is contained in:
parent
5b02b0379f
commit
23cd59bbac
@ -66,6 +66,8 @@
|
||||
"src/components/magicui/*.tsx",
|
||||
"src/app/[[]locale]/preview/**",
|
||||
"src/db/schema.ts",
|
||||
"src/payment/types.ts",
|
||||
"src/types/index.d.ts",
|
||||
"public/sw.js"
|
||||
]
|
||||
},
|
||||
@ -76,4 +78,4 @@
|
||||
"semicolons": "always"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,18 +1,18 @@
|
||||
import { DEFAULT_LOCALE, LOCALES } from "@/i18n/routing";
|
||||
import { defineCollection, defineConfig } from "@content-collections/core";
|
||||
import path from 'path';
|
||||
import { DEFAULT_LOCALE, LOCALES } from '@/i18n/routing';
|
||||
import { defineCollection, defineConfig } from '@content-collections/core';
|
||||
import {
|
||||
createDocSchema,
|
||||
createMetaSchema,
|
||||
transformMDX,
|
||||
} from '@fumadocs/content-collections/configuration';
|
||||
import path from "path";
|
||||
|
||||
/**
|
||||
* 1. Content Collections documentation
|
||||
* https://www.content-collections.dev/docs/quickstart/next
|
||||
* https://www.content-collections.dev/docs/configuration
|
||||
* https://www.content-collections.dev/docs/transform#join-collections
|
||||
*
|
||||
*
|
||||
* 2. Use Content Collections for Fumadocs
|
||||
* https://fumadocs.vercel.app/docs/headless/content-collections
|
||||
*/
|
||||
@ -38,11 +38,11 @@ const metas = defineCollection({
|
||||
|
||||
/**
|
||||
* Blog Author collection
|
||||
*
|
||||
*
|
||||
* Authors are identified by their slug across all languages
|
||||
* New format: content/author/authorname.{locale}.mdx
|
||||
* Example: content/author/mksaas.mdx (default locale) and content/author/mksaas.zh.mdx (Chinese)
|
||||
*
|
||||
*
|
||||
* For author, slug is slugAsParams
|
||||
*/
|
||||
export const authors = defineCollection({
|
||||
@ -53,7 +53,7 @@ export const authors = defineCollection({
|
||||
slug: z.string(),
|
||||
name: z.string(),
|
||||
avatar: z.string(),
|
||||
locale: z.string().optional().default(DEFAULT_LOCALE)
|
||||
locale: z.string().optional().default(DEFAULT_LOCALE),
|
||||
}),
|
||||
transform: async (data, context) => {
|
||||
// Get the filename from the path
|
||||
@ -68,16 +68,16 @@ export const authors = defineCollection({
|
||||
...data,
|
||||
locale,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Blog Category collection
|
||||
*
|
||||
*
|
||||
* Categories are identified by their slug across all languages
|
||||
* New format: content/category/categoryname.{locale}.mdx
|
||||
* Example: content/category/tutorial.mdx (default locale) and content/category/tutorial.zh.mdx (Chinese)
|
||||
*
|
||||
*
|
||||
* For category, slug is slugAsParams
|
||||
*/
|
||||
export const categories = defineCollection({
|
||||
@ -88,7 +88,7 @@ export const categories = defineCollection({
|
||||
slug: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
locale: z.string().optional().default(DEFAULT_LOCALE)
|
||||
locale: z.string().optional().default(DEFAULT_LOCALE),
|
||||
}),
|
||||
transform: async (data, context) => {
|
||||
// Get the filename from the path
|
||||
@ -101,24 +101,24 @@ export const categories = defineCollection({
|
||||
|
||||
return {
|
||||
...data,
|
||||
locale
|
||||
locale,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Blog Post collection
|
||||
*
|
||||
*
|
||||
* New format: content/blog/post-slug.{locale}.mdx
|
||||
*
|
||||
*
|
||||
* slug: /blog/first-post, used in URL or sitemap
|
||||
* slugAsParams: first-post, used in route params
|
||||
*
|
||||
*
|
||||
* 1. For a blog post at content/blog/first-post.mdx (default locale):
|
||||
* locale: en
|
||||
* slug: /blog/first-post
|
||||
* slugAsParams: first-post
|
||||
*
|
||||
*
|
||||
* 2. For a blog post at content/blog/first-post.zh.mdx (Chinese locale):
|
||||
* locale: zh
|
||||
* slug: /blog/first-post
|
||||
@ -136,7 +136,7 @@ export const posts = defineCollection({
|
||||
published: z.boolean().default(true),
|
||||
categories: z.array(z.string()),
|
||||
author: z.string(),
|
||||
estimatedTime: z.number().optional() // Reading time in minutes
|
||||
estimatedTime: z.number().optional(), // Reading time in minutes
|
||||
}),
|
||||
transform: async (data, context) => {
|
||||
// Use Fumadocs transformMDX for consistent MDX processing
|
||||
@ -156,18 +156,20 @@ export const posts = defineCollection({
|
||||
.find((a) => a.slug === data.author && a.locale === locale);
|
||||
|
||||
// Find categories by matching slug and locale
|
||||
const blogCategories = data.categories.map(categorySlug => {
|
||||
const category = context
|
||||
.documents(categories)
|
||||
.find(c => c.slug === categorySlug && c.locale === locale);
|
||||
const blogCategories = data.categories
|
||||
.map((categorySlug) => {
|
||||
const category = context
|
||||
.documents(categories)
|
||||
.find((c) => c.slug === categorySlug && c.locale === locale);
|
||||
|
||||
return category;
|
||||
}).filter(Boolean); // Remove null values
|
||||
return category;
|
||||
})
|
||||
.filter(Boolean); // Remove null values
|
||||
|
||||
// Create the slug and slugAsParams
|
||||
const slug = `/blog/${base}`;
|
||||
const slugAsParams = base;
|
||||
|
||||
|
||||
// Calculate estimated reading time
|
||||
const wordCount = data.content.split(/\s+/).length;
|
||||
const wordsPerMinute = 200; // average reading speed: 200 words per minute
|
||||
@ -182,21 +184,21 @@ export const posts = defineCollection({
|
||||
slugAsParams,
|
||||
estimatedTime,
|
||||
body: transformedData.body,
|
||||
toc: transformedData.toc
|
||||
toc: transformedData.toc,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Pages collection for policy pages like privacy-policy, terms-of-service, etc.
|
||||
*
|
||||
*
|
||||
* New format: content/pages/page-slug.{locale}.mdx
|
||||
*
|
||||
*
|
||||
* 1. For a page at content/pages/privacy-policy.mdx (default locale):
|
||||
* locale: en
|
||||
* slug: /pages/privacy-policy
|
||||
* slugAsParams: privacy-policy
|
||||
*
|
||||
*
|
||||
* 2. For a page at content/pages/privacy-policy.zh.mdx (Chinese locale):
|
||||
* locale: zh
|
||||
* slug: /pages/privacy-policy
|
||||
@ -210,7 +212,7 @@ export const pages = defineCollection({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
date: z.string().datetime(),
|
||||
published: z.boolean().default(true)
|
||||
published: z.boolean().default(true),
|
||||
}),
|
||||
transform: async (data, context) => {
|
||||
// Use Fumadocs transformMDX for consistent MDX processing
|
||||
@ -234,21 +236,21 @@ export const pages = defineCollection({
|
||||
slug,
|
||||
slugAsParams,
|
||||
body: transformedData.body,
|
||||
toc: transformedData.toc
|
||||
toc: transformedData.toc,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Releases collection for changelog
|
||||
*
|
||||
*
|
||||
* New format: content/release/version-slug.{locale}.mdx
|
||||
*
|
||||
*
|
||||
* 1. For a release at content/release/v1-0-0.mdx (default locale):
|
||||
* locale: en
|
||||
* slug: /release/v1-0-0
|
||||
* slugAsParams: v1-0-0
|
||||
*
|
||||
*
|
||||
* 2. For a release at content/release/v1-0-0.zh.mdx (Chinese locale):
|
||||
* locale: zh
|
||||
* slug: /release/v1-0-0
|
||||
@ -263,7 +265,7 @@ export const releases = defineCollection({
|
||||
description: z.string(),
|
||||
date: z.string().datetime(),
|
||||
version: z.string(),
|
||||
published: z.boolean().default(true)
|
||||
published: z.boolean().default(true),
|
||||
}),
|
||||
transform: async (data, context) => {
|
||||
// Use Fumadocs transformMDX for consistent MDX processing
|
||||
@ -287,9 +289,9 @@ export const releases = defineCollection({
|
||||
slug,
|
||||
slugAsParams,
|
||||
body: transformedData.body,
|
||||
toc: transformedData.toc
|
||||
toc: transformedData.toc,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
@ -297,11 +299,14 @@ export const releases = defineCollection({
|
||||
* Handles filename formats:
|
||||
* - name -> locale: DEFAULT_LOCALE, base: name
|
||||
* - name.zh -> locale: zh, base: name
|
||||
*
|
||||
*
|
||||
* @param fileName Filename without extension (already has .mdx removed)
|
||||
* @returns Object with locale and base name
|
||||
*/
|
||||
function extractLocaleAndBase(fileName: string): { locale: string; base: string } {
|
||||
function extractLocaleAndBase(fileName: string): {
|
||||
locale: string;
|
||||
base: string;
|
||||
} {
|
||||
// Split filename into parts
|
||||
const parts = fileName.split('.');
|
||||
|
||||
@ -319,5 +324,5 @@ function extractLocaleAndBase(fileName: string): { locale: string; base: string
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
collections: [docs, metas, authors, categories, posts, pages, releases]
|
||||
});
|
||||
collections: [docs, metas, authors, categories, posts, pages, releases],
|
||||
});
|
||||
|
6
global.d.ts
vendored
6
global.d.ts
vendored
@ -1,9 +1,9 @@
|
||||
import { routing } from '@/i18n/routing';
|
||||
import messages from './messages/en.json';
|
||||
import type { routing } from '@/i18n/routing';
|
||||
import type messages from './messages/en.json';
|
||||
|
||||
/**
|
||||
* next-intl 4.0.0
|
||||
*
|
||||
*
|
||||
* https://github.com/amannn/next-intl/blob/main/examples/example-app-router/global.d.ts
|
||||
*/
|
||||
declare module 'next-intl' {
|
||||
|
@ -848,4 +848,4 @@
|
||||
"description": "MkSaaS lets you make AI SaaS in days, simply and effortlessly",
|
||||
"content": "Working in progress"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -671,7 +671,7 @@
|
||||
},
|
||||
"item-2": {
|
||||
"title": "产品特色功能二",
|
||||
"description": "请在这里详细描述您的产品特色功能二,尽可能详细,使其更吸引用户"
|
||||
"description": "请在这里详细描述您的产品特色功能二,尽可能详细,使其更吸引用户"
|
||||
},
|
||||
"item-3": {
|
||||
"title": "产品特色功能三",
|
||||
@ -849,4 +849,4 @@
|
||||
"description": "MkSaaS 让您在几天内轻松构建您的 AI SaaS,简单且毫不费力",
|
||||
"content": "正在开发中"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import type { NextConfig } from "next";
|
||||
import { withContentCollections } from '@content-collections/next';
|
||||
import type { NextConfig } from 'next';
|
||||
import createNextIntlPlugin from 'next-intl/plugin';
|
||||
import { withContentCollections } from "@content-collections/next";
|
||||
|
||||
/**
|
||||
* https://nextjs.org/docs/app/api-reference/config/next-config-js
|
||||
@ -12,22 +12,22 @@ const nextConfig: NextConfig = {
|
||||
// https://nextjs.org/docs/architecture/nextjs-compiler#remove-console
|
||||
// Remove all console.* calls in production only
|
||||
compiler: {
|
||||
removeConsole: process.env.NODE_ENV === "production",
|
||||
removeConsole: process.env.NODE_ENV === 'production',
|
||||
},
|
||||
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "avatars.githubusercontent.com",
|
||||
protocol: 'https',
|
||||
hostname: 'avatars.githubusercontent.com',
|
||||
},
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "lh3.googleusercontent.com",
|
||||
protocol: 'https',
|
||||
hostname: 'lh3.googleusercontent.com',
|
||||
},
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "randomuser.me",
|
||||
protocol: 'https',
|
||||
hostname: 'randomuser.me',
|
||||
},
|
||||
{
|
||||
protocol: 'https',
|
||||
@ -39,14 +39,14 @@ const nextConfig: NextConfig = {
|
||||
|
||||
/**
|
||||
* You can specify the path to the request config file or use the default one (@/i18n/request.ts)
|
||||
*
|
||||
*
|
||||
* https://next-intl.dev/docs/getting-started/app-router/with-i18n-routing#next-config
|
||||
*/
|
||||
const withNextIntl = createNextIntlPlugin();
|
||||
|
||||
/**
|
||||
* withContentCollections must be the outermost plugin
|
||||
*
|
||||
*
|
||||
* https://www.content-collections.dev/docs/quickstart/next
|
||||
*/
|
||||
export default withContentCollections(withNextIntl(nextConfig));
|
||||
|
@ -31,4 +31,4 @@ export const checkNewsletterStatusAction = actionClient
|
||||
error: error instanceof Error ? error.message : 'Something went wrong',
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -1,14 +1,14 @@
|
||||
'use server';
|
||||
|
||||
import { getSession } from "@/lib/server";
|
||||
import { findPlanByPlanId } from "@/lib/price-plan";
|
||||
import { getUrlWithLocale } from "@/lib/urls/urls";
|
||||
import { createCheckout } from "@/payment";
|
||||
import { CreateCheckoutParams } from "@/payment/types";
|
||||
import { getLocale } from "next-intl/server";
|
||||
import { findPlanByPlanId } from '@/lib/price-plan';
|
||||
import { getSession } from '@/lib/server';
|
||||
import { getUrlWithLocale } from '@/lib/urls/urls';
|
||||
import { createCheckout } from '@/payment';
|
||||
import type { CreateCheckoutParams } from '@/payment/types';
|
||||
import { Routes } from '@/routes';
|
||||
import { getLocale } from 'next-intl/server';
|
||||
import { createSafeActionClient } from 'next-safe-action';
|
||||
import { z } from 'zod';
|
||||
import { Routes } from "@/routes";
|
||||
|
||||
// Create a safe action client
|
||||
const actionClient = createSafeActionClient();
|
||||
@ -33,7 +33,9 @@ export const createCheckoutAction = actionClient
|
||||
// Get the current user session for authorization
|
||||
const session = await getSession();
|
||||
if (!session) {
|
||||
console.warn(`unauthorized request to create checkout session for user ${userId}`);
|
||||
console.warn(
|
||||
`unauthorized request to create checkout session for user ${userId}`
|
||||
);
|
||||
return {
|
||||
success: false,
|
||||
error: 'Unauthorized',
|
||||
@ -42,7 +44,9 @@ export const createCheckoutAction = actionClient
|
||||
|
||||
// Only allow users to create their own checkout session
|
||||
if (session.user.id !== userId) {
|
||||
console.warn(`current user ${session.user.id} is not authorized to create checkout session for user ${userId}`);
|
||||
console.warn(
|
||||
`current user ${session.user.id} is not authorized to create checkout session for user ${userId}`
|
||||
);
|
||||
return {
|
||||
success: false,
|
||||
error: 'Not authorized to do this action',
|
||||
@ -70,7 +74,10 @@ export const createCheckoutAction = actionClient
|
||||
};
|
||||
|
||||
// Create the checkout session with localized URLs
|
||||
const successUrl = getUrlWithLocale('/settings/billing?session_id={CHECKOUT_SESSION_ID}', locale);
|
||||
const successUrl = getUrlWithLocale(
|
||||
'/settings/billing?session_id={CHECKOUT_SESSION_ID}',
|
||||
locale
|
||||
);
|
||||
const cancelUrl = getUrlWithLocale(Routes.Pricing, locale);
|
||||
const params: CreateCheckoutParams = {
|
||||
planId,
|
||||
@ -89,10 +96,10 @@ export const createCheckoutAction = actionClient
|
||||
data: result,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("create checkout session error:", error);
|
||||
console.error('create checkout session error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Something went wrong',
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -1,13 +1,13 @@
|
||||
'use server';
|
||||
|
||||
import db from "@/db";
|
||||
import { user } from "@/db/schema";
|
||||
import { getSession } from "@/lib/server";
|
||||
import { getUrlWithLocale } from "@/lib/urls/urls";
|
||||
import { createCustomerPortal } from "@/payment";
|
||||
import { CreatePortalParams } from "@/payment/types";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { getLocale } from "next-intl/server";
|
||||
import db from '@/db';
|
||||
import { user } from '@/db/schema';
|
||||
import { getSession } from '@/lib/server';
|
||||
import { getUrlWithLocale } from '@/lib/urls/urls';
|
||||
import { createCustomerPortal } from '@/payment';
|
||||
import type { CreatePortalParams } from '@/payment/types';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { getLocale } from 'next-intl/server';
|
||||
import { createSafeActionClient } from 'next-safe-action';
|
||||
import { z } from 'zod';
|
||||
|
||||
@ -17,7 +17,10 @@ const actionClient = createSafeActionClient();
|
||||
// Portal schema for validation
|
||||
const portalSchema = z.object({
|
||||
userId: z.string().min(1, { message: 'User ID is required' }),
|
||||
returnUrl: z.string().url({ message: 'Return URL must be a valid URL' }).optional(),
|
||||
returnUrl: z
|
||||
.string()
|
||||
.url({ message: 'Return URL must be a valid URL' })
|
||||
.optional(),
|
||||
});
|
||||
|
||||
/**
|
||||
@ -27,11 +30,13 @@ export const createPortalAction = actionClient
|
||||
.schema(portalSchema)
|
||||
.action(async ({ parsedInput }) => {
|
||||
const { userId, returnUrl } = parsedInput;
|
||||
|
||||
|
||||
// Get the current user session for authorization
|
||||
const session = await getSession();
|
||||
if (!session) {
|
||||
console.warn(`unauthorized request to create portal session for user ${userId}`);
|
||||
console.warn(
|
||||
`unauthorized request to create portal session for user ${userId}`
|
||||
);
|
||||
return {
|
||||
success: false,
|
||||
error: 'Unauthorized',
|
||||
@ -40,7 +45,9 @@ export const createPortalAction = actionClient
|
||||
|
||||
// Only allow users to create their own portal session
|
||||
if (session.user.id !== userId) {
|
||||
console.warn(`current user ${session.user.id} is not authorized to create portal session for user ${userId}`);
|
||||
console.warn(
|
||||
`current user ${session.user.id} is not authorized to create portal session for user ${userId}`
|
||||
);
|
||||
return {
|
||||
success: false,
|
||||
error: 'Not authorized to do this action',
|
||||
@ -67,11 +74,12 @@ export const createPortalAction = actionClient
|
||||
const locale = await getLocale();
|
||||
|
||||
// Create the portal session with localized URL if no custom return URL is provided
|
||||
const returnUrlWithLocale = returnUrl || getUrlWithLocale('/settings/billing', locale);
|
||||
const returnUrlWithLocale =
|
||||
returnUrl || getUrlWithLocale('/settings/billing', locale);
|
||||
const params: CreatePortalParams = {
|
||||
customerId: customerResult[0].customerId,
|
||||
returnUrl: returnUrlWithLocale,
|
||||
locale
|
||||
locale,
|
||||
};
|
||||
|
||||
const result = await createCustomerPortal(params);
|
||||
@ -81,10 +89,10 @@ export const createPortalAction = actionClient
|
||||
data: result,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("create customer portal error:", error);
|
||||
console.error('create customer portal error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Something went wrong',
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -1,9 +1,9 @@
|
||||
'use server';
|
||||
|
||||
import { getSession } from "@/lib/server";
|
||||
import { getSubscriptions } from "@/payment";
|
||||
import { getSession } from '@/lib/server';
|
||||
import { getSubscriptions } from '@/payment';
|
||||
import { createSafeActionClient } from 'next-safe-action';
|
||||
import { z } from "zod";
|
||||
import { z } from 'zod';
|
||||
|
||||
// Create a safe action client
|
||||
const actionClient = createSafeActionClient();
|
||||
@ -15,8 +15,8 @@ const schema = z.object({
|
||||
|
||||
/**
|
||||
* Get active subscription data
|
||||
*
|
||||
* If the user has multiple subscriptions,
|
||||
*
|
||||
* If the user has multiple subscriptions,
|
||||
* it returns the most recent active or trialing one
|
||||
*/
|
||||
export const getActiveSubscriptionAction = actionClient
|
||||
@ -27,7 +27,9 @@ export const getActiveSubscriptionAction = actionClient
|
||||
// Get the current user session for authorization
|
||||
const session = await getSession();
|
||||
if (!session) {
|
||||
console.warn(`unauthorized request to get active subscription for user ${userId}`);
|
||||
console.warn(
|
||||
`unauthorized request to get active subscription for user ${userId}`
|
||||
);
|
||||
return {
|
||||
success: false,
|
||||
error: 'Unauthorized',
|
||||
@ -36,7 +38,9 @@ export const getActiveSubscriptionAction = actionClient
|
||||
|
||||
// Only allow users to check their own status unless they're admins
|
||||
if (session.user.id !== userId && session.user.role !== 'admin') {
|
||||
console.warn(`current user ${session.user.id} is not authorized to get active subscription for user ${userId}`);
|
||||
console.warn(
|
||||
`current user ${session.user.id} is not authorized to get active subscription for user ${userId}`
|
||||
);
|
||||
return {
|
||||
success: false,
|
||||
error: 'Not authorized to do this action',
|
||||
@ -46,7 +50,7 @@ export const getActiveSubscriptionAction = actionClient
|
||||
try {
|
||||
// Find the user's most recent active subscription
|
||||
const subscriptions = await getSubscriptions({
|
||||
userId: session.user.id
|
||||
userId: session.user.id,
|
||||
});
|
||||
// console.log('get user subscriptions:', subscriptions);
|
||||
|
||||
@ -54,8 +58,8 @@ export const getActiveSubscriptionAction = actionClient
|
||||
// Find the most recent active subscription (if any)
|
||||
if (subscriptions && subscriptions.length > 0) {
|
||||
// First try to find an active subscription
|
||||
const activeSubscription = subscriptions.find(sub =>
|
||||
sub.status === 'active' || sub.status === 'trialing'
|
||||
const activeSubscription = subscriptions.find(
|
||||
(sub) => sub.status === 'active' || sub.status === 'trialing'
|
||||
);
|
||||
|
||||
// If found, use it
|
||||
@ -63,7 +67,10 @@ export const getActiveSubscriptionAction = actionClient
|
||||
console.log('find active subscription for userId:', session.user.id);
|
||||
subscriptionData = activeSubscription;
|
||||
} else {
|
||||
console.log('no active subscription found for userId:', session.user.id);
|
||||
console.log(
|
||||
'no active subscription found for userId:',
|
||||
session.user.id
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.log('no subscriptions found for userId:', session.user.id);
|
||||
@ -74,10 +81,10 @@ export const getActiveSubscriptionAction = actionClient
|
||||
data: subscriptionData,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("get user subscription data error:", error);
|
||||
console.error('get user subscription data error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Something went wrong',
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -1,13 +1,13 @@
|
||||
'use server';
|
||||
|
||||
import db from "@/db";
|
||||
import { payment } from "@/db/schema";
|
||||
import { getSession } from "@/lib/server";
|
||||
import { getAllPricePlans, findPlanByPriceId } from "@/lib/price-plan";
|
||||
import { PaymentTypes } from "@/payment/types";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import db from '@/db';
|
||||
import { payment } from '@/db/schema';
|
||||
import { findPlanByPriceId, getAllPricePlans } from '@/lib/price-plan';
|
||||
import { getSession } from '@/lib/server';
|
||||
import { PaymentTypes } from '@/payment/types';
|
||||
import { and, eq } from 'drizzle-orm';
|
||||
import { createSafeActionClient } from 'next-safe-action';
|
||||
import { z } from "zod";
|
||||
import { z } from 'zod';
|
||||
|
||||
// Create a safe action client
|
||||
const actionClient = createSafeActionClient();
|
||||
@ -19,8 +19,8 @@ const schema = z.object({
|
||||
|
||||
/**
|
||||
* Get user lifetime membership status directly from the database
|
||||
*
|
||||
* NOTICE: If you first add lifetime plan and then delete it,
|
||||
*
|
||||
* NOTICE: If you first add lifetime plan and then delete it,
|
||||
* the user with lifetime plan should be considered as a lifetime member as well,
|
||||
* in order to do this, you have to update the logic to check the lifetime status,
|
||||
* for example, just check the planId is `lifetime` or not.
|
||||
@ -33,7 +33,9 @@ export const getLifetimeStatusAction = actionClient
|
||||
// Get the current user session for authorization
|
||||
const session = await getSession();
|
||||
if (!session) {
|
||||
console.warn(`unauthorized request to get lifetime status for user ${userId}`);
|
||||
console.warn(
|
||||
`unauthorized request to get lifetime status for user ${userId}`
|
||||
);
|
||||
return {
|
||||
success: false,
|
||||
error: 'Unauthorized',
|
||||
@ -42,7 +44,9 @@ export const getLifetimeStatusAction = actionClient
|
||||
|
||||
// Only allow users to check their own status unless they're admins
|
||||
if (session.user.id !== userId && session.user.role !== 'admin') {
|
||||
console.warn(`current user ${session.user.id} is not authorized to get lifetime status for user ${userId}`);
|
||||
console.warn(
|
||||
`current user ${session.user.id} is not authorized to get lifetime status for user ${userId}`
|
||||
);
|
||||
return {
|
||||
success: false,
|
||||
error: 'Not authorized to do this action',
|
||||
@ -53,8 +57,8 @@ export const getLifetimeStatusAction = actionClient
|
||||
// Get lifetime plans
|
||||
const plans = getAllPricePlans();
|
||||
const lifetimePlanIds = plans
|
||||
.filter(plan => plan.isLifetime)
|
||||
.map(plan => plan.id);
|
||||
.filter((plan) => plan.isLifetime)
|
||||
.map((plan) => plan.id);
|
||||
|
||||
// Check if there are any lifetime plans defined in the system
|
||||
if (lifetimePlanIds.length === 0) {
|
||||
@ -66,7 +70,11 @@ export const getLifetimeStatusAction = actionClient
|
||||
|
||||
// Query the database for one-time payments with lifetime plans
|
||||
const result = await db
|
||||
.select({ id: payment.id, priceId: payment.priceId, type: payment.type })
|
||||
.select({
|
||||
id: payment.id,
|
||||
priceId: payment.priceId,
|
||||
type: payment.type,
|
||||
})
|
||||
.from(payment)
|
||||
.where(
|
||||
and(
|
||||
@ -77,7 +85,7 @@ export const getLifetimeStatusAction = actionClient
|
||||
);
|
||||
|
||||
// Check if any payment has a lifetime plan
|
||||
const hasLifetimePayment = result.some(paymentRecord => {
|
||||
const hasLifetimePayment = result.some((paymentRecord) => {
|
||||
const plan = findPlanByPriceId(paymentRecord.priceId);
|
||||
return plan && lifetimePlanIds.includes(plan.id);
|
||||
});
|
||||
@ -87,10 +95,10 @@ export const getLifetimeStatusAction = actionClient
|
||||
isLifetimeMember: hasLifetimePayment,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("get user lifetime status error:", error);
|
||||
console.error('get user lifetime status error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Something went wrong',
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -15,12 +15,13 @@ const actionClient = createSafeActionClient();
|
||||
*/
|
||||
// Contact form schema for validation
|
||||
const contactFormSchema = z.object({
|
||||
name: z.string()
|
||||
name: z
|
||||
.string()
|
||||
.min(3, { message: 'Name must be at least 3 characters' })
|
||||
.max(30, { message: 'Name must not exceed 30 characters' }),
|
||||
email: z.string()
|
||||
.email({ message: 'Please enter a valid email address' }),
|
||||
message: z.string()
|
||||
email: z.string().email({ message: 'Please enter a valid email address' }),
|
||||
message: z
|
||||
.string()
|
||||
.min(10, { message: 'Message must be at least 10 characters' })
|
||||
.max(500, { message: 'Message must not exceed 500 characters' }),
|
||||
});
|
||||
|
@ -51,4 +51,4 @@ export const subscribeNewsletterAction = actionClient
|
||||
error: error instanceof Error ? error.message : 'Something went wrong',
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -24,7 +24,7 @@ export const unsubscribeNewsletterAction = actionClient
|
||||
error: 'Unauthorized',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const unsubscribed = await unsubscribe(email);
|
||||
|
||||
@ -46,4 +46,4 @@ export const unsubscribeNewsletterAction = actionClient
|
||||
error: error instanceof Error ? error.message : 'Something went wrong',
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -1,9 +1,9 @@
|
||||
import GoogleAnalytics from "./google-analytics";
|
||||
import { UmamiAnalytics } from "./umami-analytics";
|
||||
import { PlausibleAnalytics } from "./plausible-analytics";
|
||||
import DataFastAnalytics from "./data-fast-analytics";
|
||||
import OpenPanelAnalytics from "./open-panel-analytics";
|
||||
import { SelineAnalytics } from "./seline-analytics";
|
||||
import DataFastAnalytics from './data-fast-analytics';
|
||||
import GoogleAnalytics from './google-analytics';
|
||||
import OpenPanelAnalytics from './open-panel-analytics';
|
||||
import { PlausibleAnalytics } from './plausible-analytics';
|
||||
import { SelineAnalytics } from './seline-analytics';
|
||||
import { UmamiAnalytics } from './umami-analytics';
|
||||
|
||||
/**
|
||||
* Analytics Components all in one
|
||||
@ -12,7 +12,7 @@ import { SelineAnalytics } from "./seline-analytics";
|
||||
* 2. only work if the environment variable for the analytics is set
|
||||
*/
|
||||
export function Analytics() {
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import Script from "next/script";
|
||||
import Script from 'next/script';
|
||||
|
||||
/**
|
||||
* DataFast Analytics
|
||||
@ -8,7 +8,7 @@ import Script from "next/script";
|
||||
* https://datafa.st
|
||||
*/
|
||||
export default function DataFastAnalytics() {
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import { GoogleAnalytics as NextGoogleAnalytics } from "@next/third-parties/google";
|
||||
import { GoogleAnalytics as NextGoogleAnalytics } from '@next/third-parties/google';
|
||||
|
||||
/**
|
||||
* Google Analytics
|
||||
@ -9,7 +9,7 @@ import { GoogleAnalytics as NextGoogleAnalytics } from "@next/third-parties/goog
|
||||
* https://nextjs.org/docs/app/building-your-application/optimizing/third-party-libraries#google-analytics
|
||||
*/
|
||||
export default function GoogleAnalytics() {
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { OpenPanelComponent } from "@openpanel/nextjs";
|
||||
import { OpenPanelComponent } from '@openpanel/nextjs';
|
||||
|
||||
/**
|
||||
* OpenPanel Analytics (https://openpanel.dev)
|
||||
@ -6,7 +6,7 @@ import { OpenPanelComponent } from "@openpanel/nextjs";
|
||||
* https://docs.openpanel.dev/docs/sdks/nextjs#options
|
||||
*/
|
||||
export default function OpenPanelAnalytics() {
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import Script from "next/script";
|
||||
import Script from 'next/script';
|
||||
|
||||
/**
|
||||
* Plausible Analytics
|
||||
@ -8,7 +8,7 @@ import Script from "next/script";
|
||||
* https://plausible.io
|
||||
*/
|
||||
export function PlausibleAnalytics() {
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -23,11 +23,6 @@ export function PlausibleAnalytics() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Script
|
||||
defer
|
||||
type="text/javascript"
|
||||
data-domain={domain}
|
||||
src={script}
|
||||
/>
|
||||
<Script defer type="text/javascript" data-domain={domain} src={script} />
|
||||
);
|
||||
}
|
||||
|
@ -1,16 +1,16 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import Script from "next/script";
|
||||
import Script from 'next/script';
|
||||
|
||||
/**
|
||||
* Seline Analytics
|
||||
*
|
||||
*
|
||||
* https://seline.com
|
||||
* https://seline.com/docs/install-seline
|
||||
* https://seline.com/docs/stripe
|
||||
*/
|
||||
export function SelineAnalytics() {
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import Script from "next/script";
|
||||
import Script from 'next/script';
|
||||
|
||||
/**
|
||||
* Umami Analytics
|
||||
@ -8,7 +8,7 @@ import Script from "next/script";
|
||||
* https://umami.is
|
||||
*/
|
||||
export function UmamiAnalytics() {
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -30,4 +30,4 @@ export function UmamiAnalytics() {
|
||||
src={script}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -14,8 +14,8 @@ import StatsSection from '@/components/blocks/stats/stats';
|
||||
import TestimonialsSection from '@/components/blocks/testimonials/testimonials';
|
||||
import { constructMetadata } from '@/lib/metadata';
|
||||
import { getUrlWithLocale } from '@/lib/urls/urls';
|
||||
import { Metadata } from 'next';
|
||||
import { Locale } from 'next-intl';
|
||||
import type { Metadata } from 'next';
|
||||
import type { Locale } from 'next-intl';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
|
||||
/**
|
||||
|
@ -4,7 +4,7 @@ import { getPage } from '@/lib/page/get-page';
|
||||
import { getUrlWithLocale } from '@/lib/urls/urls';
|
||||
import type { NextPageProps } from '@/types/next-page-props';
|
||||
import type { Metadata } from 'next';
|
||||
import { Locale } from 'next-intl';
|
||||
import type { Locale } from 'next-intl';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
@ -13,7 +13,7 @@ export async function generateMetadata({
|
||||
}: {
|
||||
params: Promise<{ locale: Locale }>;
|
||||
}): Promise<Metadata | undefined> {
|
||||
const {locale} = await params;
|
||||
const { locale } = await params;
|
||||
const page = await getPage('cookie-policy', locale);
|
||||
|
||||
if (!page) {
|
||||
@ -23,12 +23,12 @@ export async function generateMetadata({
|
||||
return {};
|
||||
}
|
||||
|
||||
const t = await getTranslations({locale, namespace: 'Metadata'});
|
||||
const t = await getTranslations({ locale, namespace: 'Metadata' });
|
||||
|
||||
return constructMetadata({
|
||||
title: page.title + ' | ' + t('title'),
|
||||
description: page.description,
|
||||
canonicalUrl: getUrlWithLocale("/cookie", locale),
|
||||
canonicalUrl: getUrlWithLocale('/cookie', locale),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import Container from '@/components/layout/container';
|
||||
import { PropsWithChildren } from 'react';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
|
||||
import '@/styles/mdx.css';
|
||||
|
||||
|
@ -4,7 +4,7 @@ import { getPage } from '@/lib/page/get-page';
|
||||
import { getUrlWithLocale } from '@/lib/urls/urls';
|
||||
import type { NextPageProps } from '@/types/next-page-props';
|
||||
import type { Metadata } from 'next';
|
||||
import { Locale } from 'next-intl';
|
||||
import type { Locale } from 'next-intl';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
@ -13,7 +13,7 @@ export async function generateMetadata({
|
||||
}: {
|
||||
params: Promise<{ locale: Locale }>;
|
||||
}): Promise<Metadata | undefined> {
|
||||
const {locale} = await params;
|
||||
const { locale } = await params;
|
||||
const page = await getPage('privacy-policy', locale);
|
||||
|
||||
if (!page) {
|
||||
@ -23,12 +23,12 @@ export async function generateMetadata({
|
||||
return {};
|
||||
}
|
||||
|
||||
const t = await getTranslations({locale, namespace: 'Metadata'});
|
||||
const t = await getTranslations({ locale, namespace: 'Metadata' });
|
||||
|
||||
return constructMetadata({
|
||||
title: page.title + ' | ' + t('title'),
|
||||
description: page.description,
|
||||
canonicalUrl: getUrlWithLocale("/privacy", locale),
|
||||
canonicalUrl: getUrlWithLocale('/privacy', locale),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,7 @@ import { getPage } from '@/lib/page/get-page';
|
||||
import { getUrlWithLocale } from '@/lib/urls/urls';
|
||||
import type { NextPageProps } from '@/types/next-page-props';
|
||||
import type { Metadata } from 'next';
|
||||
import { Locale } from 'next-intl';
|
||||
import type { Locale } from 'next-intl';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
@ -13,7 +13,7 @@ export async function generateMetadata({
|
||||
}: {
|
||||
params: Promise<{ locale: Locale }>;
|
||||
}): Promise<Metadata | undefined> {
|
||||
const {locale} = await params;
|
||||
const { locale } = await params;
|
||||
const page = await getPage('terms-of-service', locale);
|
||||
|
||||
if (!page) {
|
||||
@ -23,12 +23,12 @@ export async function generateMetadata({
|
||||
return {};
|
||||
}
|
||||
|
||||
const t = await getTranslations({locale, namespace: 'Metadata'});
|
||||
const t = await getTranslations({ locale, namespace: 'Metadata' });
|
||||
|
||||
return constructMetadata({
|
||||
title: page.title + ' | ' + t('title'),
|
||||
description: page.description,
|
||||
canonicalUrl: getUrlWithLocale("/terms", locale),
|
||||
canonicalUrl: getUrlWithLocale('/terms', locale),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -4,8 +4,8 @@ import { websiteConfig } from '@/config/website';
|
||||
import { constructMetadata } from '@/lib/metadata';
|
||||
import { getUrlWithLocale } from '@/lib/urls/urls';
|
||||
import { MailIcon } from 'lucide-react';
|
||||
import { Metadata } from 'next';
|
||||
import { Locale } from 'next-intl';
|
||||
import type { Metadata } from 'next';
|
||||
import type { Locale } from 'next-intl';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
|
||||
export async function generateMetadata({
|
||||
@ -20,7 +20,7 @@ export async function generateMetadata({
|
||||
return constructMetadata({
|
||||
title: pt('title') + ' | ' + t('title'),
|
||||
description: pt('description'),
|
||||
canonicalUrl: getUrlWithLocale("/about", locale),
|
||||
canonicalUrl: getUrlWithLocale('/about', locale),
|
||||
});
|
||||
}
|
||||
|
||||
@ -49,9 +49,7 @@ export default async function AboutPage() {
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<h1 className="text-4xl text-foreground">
|
||||
{t('authorName')}
|
||||
</h1>
|
||||
<h1 className="text-4xl text-foreground">{t('authorName')}</h1>
|
||||
<p className="text-base text-muted-foreground mt-2">
|
||||
{t('authorBio')}
|
||||
</p>
|
||||
@ -67,7 +65,9 @@ export default async function AboutPage() {
|
||||
<div className="flex items-center gap-4">
|
||||
<Button className="rounded-lg cursor-pointer">
|
||||
<MailIcon className="mr-1 size-4" />
|
||||
<a href={`mailto:${websiteConfig.mail.from}`}>{t('talkWithMe')}</a>
|
||||
<a href={`mailto:${websiteConfig.mail.from}`}>
|
||||
{t('talkWithMe')}
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -4,7 +4,7 @@ import { getReleases } from '@/lib/release/get-releases';
|
||||
import { getUrlWithLocale } from '@/lib/urls/urls';
|
||||
import type { NextPageProps } from '@/types/next-page-props';
|
||||
import type { Metadata } from 'next';
|
||||
import { Locale } from 'next-intl';
|
||||
import type { Locale } from 'next-intl';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
@ -15,14 +15,14 @@ export async function generateMetadata({
|
||||
}: {
|
||||
params: Promise<{ locale: Locale }>;
|
||||
}): Promise<Metadata | undefined> {
|
||||
const {locale} = await params;
|
||||
const t = await getTranslations({locale, namespace: 'Metadata'});
|
||||
const pt = await getTranslations({locale, namespace: 'ChangelogPage'});
|
||||
|
||||
const { locale } = await params;
|
||||
const t = await getTranslations({ locale, namespace: 'Metadata' });
|
||||
const pt = await getTranslations({ locale, namespace: 'ChangelogPage' });
|
||||
|
||||
return constructMetadata({
|
||||
title: pt('title') + ' | ' + t('title'),
|
||||
description: pt('description'),
|
||||
canonicalUrl: getUrlWithLocale("/changelog", locale),
|
||||
canonicalUrl: getUrlWithLocale('/changelog', locale),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { ContactFormCard } from '@/components/contact/contact-form-card';
|
||||
import { constructMetadata } from '@/lib/metadata';
|
||||
import { getUrlWithLocale } from '@/lib/urls/urls';
|
||||
import { Metadata } from 'next';
|
||||
import { Locale } from 'next-intl';
|
||||
import type { Metadata } from 'next';
|
||||
import type { Locale } from 'next-intl';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
@ -17,7 +17,7 @@ export async function generateMetadata({
|
||||
return constructMetadata({
|
||||
title: pt('title') + ' | ' + t('title'),
|
||||
description: pt('description'),
|
||||
canonicalUrl: getUrlWithLocale("/contact", locale),
|
||||
canonicalUrl: getUrlWithLocale('/contact', locale),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import Container from '@/components/layout/container';
|
||||
import { PropsWithChildren } from 'react';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
|
||||
export default function PageLayout({ children }: PropsWithChildren) {
|
||||
return (
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { WaitlistFormCard } from '@/components/waitlist/waitlist-form-card';
|
||||
import { constructMetadata } from '@/lib/metadata';
|
||||
import { getUrlWithLocale } from '@/lib/urls/urls';
|
||||
import { Metadata } from 'next';
|
||||
import { Locale } from 'next-intl';
|
||||
import type { Metadata } from 'next';
|
||||
import type { Locale } from 'next-intl';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
|
||||
export async function generateMetadata({
|
||||
@ -10,13 +10,13 @@ export async function generateMetadata({
|
||||
}: {
|
||||
params: Promise<{ locale: Locale }>;
|
||||
}): Promise<Metadata | undefined> {
|
||||
const {locale} = await params;
|
||||
const t = await getTranslations({locale, namespace: 'Metadata'});
|
||||
const pt = await getTranslations({locale, namespace: 'WaitlistPage'});
|
||||
const { locale } = await params;
|
||||
const t = await getTranslations({ locale, namespace: 'Metadata' });
|
||||
const pt = await getTranslations({ locale, namespace: 'WaitlistPage' });
|
||||
return constructMetadata({
|
||||
title: pt('title') + ' | ' + t('title'),
|
||||
description: pt('description'),
|
||||
canonicalUrl: getUrlWithLocale("/waitlist", locale),
|
||||
canonicalUrl: getUrlWithLocale('/waitlist', locale),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { constructMetadata } from '@/lib/metadata';
|
||||
import { getUrlWithLocale } from '@/lib/urls/urls';
|
||||
import { Metadata } from 'next';
|
||||
import { Locale } from 'next-intl';
|
||||
import type { Metadata } from 'next';
|
||||
import type { Locale } from 'next-intl';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
|
||||
export async function generateMetadata({
|
||||
@ -17,7 +17,7 @@ export async function generateMetadata({
|
||||
return constructMetadata({
|
||||
title: pt('title') + ' | ' + t('title'),
|
||||
description: pt('description'),
|
||||
canonicalUrl: getUrlWithLocale("/ai/audio", locale),
|
||||
canonicalUrl: getUrlWithLocale('/ai/audio', locale),
|
||||
});
|
||||
}
|
||||
|
||||
@ -44,9 +44,7 @@ export default async function AIAudioPage() {
|
||||
</Avatar>
|
||||
|
||||
<div>
|
||||
<h1 className="text-4xl text-foreground">
|
||||
{t('content')}
|
||||
</h1>
|
||||
<h1 className="text-4xl text-foreground">{t('content')}</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { constructMetadata } from '@/lib/metadata';
|
||||
import { getUrlWithLocale } from '@/lib/urls/urls';
|
||||
import { Metadata } from 'next';
|
||||
import { Locale } from 'next-intl';
|
||||
import type { Metadata } from 'next';
|
||||
import type { Locale } from 'next-intl';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
|
||||
export async function generateMetadata({
|
||||
@ -17,7 +17,7 @@ export async function generateMetadata({
|
||||
return constructMetadata({
|
||||
title: pt('title') + ' | ' + t('title'),
|
||||
description: pt('description'),
|
||||
canonicalUrl: getUrlWithLocale("/ai/image", locale),
|
||||
canonicalUrl: getUrlWithLocale('/ai/image', locale),
|
||||
});
|
||||
}
|
||||
|
||||
@ -44,9 +44,7 @@ export default async function AIImagePage() {
|
||||
</Avatar>
|
||||
|
||||
<div>
|
||||
<h1 className="text-4xl text-foreground">
|
||||
{t('content')}
|
||||
</h1>
|
||||
<h1 className="text-4xl text-foreground">{t('content')}</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import Container from '@/components/layout/container';
|
||||
import { PropsWithChildren } from 'react';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
|
||||
export default function PageLayout({ children }: PropsWithChildren) {
|
||||
return (
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { constructMetadata } from '@/lib/metadata';
|
||||
import { getUrlWithLocale } from '@/lib/urls/urls';
|
||||
import { Metadata } from 'next';
|
||||
import { Locale } from 'next-intl';
|
||||
import type { Metadata } from 'next';
|
||||
import type { Locale } from 'next-intl';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
|
||||
export async function generateMetadata({
|
||||
@ -17,7 +17,7 @@ export async function generateMetadata({
|
||||
return constructMetadata({
|
||||
title: pt('title') + ' | ' + t('title'),
|
||||
description: pt('description'),
|
||||
canonicalUrl: getUrlWithLocale("/ai/text", locale),
|
||||
canonicalUrl: getUrlWithLocale('/ai/text', locale),
|
||||
});
|
||||
}
|
||||
|
||||
@ -44,9 +44,7 @@ export default async function AITextPage() {
|
||||
</Avatar>
|
||||
|
||||
<div>
|
||||
<h1 className="text-4xl text-foreground">
|
||||
{t('content')}
|
||||
</h1>
|
||||
<h1 className="text-4xl text-foreground">{t('content')}</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { constructMetadata } from '@/lib/metadata';
|
||||
import { getUrlWithLocale } from '@/lib/urls/urls';
|
||||
import { Metadata } from 'next';
|
||||
import { Locale } from 'next-intl';
|
||||
import type { Metadata } from 'next';
|
||||
import type { Locale } from 'next-intl';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
|
||||
export async function generateMetadata({
|
||||
@ -17,7 +17,7 @@ export async function generateMetadata({
|
||||
return constructMetadata({
|
||||
title: pt('title') + ' | ' + t('title'),
|
||||
description: pt('description'),
|
||||
canonicalUrl: getUrlWithLocale("/ai/video", locale),
|
||||
canonicalUrl: getUrlWithLocale('/ai/video', locale),
|
||||
});
|
||||
}
|
||||
|
||||
@ -44,9 +44,7 @@ export default async function AIVideoPage() {
|
||||
</Avatar>
|
||||
|
||||
<div>
|
||||
<h1 className="text-4xl text-foreground">
|
||||
{t('content')}
|
||||
</h1>
|
||||
<h1 className="text-4xl text-foreground">{t('content')}</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { categories } from '@/components/nsui/blocks';
|
||||
import BlocksNav from '@/components/nsui/blocks-nav';
|
||||
import { PropsWithChildren } from 'react';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
|
||||
/**
|
||||
* The locale inconsistency issue has been fixed in the BlocksNav component
|
||||
@ -10,9 +10,7 @@ export default function BlockCategoryLayout({ children }: PropsWithChildren) {
|
||||
<>
|
||||
<BlocksNav categories={categories} />
|
||||
|
||||
<main>
|
||||
{children}
|
||||
</main>
|
||||
<main>{children}</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -2,8 +2,8 @@ import BlockPreview from '@/components/nsui/block-preview';
|
||||
import { blocks, categories } from '@/components/nsui/blocks';
|
||||
import { constructMetadata } from '@/lib/metadata';
|
||||
import { getUrlWithLocale } from '@/lib/urls/urls';
|
||||
import { Metadata } from 'next';
|
||||
import { Locale } from 'next-intl';
|
||||
import type { Metadata } from 'next';
|
||||
import type { Locale } from 'next-intl';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
@ -26,7 +26,7 @@ export async function generateMetadata({
|
||||
return constructMetadata({
|
||||
title: category + ' | ' + t('title'),
|
||||
description: t('description'),
|
||||
canonicalUrl: getUrlWithLocale("/blocks/${category}", locale),
|
||||
canonicalUrl: getUrlWithLocale('/blocks/${category}', locale),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -4,10 +4,10 @@ import CustomPagination from '@/components/shared/pagination';
|
||||
import { websiteConfig } from '@/config/website';
|
||||
import { constructMetadata } from '@/lib/metadata';
|
||||
import { getUrlWithLocale } from '@/lib/urls/urls';
|
||||
import { NextPageProps } from '@/types/next-page-props';
|
||||
import type { NextPageProps } from '@/types/next-page-props';
|
||||
import { allCategories, allPosts } from 'content-collections';
|
||||
import type { Metadata } from 'next';
|
||||
import { Locale } from 'next-intl';
|
||||
import type { Locale } from 'next-intl';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
|
||||
export async function generateMetadata({
|
||||
@ -29,12 +29,12 @@ export async function generateMetadata({
|
||||
return {};
|
||||
}
|
||||
|
||||
const t = await getTranslations({locale, namespace: 'Metadata'});
|
||||
const t = await getTranslations({ locale, namespace: 'Metadata' });
|
||||
|
||||
return constructMetadata({
|
||||
title: `${category.name} | ${t('title')}`,
|
||||
description: category.description,
|
||||
canonicalUrl: getUrlWithLocale("/blog/category/${slug}", locale),
|
||||
canonicalUrl: getUrlWithLocale('/blog/category/${slug}', locale),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { BlogCategoryFilter } from '@/components/blog/blog-category-filter';
|
||||
import Container from '@/components/layout/container';
|
||||
import { NextPageProps } from '@/types/next-page-props';
|
||||
import type { NextPageProps } from '@/types/next-page-props';
|
||||
import { allCategories } from 'content-collections';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import { PropsWithChildren } from 'react';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
|
||||
interface BlogListLayoutProps extends PropsWithChildren, NextPageProps { }
|
||||
interface BlogListLayoutProps extends PropsWithChildren, NextPageProps {}
|
||||
|
||||
export default async function BlogListLayout({
|
||||
children,
|
||||
|
@ -4,10 +4,10 @@ import CustomPagination from '@/components/shared/pagination';
|
||||
import { websiteConfig } from '@/config/website';
|
||||
import { constructMetadata } from '@/lib/metadata';
|
||||
import { getUrlWithLocale } from '@/lib/urls/urls';
|
||||
import { NextPageProps } from '@/types/next-page-props';
|
||||
import type { NextPageProps } from '@/types/next-page-props';
|
||||
import { allPosts } from 'content-collections';
|
||||
import { Metadata } from 'next';
|
||||
import { Locale } from 'next-intl';
|
||||
import type { Metadata } from 'next';
|
||||
import type { Locale } from 'next-intl';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
|
||||
export async function generateMetadata({
|
||||
@ -15,13 +15,13 @@ export async function generateMetadata({
|
||||
}: {
|
||||
params: Promise<{ locale: Locale }>;
|
||||
}): Promise<Metadata | undefined> {
|
||||
const {locale} = await params;
|
||||
const t = await getTranslations({locale, namespace: 'Metadata'});
|
||||
const pt = await getTranslations({locale, namespace: 'BlogPage'});
|
||||
const { locale } = await params;
|
||||
const t = await getTranslations({ locale, namespace: 'Metadata' });
|
||||
const pt = await getTranslations({ locale, namespace: 'BlogPage' });
|
||||
return constructMetadata({
|
||||
title: pt('title') + ' | ' + t('title'),
|
||||
description: pt('description'),
|
||||
canonicalUrl: getUrlWithLocale("/blog", locale),
|
||||
canonicalUrl: getUrlWithLocale('/blog', locale),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import Container from '@/components/layout/container';
|
||||
import { PropsWithChildren } from 'react';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
|
||||
export default function BlogPostLayout({ children }: PropsWithChildren) {
|
||||
return (
|
||||
|
@ -6,17 +6,17 @@ import { CustomMDXContent } from '@/components/shared/custom-mdx-content';
|
||||
import { websiteConfig } from '@/config/website';
|
||||
import { LocaleLink } from '@/i18n/navigation';
|
||||
import { getTableOfContents } from '@/lib/blog/toc';
|
||||
import { formatDate } from '@/lib/formatter';
|
||||
import { constructMetadata } from '@/lib/metadata';
|
||||
import { getUrlWithLocale } from '@/lib/urls/urls';
|
||||
import type { NextPageProps } from '@/types/next-page-props';
|
||||
import { allPosts, Post } from 'content-collections';
|
||||
import { type Post, allPosts } from 'content-collections';
|
||||
import { CalendarIcon, ClockIcon, FileTextIcon } from 'lucide-react';
|
||||
import type { Metadata } from 'next';
|
||||
import { Locale } from 'next-intl';
|
||||
import type { Locale } from 'next-intl';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import Image from 'next/image';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { formatDate } from '@/lib/formatter';
|
||||
|
||||
import '@/styles/mdx.css';
|
||||
|
||||
@ -85,7 +85,7 @@ export async function generateMetadata({
|
||||
|
||||
const post = await getBlogPostFromParams({
|
||||
params: Promise.resolve({ slug, locale }),
|
||||
searchParams: Promise.resolve({})
|
||||
searchParams: Promise.resolve({}),
|
||||
});
|
||||
if (!post) {
|
||||
console.warn(
|
||||
@ -194,9 +194,7 @@ export default async function BlogPostPage(props: NextPageProps) {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<span className="line-clamp-1">
|
||||
{post.author?.name}
|
||||
</span>
|
||||
<span className="line-clamp-1">{post.author?.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Footer } from '@/components/layout/footer';
|
||||
import { Navbar } from '@/components/layout/navbar';
|
||||
import { ReactNode } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export default function MarketingLayout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { constructMetadata } from '@/lib/metadata';
|
||||
import { getUrlWithLocale } from '@/lib/urls/urls';
|
||||
import { Metadata } from 'next';
|
||||
import { Locale } from 'next-intl';
|
||||
import type { Metadata } from 'next';
|
||||
import type { Locale } from 'next-intl';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
|
||||
export async function generateMetadata({
|
||||
|
@ -4,11 +4,11 @@ import { DataTable } from '@/components/dashboard/data-table';
|
||||
import { SectionCards } from '@/components/dashboard/section-cards';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
import data from "./data.json";
|
||||
import data from './data.json';
|
||||
|
||||
/**
|
||||
* Admin users page
|
||||
*
|
||||
*
|
||||
* NOTICE: This is a demo page for the admin, no real data is used,
|
||||
* we will show real data in the future
|
||||
*/
|
||||
|
@ -4,11 +4,11 @@ import { DataTable } from '@/components/dashboard/data-table';
|
||||
import { SectionCards } from '@/components/dashboard/section-cards';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
import data from "./data.json";
|
||||
import data from './data.json';
|
||||
|
||||
/**
|
||||
* Dashboard page
|
||||
*
|
||||
*
|
||||
* NOTICE: This is a demo page for the dashboard, no real data is used,
|
||||
* we will show real data in the future
|
||||
*/
|
||||
|
@ -1,9 +1,6 @@
|
||||
import { DashboardSidebar } from '@/components/dashboard/dashboard-sidebar';
|
||||
import {
|
||||
SidebarInset,
|
||||
SidebarProvider
|
||||
} from '@/components/ui/sidebar';
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
|
||||
/**
|
||||
* inspired by dashboard-01
|
||||
@ -14,16 +11,14 @@ export default function DashboardLayout({ children }: PropsWithChildren) {
|
||||
<SidebarProvider
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": "calc(var(--spacing) * 72)",
|
||||
"--header-height": "calc(var(--spacing) * 12)",
|
||||
'--sidebar-width': 'calc(var(--spacing) * 72)',
|
||||
'--header-height': 'calc(var(--spacing) * 12)',
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<DashboardSidebar variant="inset" />
|
||||
|
||||
<SidebarInset>
|
||||
{children}
|
||||
</SidebarInset>
|
||||
<SidebarInset>{children}</SidebarInset>
|
||||
</SidebarProvider>
|
||||
);
|
||||
}
|
||||
|
@ -5,9 +5,7 @@ interface BillingLayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default async function BillingLayout({
|
||||
children,
|
||||
}: BillingLayoutProps) {
|
||||
export default async function BillingLayout({ children }: BillingLayoutProps) {
|
||||
const t = await getTranslations('Dashboard.settings');
|
||||
|
||||
const breadcrumbs = [
|
||||
|
@ -1,7 +1,5 @@
|
||||
import BillingCard from '@/components/settings/billing/billing-card';
|
||||
|
||||
export default function BillingPage() {
|
||||
return (
|
||||
<BillingCard />
|
||||
);
|
||||
return <BillingCard />;
|
||||
}
|
||||
|
@ -5,9 +5,7 @@ interface ProfileLayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default async function ProfileLayout({
|
||||
children,
|
||||
}: ProfileLayoutProps) {
|
||||
export default async function ProfileLayout({ children }: ProfileLayoutProps) {
|
||||
const t = await getTranslations('Dashboard.settings');
|
||||
|
||||
const breadcrumbs = [
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { ErrorCard } from '@/components/auth/error-card';
|
||||
import { constructMetadata } from '@/lib/metadata';
|
||||
import { getUrlWithLocale } from '@/lib/urls/urls';
|
||||
import { Metadata } from 'next';
|
||||
import { Locale } from 'next-intl';
|
||||
import type { Metadata } from 'next';
|
||||
import type { Locale } from 'next-intl';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
|
||||
export async function generateMetadata({
|
||||
@ -10,10 +10,10 @@ export async function generateMetadata({
|
||||
}: {
|
||||
params: Promise<{ locale: Locale }>;
|
||||
}): Promise<Metadata | undefined> {
|
||||
const {locale} = await params;
|
||||
const t = await getTranslations({locale, namespace: 'Metadata'});
|
||||
const pt = await getTranslations({locale, namespace: 'AuthPage.error'});
|
||||
|
||||
const { locale } = await params;
|
||||
const t = await getTranslations({ locale, namespace: 'Metadata' });
|
||||
const pt = await getTranslations({ locale, namespace: 'AuthPage.error' });
|
||||
|
||||
return constructMetadata({
|
||||
title: pt('title') + ' | ' + t('title'),
|
||||
description: t('description'),
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { ForgotPasswordForm } from '@/components/auth/forgot-password-form';
|
||||
import { constructMetadata } from '@/lib/metadata';
|
||||
import { getUrlWithLocale } from '@/lib/urls/urls';
|
||||
import { Metadata } from 'next';
|
||||
import { Locale } from 'next-intl';
|
||||
import type { Metadata } from 'next';
|
||||
import type { Locale } from 'next-intl';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
|
||||
export async function generateMetadata({
|
||||
@ -10,14 +10,17 @@ export async function generateMetadata({
|
||||
}: {
|
||||
params: Promise<{ locale: Locale }>;
|
||||
}): Promise<Metadata | undefined> {
|
||||
const {locale} = await params;
|
||||
const t = await getTranslations({locale, namespace: 'Metadata'});
|
||||
const pt = await getTranslations({locale, namespace: 'AuthPage.forgotPassword'});
|
||||
|
||||
const { locale } = await params;
|
||||
const t = await getTranslations({ locale, namespace: 'Metadata' });
|
||||
const pt = await getTranslations({
|
||||
locale,
|
||||
namespace: 'AuthPage.forgotPassword',
|
||||
});
|
||||
|
||||
return constructMetadata({
|
||||
title: pt('title') + ' | ' + t('title'),
|
||||
description: t('description'),
|
||||
canonicalUrl: getUrlWithLocale("/auth/forgot-password", locale),
|
||||
canonicalUrl: getUrlWithLocale('/auth/forgot-password', locale),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -3,8 +3,8 @@ import { LocaleLink } from '@/i18n/navigation';
|
||||
import { constructMetadata } from '@/lib/metadata';
|
||||
import { getUrlWithLocale } from '@/lib/urls/urls';
|
||||
import { Routes } from '@/routes';
|
||||
import { Metadata } from 'next';
|
||||
import { Locale } from 'next-intl';
|
||||
import type { Metadata } from 'next';
|
||||
import type { Locale } from 'next-intl';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
|
||||
export async function generateMetadata({
|
||||
@ -12,14 +12,14 @@ export async function generateMetadata({
|
||||
}: {
|
||||
params: Promise<{ locale: Locale }>;
|
||||
}): Promise<Metadata | undefined> {
|
||||
const {locale} = await params;
|
||||
const t = await getTranslations({locale, namespace: 'Metadata'});
|
||||
const pt = await getTranslations({locale, namespace: 'AuthPage.login'});
|
||||
|
||||
const { locale } = await params;
|
||||
const t = await getTranslations({ locale, namespace: 'Metadata' });
|
||||
const pt = await getTranslations({ locale, namespace: 'AuthPage.login' });
|
||||
|
||||
return constructMetadata({
|
||||
title: pt('title') + ' | ' + t('title'),
|
||||
description: t('description'),
|
||||
canonicalUrl: getUrlWithLocale("/auth/login", locale),
|
||||
canonicalUrl: getUrlWithLocale('/auth/login', locale),
|
||||
});
|
||||
}
|
||||
|
||||
@ -47,4 +47,4 @@ export default async function LoginPage() {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
@ -3,8 +3,8 @@ import { LocaleLink } from '@/i18n/navigation';
|
||||
import { constructMetadata } from '@/lib/metadata';
|
||||
import { getUrlWithLocale } from '@/lib/urls/urls';
|
||||
import { Routes } from '@/routes';
|
||||
import { Metadata } from 'next';
|
||||
import { Locale } from 'next-intl';
|
||||
import type { Metadata } from 'next';
|
||||
import type { Locale } from 'next-intl';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
|
||||
export async function generateMetadata({
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { ResetPasswordForm } from '@/components/auth/reset-password-form';
|
||||
import { constructMetadata } from '@/lib/metadata';
|
||||
import { getUrlWithLocale } from '@/lib/urls/urls';
|
||||
import { Metadata } from 'next';
|
||||
import { Locale } from 'next-intl';
|
||||
import type { Metadata } from 'next';
|
||||
import type { Locale } from 'next-intl';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
|
||||
export async function generateMetadata({
|
||||
@ -10,14 +10,17 @@ export async function generateMetadata({
|
||||
}: {
|
||||
params: Promise<{ locale: Locale }>;
|
||||
}): Promise<Metadata | undefined> {
|
||||
const {locale} = await params;
|
||||
const t = await getTranslations({locale, namespace: 'Metadata'});
|
||||
const pt = await getTranslations({locale, namespace: 'AuthPage.resetPassword'});
|
||||
|
||||
const { locale } = await params;
|
||||
const t = await getTranslations({ locale, namespace: 'Metadata' });
|
||||
const pt = await getTranslations({
|
||||
locale,
|
||||
namespace: 'AuthPage.resetPassword',
|
||||
});
|
||||
|
||||
return constructMetadata({
|
||||
title: pt('title') + ' | ' + t('title'),
|
||||
description: t('description'),
|
||||
canonicalUrl: getUrlWithLocale("/auth/reset-password", locale),
|
||||
canonicalUrl: getUrlWithLocale('/auth/reset-password', locale),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,31 +1,38 @@
|
||||
import * as Preview from '@/components/docs';
|
||||
import { CustomMDXContent } from '@/components/shared/custom-mdx-content';
|
||||
import { HoverCard, HoverCardContent, HoverCardTrigger, } from '@/components/ui/hover-card';
|
||||
import {
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
} from '@/components/ui/hover-card';
|
||||
import { LOCALES } from '@/i18n/routing';
|
||||
import { source } from '@/lib/docs/source';
|
||||
import Link from 'fumadocs-core/link';
|
||||
import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/page';
|
||||
import {
|
||||
DocsBody,
|
||||
DocsDescription,
|
||||
DocsPage,
|
||||
DocsTitle,
|
||||
} from 'fumadocs-ui/page';
|
||||
import type { Metadata } from 'next';
|
||||
import { Locale } from 'next-intl';
|
||||
import type { Locale } from 'next-intl';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { ReactNode } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export function generateStaticParams() {
|
||||
const locales = LOCALES;
|
||||
const slugParams = source.generateParams();
|
||||
const params = locales.flatMap(locale =>
|
||||
slugParams.map(param => ({
|
||||
const params = locales.flatMap((locale) =>
|
||||
slugParams.map((param) => ({
|
||||
locale,
|
||||
slug: param.slug
|
||||
slug: param.slug,
|
||||
}))
|
||||
);
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: DocPageProps) {
|
||||
export async function generateMetadata({ params }: DocPageProps) {
|
||||
const { slug, locale } = await params;
|
||||
const language = locale as string;
|
||||
const page = source.getPage(slug, language);
|
||||
@ -54,19 +61,17 @@ export const revalidate = false;
|
||||
interface DocPageProps {
|
||||
params: Promise<{
|
||||
slug?: string[];
|
||||
locale: Locale
|
||||
locale: Locale;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Doc Page
|
||||
*
|
||||
*
|
||||
* ref:
|
||||
* https://github.com/fuma-nama/fumadocs/blob/dev/apps/docs/app/docs/%5B...slug%5D/page.tsx
|
||||
*/
|
||||
export default async function DocPage({
|
||||
params,
|
||||
}: DocPageProps) {
|
||||
export default async function DocPage({ params }: DocPageProps) {
|
||||
const { slug, locale } = await params;
|
||||
const language = locale as string;
|
||||
const page = source.getPage(slug, language);
|
||||
@ -79,18 +84,15 @@ export default async function DocPage({
|
||||
const preview = page.data.preview;
|
||||
|
||||
return (
|
||||
<DocsPage toc={page.data.toc}
|
||||
<DocsPage
|
||||
toc={page.data.toc}
|
||||
full={page.data.full}
|
||||
tableOfContent={{
|
||||
style: "clerk",
|
||||
style: 'clerk',
|
||||
}}
|
||||
>
|
||||
<DocsTitle>
|
||||
{page.data.title}
|
||||
</DocsTitle>
|
||||
<DocsDescription>
|
||||
{page.data.description}
|
||||
</DocsDescription>
|
||||
<DocsTitle>{page.data.title}</DocsTitle>
|
||||
<DocsDescription>{page.data.description}</DocsDescription>
|
||||
<DocsBody>
|
||||
{/* Preview Rendered Component */}
|
||||
{preview ? <PreviewRenderer preview={preview} /> : null}
|
||||
@ -99,7 +101,7 @@ export default async function DocPage({
|
||||
<CustomMDXContent
|
||||
code={page.data.body}
|
||||
customComponents={{
|
||||
a: ({ href, ...props }: { href?: string;[key: string]: any }) => {
|
||||
a: ({ href, ...props }: { href?: string; [key: string]: any }) => {
|
||||
const found = source.getPageByHref(href ?? '', {
|
||||
dir: page.file.dirname,
|
||||
});
|
||||
|
@ -5,11 +5,11 @@ import { websiteConfig } from '@/config/website';
|
||||
import { docsI18nConfig } from '@/lib/docs/i18n';
|
||||
import { source } from '@/lib/docs/source';
|
||||
import { getUrlWithLocale } from '@/lib/urls/urls';
|
||||
import { I18nProvider, Translations } from 'fumadocs-ui/i18n';
|
||||
import { I18nProvider, type Translations } from 'fumadocs-ui/i18n';
|
||||
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
|
||||
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
|
||||
import { BookIcon, HomeIcon } from 'lucide-react';
|
||||
import { Locale } from 'next-intl';
|
||||
import type { Locale } from 'next-intl';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
@ -17,10 +17,12 @@ import '@/styles/mdx.css';
|
||||
|
||||
// available languages that will be displayed on UI
|
||||
// make sure `locale` is consistent with your i18n config
|
||||
const locales = Object.entries(websiteConfig.i18n.locales).map(([locale, data]) => ({
|
||||
name: data.name,
|
||||
locale,
|
||||
}));
|
||||
const locales = Object.entries(websiteConfig.i18n.locales).map(
|
||||
([locale, data]) => ({
|
||||
name: data.name,
|
||||
locale,
|
||||
})
|
||||
);
|
||||
|
||||
interface DocsLayoutProps {
|
||||
children: ReactNode;
|
||||
@ -31,17 +33,20 @@ interface DocsLayoutProps {
|
||||
* 1. Configure navigation
|
||||
* https://fumadocs.vercel.app/docs/ui/navigation/links
|
||||
* https://fumadocs.vercel.app/docs/ui/navigation/sidebar
|
||||
*
|
||||
*
|
||||
* ref:
|
||||
* https://github.com/fuma-nama/fumadocs/blob/dev/apps/docs/app/layout.config.tsx
|
||||
*
|
||||
*
|
||||
* 2. Organizing Pages
|
||||
* https://fumadocs.vercel.app/docs/ui/page-conventions
|
||||
*
|
||||
*
|
||||
* ref:
|
||||
* https://github.com/fuma-nama/fumadocs/blob/dev/apps/docs/content/docs/ui/meta.json
|
||||
*/
|
||||
export default async function DocsRootLayout({ children, params }: DocsLayoutProps) {
|
||||
export default async function DocsRootLayout({
|
||||
children,
|
||||
params,
|
||||
}: DocsLayoutProps) {
|
||||
const { locale } = await params;
|
||||
const t = await getTranslations({ locale, namespace: 'DocsPage' });
|
||||
|
||||
@ -80,29 +85,25 @@ export default async function DocsRootLayout({ children, params }: DocsLayoutPro
|
||||
},
|
||||
...(websiteConfig.metadata.social?.twitter
|
||||
? [
|
||||
{
|
||||
type: "icon" as const,
|
||||
icon: <XTwitterIcon />,
|
||||
text: "X",
|
||||
url: websiteConfig.metadata.social.twitter,
|
||||
secondary: true,
|
||||
}
|
||||
]
|
||||
: [])
|
||||
{
|
||||
type: 'icon' as const,
|
||||
icon: <XTwitterIcon />,
|
||||
text: 'X',
|
||||
url: websiteConfig.metadata.social.twitter,
|
||||
secondary: true,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
themeSwitch: {
|
||||
enabled: true,
|
||||
mode: 'light-dark-system',
|
||||
component: <ModeSwitcher />
|
||||
component: <ModeSwitcher />,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<I18nProvider
|
||||
locales={locales}
|
||||
locale={locale}
|
||||
translations={translations}
|
||||
>
|
||||
<I18nProvider locales={locales} locale={locale} translations={translations}>
|
||||
<DocsLayout tree={source.pageTree[locale]} {...docsOptions}>
|
||||
{children}
|
||||
</DocsLayout>
|
||||
|
@ -1,15 +1,20 @@
|
||||
import { fontBricolageGrotesque, fontNotoSans, fontNotoSansMono, fontNotoSerif } from '@/assets/fonts';
|
||||
import {
|
||||
fontBricolageGrotesque,
|
||||
fontNotoSans,
|
||||
fontNotoSansMono,
|
||||
fontNotoSerif,
|
||||
} from '@/assets/fonts';
|
||||
import { routing } from '@/i18n/routing';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { hasLocale, Locale, NextIntlClientProvider } from 'next-intl';
|
||||
import { type Locale, NextIntlClientProvider, hasLocale } from 'next-intl';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { ReactNode } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { Toaster } from 'sonner';
|
||||
import { Providers } from './providers';
|
||||
|
||||
import '@/styles/globals.css';
|
||||
import { TailwindIndicator } from '@/components/layout/tailwind-indicator';
|
||||
import { Analytics } from '@/analytics/analytics';
|
||||
import { TailwindIndicator } from '@/components/layout/tailwind-indicator';
|
||||
|
||||
interface LocaleLayoutProps {
|
||||
children: ReactNode;
|
||||
|
@ -6,13 +6,13 @@ import { TooltipProvider } from '@/components/ui/tooltip';
|
||||
import { websiteConfig } from '@/config/website';
|
||||
import { RootProvider } from 'fumadocs-ui/provider';
|
||||
import { ThemeProvider, useTheme } from 'next-themes';
|
||||
import { PropsWithChildren } from 'react';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
|
||||
/**
|
||||
* Providers
|
||||
*
|
||||
*
|
||||
* This component is used to wrap the app in the providers.
|
||||
*
|
||||
*
|
||||
* - ThemeProvider: Provides the theme to the app.
|
||||
* - ActiveThemeProvider: Provides the active theme to the app.
|
||||
* - RootProvider: Provides the root provider for Fumadocs UI.
|
||||
@ -21,8 +21,8 @@ import { PropsWithChildren } from 'react';
|
||||
*/
|
||||
export function Providers({ children }: PropsWithChildren) {
|
||||
const theme = useTheme();
|
||||
const defaultMode = websiteConfig.metadata.mode?.defaultMode ?? "system";
|
||||
|
||||
const defaultMode = websiteConfig.metadata.mode?.defaultMode ?? 'system';
|
||||
|
||||
return (
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
@ -33,9 +33,7 @@ export function Providers({ children }: PropsWithChildren) {
|
||||
<ActiveThemeProvider>
|
||||
<RootProvider theme={theme}>
|
||||
<TooltipProvider>
|
||||
<PaymentProvider>
|
||||
{children}
|
||||
</PaymentProvider>
|
||||
<PaymentProvider>{children}</PaymentProvider>
|
||||
</TooltipProvider>
|
||||
</RootProvider>
|
||||
</ActiveThemeProvider>
|
||||
|
@ -1,14 +1,14 @@
|
||||
import { docsI18nConfig } from '@/lib/docs/i18n';
|
||||
import { source } from '@/lib/docs/source';
|
||||
import { createTokenizer } from '@orama/tokenizers/mandarin';
|
||||
import { createI18nSearchAPI } from 'fumadocs-core/search/server';
|
||||
import { docsI18nConfig } from '@/lib/docs/i18n';
|
||||
|
||||
/**
|
||||
* Fumadocs i18n search configuration
|
||||
*
|
||||
*
|
||||
* 1. For internationalization, use createI18nSearchAPI:
|
||||
* https://fumadocs.vercel.app/docs/headless/search/orama#internationalization
|
||||
*
|
||||
*
|
||||
* 2. For special languages like Chinese, configure custom tokenizers:
|
||||
* https://fumadocs.vercel.app/docs/headless/search/orama#special-languages
|
||||
* https://docs.orama.com/open-source/supported-languages/using-chinese-with-orama
|
||||
@ -26,7 +26,7 @@ const searchAPI = createI18nSearchAPI('advanced', {
|
||||
id: page.url,
|
||||
url: page.url,
|
||||
locale: language,
|
||||
})),
|
||||
}))
|
||||
),
|
||||
|
||||
// Configure special language tokenizers and search options
|
||||
@ -73,7 +73,10 @@ export const GET = async (request: Request) => {
|
||||
console.log('search, referer pathname:', refererUrl.pathname);
|
||||
const refererPathParts = refererUrl.pathname.split('/').filter(Boolean);
|
||||
console.log('search, referer path parts:', refererPathParts);
|
||||
if (refererPathParts.length > 0 && docsI18nConfig.languages.includes(refererPathParts[0])) {
|
||||
if (
|
||||
refererPathParts.length > 0 &&
|
||||
docsI18nConfig.languages.includes(refererPathParts[0])
|
||||
) {
|
||||
locale = refererPathParts[0];
|
||||
console.log(`search, detected locale from referer: ${locale}`);
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
|
||||
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
||||
import { StorageError } from '@/storage/types';
|
||||
import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3';
|
||||
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
||||
import { type NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
@ -68,10 +68,7 @@ export async function POST(request: NextRequest) {
|
||||
console.error('Error getting file URL:', error);
|
||||
|
||||
if (error instanceof StorageError) {
|
||||
return NextResponse.json(
|
||||
{ error: error.message },
|
||||
{ status: 500 }
|
||||
);
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
@ -79,4 +76,4 @@ export async function POST(request: NextRequest) {
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getPresignedUploadUrl } from '@/storage';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { getPresignedUploadUrl } from '@/storage';
|
||||
import { StorageError } from '@/storage/types';
|
||||
import { type NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
@ -47,10 +47,7 @@ export async function POST(request: NextRequest) {
|
||||
console.error('Error generating pre-signed URL:', error);
|
||||
|
||||
if (error instanceof StorageError) {
|
||||
return NextResponse.json(
|
||||
{ error: error.message },
|
||||
{ status: 500 }
|
||||
);
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
@ -58,4 +55,4 @@ export async function POST(request: NextRequest) {
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { uploadFile } from '@/storage';
|
||||
import { StorageError } from '@/storage/types';
|
||||
import { type NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
@ -9,10 +9,7 @@ export async function POST(request: NextRequest) {
|
||||
const folder = formData.get('folder') as string | null;
|
||||
|
||||
if (!file) {
|
||||
return NextResponse.json(
|
||||
{ error: 'No file provided' },
|
||||
{ status: 400 }
|
||||
);
|
||||
return NextResponse.json({ error: 'No file provided' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Validate file size (max 10MB)
|
||||
@ -49,10 +46,7 @@ export async function POST(request: NextRequest) {
|
||||
console.error('Error uploading file:', error);
|
||||
|
||||
if (error instanceof StorageError) {
|
||||
return NextResponse.json(
|
||||
{ error: error.message },
|
||||
{ status: 500 }
|
||||
);
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
@ -69,4 +63,4 @@ export const config = {
|
||||
sizeLimit: '10mb',
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { handleWebhookEvent } from '@/payment';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { type NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
/**
|
||||
* Stripe webhook handler
|
||||
* This endpoint receives webhook events from Stripe and processes them
|
||||
*
|
||||
*
|
||||
* @param req The incoming request
|
||||
* @returns NextResponse
|
||||
*/
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ReactNode } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
|
@ -1,21 +1,21 @@
|
||||
import { defaultMessages } from '@/i18n/messages';
|
||||
import { type MetadataRoute } from 'next';
|
||||
import type { MetadataRoute } from 'next';
|
||||
|
||||
/**
|
||||
* Generates the Web App Manifest for the application
|
||||
*
|
||||
*
|
||||
* generated file name: manifest.webmanifest
|
||||
*
|
||||
* ref: https://github.com/amannn/next-intl/blob/main/examples/example-app-router/src/app/manifest.ts
|
||||
*
|
||||
* The manifest.json provides metadata used when the web app is installed on a
|
||||
* user's mobile device or desktop. See https://web.dev/add-manifest/
|
||||
*
|
||||
* Since the manifest file needs to be placed in the root of the app folder (outside the [locale] dynamic segment),
|
||||
*
|
||||
* Since the manifest file needs to be placed in the root of the app folder (outside the [locale] dynamic segment),
|
||||
* you need to provide a locale explicitly since next-intl can’t infer it from the pathname.
|
||||
*
|
||||
*
|
||||
* Solution: use the default messages (get from the default locale)
|
||||
*
|
||||
*
|
||||
* https://next-intl.dev/docs/environments/actions-metadata-route-handlers#manifest
|
||||
*
|
||||
* @returns {MetadataRoute.Manifest} The manifest configuration object
|
||||
|
@ -15,7 +15,7 @@ export default function GlobalNotFound() {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<Error statusCode={404} />;
|
||||
<Error statusCode={404} />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { MetadataRoute } from 'next';
|
||||
import type { MetadataRoute } from 'next';
|
||||
import { getBaseUrl } from '../lib/urls/urls';
|
||||
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
|
@ -2,8 +2,8 @@ import { getLocalePathname } from '@/i18n/navigation';
|
||||
import { routing } from '@/i18n/routing';
|
||||
import { source } from '@/lib/docs/source';
|
||||
import { allCategories, allPosts } from 'content-collections';
|
||||
import { MetadataRoute } from 'next';
|
||||
import { Locale } from 'next-intl';
|
||||
import type { MetadataRoute } from 'next';
|
||||
import type { Locale } from 'next-intl';
|
||||
import { getBaseUrl } from '../lib/urls/urls';
|
||||
|
||||
type Href = Parameters<typeof getLocalePathname>[0]['href'];
|
||||
@ -29,7 +29,7 @@ const staticRoutes = [
|
||||
|
||||
/**
|
||||
* Generate a sitemap for the website
|
||||
*
|
||||
*
|
||||
* https://nextjs.org/docs/app/api-reference/functions/generate-sitemaps
|
||||
* https://github.com/javayhu/cnblocks/blob/main/app/sitemap.ts
|
||||
*/
|
||||
@ -37,45 +37,53 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
const sitemapList: MetadataRoute.Sitemap = []; // final result
|
||||
|
||||
// add static routes
|
||||
sitemapList.push(...staticRoutes.flatMap((route) => {
|
||||
return routing.locales.map((locale) => ({
|
||||
url: getUrl(route, locale),
|
||||
lastModified: new Date(),
|
||||
priority: 1,
|
||||
changeFrequency: 'weekly' as const
|
||||
}));
|
||||
}));
|
||||
sitemapList.push(
|
||||
...staticRoutes.flatMap((route) => {
|
||||
return routing.locales.map((locale) => ({
|
||||
url: getUrl(route, locale),
|
||||
lastModified: new Date(),
|
||||
priority: 1,
|
||||
changeFrequency: 'weekly' as const,
|
||||
}));
|
||||
})
|
||||
);
|
||||
|
||||
// add categories
|
||||
sitemapList.push(...allCategories.flatMap((category: { slug: string }) =>
|
||||
routing.locales.map((locale) => ({
|
||||
url: getUrl(`/blog/category/${category.slug}`, locale),
|
||||
lastModified: new Date(),
|
||||
priority: 0.8,
|
||||
changeFrequency: 'weekly' as const
|
||||
}))
|
||||
));
|
||||
sitemapList.push(
|
||||
...allCategories.flatMap((category: { slug: string }) =>
|
||||
routing.locales.map((locale) => ({
|
||||
url: getUrl(`/blog/category/${category.slug}`, locale),
|
||||
lastModified: new Date(),
|
||||
priority: 0.8,
|
||||
changeFrequency: 'weekly' as const,
|
||||
}))
|
||||
)
|
||||
);
|
||||
|
||||
// add posts
|
||||
sitemapList.push(...allPosts.flatMap((post: { slugAsParams: string }) =>
|
||||
routing.locales.map((locale) => ({
|
||||
url: getUrl(`/blog/${post.slugAsParams}`, locale),
|
||||
lastModified: new Date(),
|
||||
priority: 0.8,
|
||||
changeFrequency: 'weekly' as const
|
||||
}))
|
||||
));
|
||||
sitemapList.push(
|
||||
...allPosts.flatMap((post: { slugAsParams: string }) =>
|
||||
routing.locales.map((locale) => ({
|
||||
url: getUrl(`/blog/${post.slugAsParams}`, locale),
|
||||
lastModified: new Date(),
|
||||
priority: 0.8,
|
||||
changeFrequency: 'weekly' as const,
|
||||
}))
|
||||
)
|
||||
);
|
||||
|
||||
// add docs
|
||||
const docsParams = source.generateParams();
|
||||
sitemapList.push(...docsParams.flatMap(param =>
|
||||
routing.locales.map((locale) => ({
|
||||
url: getUrl(`/docs/${param.slug.join('/')}`, locale),
|
||||
lastModified: new Date(),
|
||||
priority: 0.8,
|
||||
changeFrequency: 'weekly' as const
|
||||
}))
|
||||
));
|
||||
sitemapList.push(
|
||||
...docsParams.flatMap((param) =>
|
||||
routing.locales.map((locale) => ({
|
||||
url: getUrl(`/docs/${param.slug.join('/')}`, locale),
|
||||
lastModified: new Date(),
|
||||
priority: 0.8,
|
||||
changeFrequency: 'weekly' as const,
|
||||
}))
|
||||
)
|
||||
);
|
||||
|
||||
return sitemapList;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -35,9 +35,7 @@ export const AuthCard = ({
|
||||
</LocaleLink>
|
||||
<CardDescription>{headerLabel}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{children}
|
||||
</CardContent>
|
||||
<CardContent>{children}</CardContent>
|
||||
<CardFooter>
|
||||
<BottomLink label={bottomButtonLabel} href={bottomButtonHref} />
|
||||
</CardFooter>
|
||||
|
@ -8,17 +8,12 @@ interface DividerWithTextProps {
|
||||
/**
|
||||
* A horizontal divider with text in the middle
|
||||
*/
|
||||
export const DividerWithText = ({
|
||||
text,
|
||||
className,
|
||||
}: DividerWithTextProps) => {
|
||||
export const DividerWithText = ({ text, className }: DividerWithTextProps) => {
|
||||
return (
|
||||
<div className={cn('relative flex items-center', className)}>
|
||||
<div className="grow border-t border-border"></div>
|
||||
<span className="shrink mx-4 text-sm text-muted-foreground">
|
||||
{text}
|
||||
</span>
|
||||
<div className="grow border-t border-border"></div>
|
||||
<div className="grow border-t border-border" />
|
||||
<span className="shrink mx-4 text-sm text-muted-foreground">{text}</span>
|
||||
<div className="grow border-t border-border" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
@ -118,9 +118,7 @@ export const ForgotPasswordForm = ({ className }: { className?: string }) => {
|
||||
type="submit"
|
||||
className="w-full cursor-pointer"
|
||||
>
|
||||
{isPending && (
|
||||
<Loader2Icon className="mr-2 size-4 animate-spin" />
|
||||
)}
|
||||
{isPending && <Loader2Icon className="mr-2 size-4 animate-spin" />}
|
||||
<span>{t('send')}</span>
|
||||
</Button>
|
||||
</form>
|
||||
|
@ -32,14 +32,20 @@ export interface LoginFormProps {
|
||||
callbackUrl?: string;
|
||||
}
|
||||
|
||||
export const LoginForm = ({ className, callbackUrl: propCallbackUrl }: LoginFormProps) => {
|
||||
export const LoginForm = ({
|
||||
className,
|
||||
callbackUrl: propCallbackUrl,
|
||||
}: LoginFormProps) => {
|
||||
const t = useTranslations('AuthPage.login');
|
||||
const searchParams = useSearchParams();
|
||||
const urlError = searchParams.get('error');
|
||||
const paramCallbackUrl = searchParams.get('callbackUrl');
|
||||
// Use prop callback URL or param callback URL if provided, otherwise use the default login redirect
|
||||
const locale = useLocale();
|
||||
const defaultCallbackUrl = getUrlWithLocaleInCallbackUrl(DEFAULT_LOGIN_REDIRECT, locale);
|
||||
const defaultCallbackUrl = getUrlWithLocaleInCallbackUrl(
|
||||
DEFAULT_LOGIN_REDIRECT,
|
||||
locale
|
||||
);
|
||||
const callbackUrl = propCallbackUrl || paramCallbackUrl || defaultCallbackUrl;
|
||||
console.log('login form, callbackUrl', callbackUrl);
|
||||
|
||||
@ -158,7 +164,7 @@ export const LoginForm = ({ className, callbackUrl: propCallbackUrl }: LoginForm
|
||||
{...field}
|
||||
disabled={isPending}
|
||||
placeholder="******"
|
||||
type={showPassword ? "text" : "password"}
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
className="pr-10"
|
||||
/>
|
||||
<Button
|
||||
@ -193,9 +199,7 @@ export const LoginForm = ({ className, callbackUrl: propCallbackUrl }: LoginForm
|
||||
type="submit"
|
||||
className="w-full flex items-center justify-center gap-2 cursor-pointer"
|
||||
>
|
||||
{isPending && (
|
||||
<Loader2Icon className="mr-2 size-4 animate-spin" />
|
||||
)}
|
||||
{isPending && <Loader2Icon className="mr-2 size-4 animate-spin" />}
|
||||
<span>{t('signIn')}</span>
|
||||
</Button>
|
||||
</form>
|
||||
|
@ -27,8 +27,6 @@ export const LoginWrapper = ({
|
||||
callbackUrl,
|
||||
}: LoginWrapperProps) => {
|
||||
const router = useLocaleRouter();
|
||||
const pathname = useLocalePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
const handleLogin = () => {
|
||||
@ -40,11 +38,6 @@ export const LoginWrapper = ({
|
||||
router.push(loginPath);
|
||||
};
|
||||
|
||||
// Close the modal on route change
|
||||
useEffect(() => {
|
||||
setIsModalOpen(false);
|
||||
}, [pathname, searchParams]);
|
||||
|
||||
if (mode === 'modal') {
|
||||
return (
|
||||
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
|
||||
|
@ -29,13 +29,18 @@ interface RegisterFormProps {
|
||||
callbackUrl?: string;
|
||||
}
|
||||
|
||||
export const RegisterForm = ({ callbackUrl: propCallbackUrl }: RegisterFormProps) => {
|
||||
export const RegisterForm = ({
|
||||
callbackUrl: propCallbackUrl,
|
||||
}: RegisterFormProps) => {
|
||||
const t = useTranslations('AuthPage.register');
|
||||
const searchParams = useSearchParams();
|
||||
const paramCallbackUrl = searchParams.get('callbackUrl');
|
||||
// Use prop callback URL or param callback URL if provided, otherwise use the default login redirect
|
||||
const locale = useLocale();
|
||||
const defaultCallbackUrl = getUrlWithLocaleInCallbackUrl(DEFAULT_LOGIN_REDIRECT, locale);
|
||||
const defaultCallbackUrl = getUrlWithLocaleInCallbackUrl(
|
||||
DEFAULT_LOGIN_REDIRECT,
|
||||
locale
|
||||
);
|
||||
const callbackUrl = propCallbackUrl || paramCallbackUrl || defaultCallbackUrl;
|
||||
console.log('register form, callbackUrl', callbackUrl);
|
||||
|
||||
@ -159,7 +164,7 @@ export const RegisterForm = ({ callbackUrl: propCallbackUrl }: RegisterFormProps
|
||||
{...field}
|
||||
disabled={isPending}
|
||||
placeholder="******"
|
||||
type={showPassword ? "text" : "password"}
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
className="pr-10"
|
||||
/>
|
||||
<Button
|
||||
@ -194,9 +199,7 @@ export const RegisterForm = ({ callbackUrl: propCallbackUrl }: RegisterFormProps
|
||||
type="submit"
|
||||
className="cursor-pointer w-full flex items-center justify-center gap-2"
|
||||
>
|
||||
{isPending && (
|
||||
<Loader2Icon className="mr-2 size-4 animate-spin" />
|
||||
)}
|
||||
{isPending && <Loader2Icon className="mr-2 size-4 animate-spin" />}
|
||||
<span>{t('signUp')}</span>
|
||||
</Button>
|
||||
</form>
|
||||
|
@ -117,7 +117,7 @@ export const ResetPasswordForm = () => {
|
||||
{...field}
|
||||
disabled={isPending}
|
||||
placeholder="******"
|
||||
type={showPassword ? "text" : "password"}
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
className="pr-10"
|
||||
/>
|
||||
<Button
|
||||
@ -152,9 +152,7 @@ export const ResetPasswordForm = () => {
|
||||
type="submit"
|
||||
className="w-full cursor-pointer"
|
||||
>
|
||||
{isPending && (
|
||||
<Loader2Icon className="mr-2 size-4 animate-spin" />
|
||||
)}
|
||||
{isPending && <Loader2Icon className="mr-2 size-4 animate-spin" />}
|
||||
<span>{t('reset')}</span>
|
||||
</Button>
|
||||
</form>
|
||||
|
@ -19,13 +19,18 @@ interface SocialLoginButtonProps {
|
||||
/**
|
||||
* social login buttons
|
||||
*/
|
||||
export const SocialLoginButton = ({ callbackUrl: propCallbackUrl }: SocialLoginButtonProps) => {
|
||||
export const SocialLoginButton = ({
|
||||
callbackUrl: propCallbackUrl,
|
||||
}: SocialLoginButtonProps) => {
|
||||
const t = useTranslations('AuthPage.login');
|
||||
const searchParams = useSearchParams();
|
||||
const paramCallbackUrl = searchParams.get('callbackUrl');
|
||||
// Use prop callback URL or param callback URL if provided, otherwise use the default login redirect
|
||||
const locale = useLocale();
|
||||
const defaultCallbackUrl = getUrlWithLocaleInCallbackUrl(DEFAULT_LOGIN_REDIRECT, locale);
|
||||
const defaultCallbackUrl = getUrlWithLocaleInCallbackUrl(
|
||||
DEFAULT_LOGIN_REDIRECT,
|
||||
locale
|
||||
);
|
||||
const callbackUrl = propCallbackUrl || paramCallbackUrl || defaultCallbackUrl;
|
||||
const [isLoading, setIsLoading] = useState<'google' | 'github' | null>(null);
|
||||
console.log('social login button, callbackUrl', callbackUrl);
|
||||
|
@ -12,9 +12,7 @@ export default function CallToActionSection() {
|
||||
<h2 className="text-balance text-4xl font-semibold lg:text-5xl">
|
||||
{t('title')}
|
||||
</h2>
|
||||
<p className="mt-4">
|
||||
{t('description')}
|
||||
</p>
|
||||
<p className="mt-4">{t('description')}</p>
|
||||
|
||||
<div className="mt-12 flex flex-wrap justify-center gap-4">
|
||||
<Button asChild size="lg">
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from '@/components/ui/accordion';
|
||||
import { IconName } from 'lucide-react/dynamic';
|
||||
import type { IconName } from 'lucide-react/dynamic';
|
||||
import { useLocale, useTranslations } from 'next-intl';
|
||||
|
||||
type FAQItem = {
|
||||
|
@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { BorderBeam } from '@/components/magicui/border-beam';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
@ -12,11 +13,10 @@ import {
|
||||
Fingerprint,
|
||||
IdCard,
|
||||
} from 'lucide-react';
|
||||
import { AnimatePresence, motion } from 'motion/react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import Image from 'next/image';
|
||||
import { useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'motion/react';
|
||||
import { BorderBeam } from '@/components/magicui/border-beam';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
/**
|
||||
* https://nsui.irung.me/features
|
||||
@ -48,15 +48,13 @@ export default function Features2Section() {
|
||||
|
||||
return (
|
||||
<section className="py-16">
|
||||
<div className="bg-linear-to-b absolute inset-0 -z-10 sm:inset-6 sm:rounded-b-3xl dark:block dark:to-[color-mix(in_oklab,var(--color-zinc-900)_75%,var(--color-background))]"></div>
|
||||
<div className="bg-linear-to-b absolute inset-0 -z-10 sm:inset-6 sm:rounded-b-3xl dark:block dark:to-[color-mix(in_oklab,var(--color-zinc-900)_75%,var(--color-background))]" />
|
||||
<div className="mx-auto max-w-6xl space-y-8 px-6 md:space-y-16 lg:space-y-20 dark:[--color-border:color-mix(in_oklab,var(--color-white)_10%,transparent)]">
|
||||
<div className="relative z-10 mx-auto max-w-2xl space-y-6 text-center">
|
||||
<h2 className="text-balance text-4xl lg:text-5xl font-semibold">
|
||||
{t('title')}
|
||||
</h2>
|
||||
<p>
|
||||
{t('description')}
|
||||
</p>
|
||||
<p>{t('description')}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-12 sm:px-12 md:grid-cols-2 lg:grid-cols-12 md:gap-12 lg:gap-24 lg:px-0">
|
||||
|
@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { BorderBeam } from '@/components/magicui/border-beam';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
@ -12,11 +13,10 @@ import {
|
||||
Fingerprint,
|
||||
IdCard,
|
||||
} from 'lucide-react';
|
||||
import { AnimatePresence, motion } from 'motion/react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import Image from 'next/image';
|
||||
import { useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'motion/react';
|
||||
import { BorderBeam } from '@/components/magicui/border-beam';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
/**
|
||||
* https://nsui.irung.me/features
|
||||
@ -54,13 +54,10 @@ export default function Features2Section() {
|
||||
<h2 className="text-balance text-4xl lg:text-5xl font-semibold">
|
||||
{t('title')}
|
||||
</h2>
|
||||
<p>
|
||||
{t('description')}
|
||||
</p>
|
||||
<p>{t('description')}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-12 sm:px-12 md:grid-cols-2 lg:grid-cols-12 md:gap-12 lg:gap-24 lg:px-0">
|
||||
|
||||
<div className="bg-background w-full relative flex overflow-hidden rounded-2xl border p-2 md:h-auto lg:col-span-7">
|
||||
<div className="aspect-76/59 bg-background relative w-full rounded-2xl">
|
||||
<AnimatePresence mode="wait">
|
||||
|
@ -1,4 +1,9 @@
|
||||
import { ActivityIcon, DraftingCompassIcon, MailIcon, ZapIcon } from 'lucide-react';
|
||||
import {
|
||||
ActivityIcon,
|
||||
DraftingCompassIcon,
|
||||
MailIcon,
|
||||
ZapIcon,
|
||||
} from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import Image from 'next/image';
|
||||
|
||||
@ -15,12 +20,8 @@ export default function Features3Section() {
|
||||
<div className="grid items-center gap-12 md:grid-cols-2 md:gap-12 lg:grid-cols-5 lg:gap-24">
|
||||
<div className="lg:col-span-2">
|
||||
<div className="md:pr-6 lg:pr-0">
|
||||
<h2 className="text-4xl font-semibold">
|
||||
{t('title')}
|
||||
</h2>
|
||||
<p className="mt-6">
|
||||
{t('description')}
|
||||
</p>
|
||||
<h2 className="text-4xl font-semibold">{t('title')}</h2>
|
||||
<p className="mt-6">{t('description')}</p>
|
||||
</div>
|
||||
|
||||
<ul className="mt-8 divide-y border-y *:flex *:items-center *:gap-3 *:py-3">
|
||||
|
@ -1,10 +1,15 @@
|
||||
import { ActivityIcon, DraftingCompassIcon, MailIcon, ZapIcon } from 'lucide-react';
|
||||
import {
|
||||
ActivityIcon,
|
||||
DraftingCompassIcon,
|
||||
MailIcon,
|
||||
ZapIcon,
|
||||
} from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import Image from 'next/image';
|
||||
|
||||
/**
|
||||
* Features4Section is Features3Section with a different layout
|
||||
*
|
||||
*
|
||||
* https://nsui.irung.me/features
|
||||
* pnpm dlx shadcn@canary add https://nsui.irung.me/r/features-5.json
|
||||
*/
|
||||
@ -36,12 +41,8 @@ export default function Features4Section() {
|
||||
|
||||
<div className="lg:col-span-2">
|
||||
<div className="md:pr-6 lg:pr-0">
|
||||
<h2 className="text-4xl font-semibold">
|
||||
{t('title')}
|
||||
</h2>
|
||||
<p className="mt-6">
|
||||
{t('description')}
|
||||
</p>
|
||||
<h2 className="text-4xl font-semibold">{t('title')}</h2>
|
||||
<p className="mt-6">{t('description')}</p>
|
||||
</div>
|
||||
|
||||
<ul className="mt-8 divide-y border-y *:flex *:items-center *:gap-3 *:py-3">
|
||||
|
@ -1,4 +1,11 @@
|
||||
import { CpuIcon, FingerprintIcon, PencilIcon, Settings2Icon, SparklesIcon, ZapIcon } from 'lucide-react';
|
||||
import {
|
||||
CpuIcon,
|
||||
FingerprintIcon,
|
||||
PencilIcon,
|
||||
Settings2Icon,
|
||||
SparklesIcon,
|
||||
ZapIcon,
|
||||
} from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
/**
|
||||
@ -15,16 +22,16 @@ export default function Features5Section() {
|
||||
<h2 className="text-balance text-4xl lg:text-5xl font-semibold">
|
||||
{t('title')}
|
||||
</h2>
|
||||
<p className="mt-4">
|
||||
{t('description')}
|
||||
</p>
|
||||
<p className="mt-4">{t('description')}</p>
|
||||
</div>
|
||||
|
||||
<div className="relative mx-auto grid divide-x divide-y border *:p-8 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<ZapIcon className="size-4" />
|
||||
<h3 className="text-base font-medium">{t('items.item-1.title')}</h3>
|
||||
<h3 className="text-base font-medium">
|
||||
{t('items.item-1.title')}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mt-4">
|
||||
{t('items.item-1.description')}
|
||||
@ -33,7 +40,9 @@ export default function Features5Section() {
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<CpuIcon className="size-4" />
|
||||
<h3 className="text-base font-medium">{t('items.item-2.title')}</h3>
|
||||
<h3 className="text-base font-medium">
|
||||
{t('items.item-2.title')}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mt-4">
|
||||
{t('items.item-2.description')}
|
||||
@ -43,7 +52,9 @@ export default function Features5Section() {
|
||||
<div className="flex items-center gap-2">
|
||||
<FingerprintIcon className="size-4" />
|
||||
|
||||
<h3 className="text-base font-medium">{t('items.item-3.title')}</h3>
|
||||
<h3 className="text-base font-medium">
|
||||
{t('items.item-3.title')}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mt-4">
|
||||
{t('items.item-3.description')}
|
||||
@ -53,7 +64,9 @@ export default function Features5Section() {
|
||||
<div className="flex items-center gap-2">
|
||||
<PencilIcon className="size-4" />
|
||||
|
||||
<h3 className="text-base font-medium">{t('items.item-4.title')}</h3>
|
||||
<h3 className="text-base font-medium">
|
||||
{t('items.item-4.title')}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mt-4">
|
||||
{t('items.item-4.description')}
|
||||
@ -63,7 +76,9 @@ export default function Features5Section() {
|
||||
<div className="flex items-center gap-2">
|
||||
<Settings2Icon className="size-4" />
|
||||
|
||||
<h3 className="text-base font-medium">{t('items.item-5.title')}</h3>
|
||||
<h3 className="text-base font-medium">
|
||||
{t('items.item-5.title')}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mt-4">
|
||||
{t('items.item-5.description')}
|
||||
@ -73,7 +88,9 @@ export default function Features5Section() {
|
||||
<div className="flex items-center gap-2">
|
||||
<SparklesIcon className="size-4" />
|
||||
|
||||
<h3 className="text-base font-medium">{t('items.item-6.title')}</h3>
|
||||
<h3 className="text-base font-medium">
|
||||
{t('items.item-6.title')}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mt-4">
|
||||
{t('items.item-6.description')}
|
||||
@ -83,4 +100,4 @@ export default function Features5Section() {
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -119,9 +119,7 @@ export default function HeroSection() {
|
||||
className="rounded-xl px-5 text-base"
|
||||
>
|
||||
<LocaleLink href={linkPrimary}>
|
||||
<span className="text-nowrap">
|
||||
{t('primary')}
|
||||
</span>
|
||||
<span className="text-nowrap">{t('primary')}</span>
|
||||
</LocaleLink>
|
||||
</Button>
|
||||
</div>
|
||||
@ -133,9 +131,7 @@ export default function HeroSection() {
|
||||
className="h-10.5 rounded-xl px-5"
|
||||
>
|
||||
<LocaleLink href={linkSecondary}>
|
||||
<span className="text-nowrap">
|
||||
{t('secondary')}
|
||||
</span>
|
||||
<span className="text-nowrap">{t('secondary')}</span>
|
||||
</LocaleLink>
|
||||
</Button>
|
||||
</AnimatedGroup>
|
||||
|
@ -11,7 +11,7 @@ import { Card } from '@/components/ui/card';
|
||||
import { LocaleLink } from '@/i18n/navigation';
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import * as React from 'react';
|
||||
import type * as React from 'react';
|
||||
|
||||
export default function IntegrationSection() {
|
||||
const t = useTranslations('HomePage.integration');
|
||||
@ -24,9 +24,7 @@ export default function IntegrationSection() {
|
||||
<h2 className="text-balance text-3xl font-semibold md:text-4xl">
|
||||
{t('title')}
|
||||
</h2>
|
||||
<p className="text-muted-foreground mt-6">
|
||||
{t('description')}
|
||||
</p>
|
||||
<p className="text-muted-foreground mt-6">{t('description')}</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-12 grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
|
@ -59,9 +59,7 @@ export default function Integration2Section() {
|
||||
<h2 className="text-balance text-3xl font-semibold md:text-4xl">
|
||||
{t('title')}
|
||||
</h2>
|
||||
<p className="text-muted-foreground">
|
||||
{t('description')}
|
||||
</p>
|
||||
<p className="text-muted-foreground">{t('description')}</p>
|
||||
|
||||
<div className="mt-12 flex flex-wrap justify-start gap-4">
|
||||
<Button asChild size="lg">
|
||||
|
@ -1,88 +1,85 @@
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
export default function LogoCloudSection() {
|
||||
const t = useTranslations('HomePage.logocloud');
|
||||
|
||||
return (
|
||||
<section className="py-16">
|
||||
<div className="mx-auto max-w-5xl px-6">
|
||||
<h2 className="text-center text-xl font-medium">
|
||||
{t('title')}
|
||||
</h2>
|
||||
<div className="mx-auto mt-20 flex max-w-4xl flex-wrap items-center justify-center gap-x-12 gap-y-8 sm:gap-x-16 sm:gap-y-12">
|
||||
<img
|
||||
className="h-4 w-fit dark:invert"
|
||||
src="/svg/nextjs_logo_light.svg"
|
||||
alt="Nextjs Logo"
|
||||
height="20"
|
||||
width="auto"
|
||||
/>
|
||||
<img
|
||||
className="h-4 w-fit dark:invert"
|
||||
src="/svg/tailwindcss.svg"
|
||||
alt="Tailwind CSS Logo"
|
||||
height="16"
|
||||
width="auto"
|
||||
/>
|
||||
<img
|
||||
className="h-6 w-fit dark:invert"
|
||||
src="/svg/resend-wordmark-black.svg"
|
||||
alt="Resend Logo"
|
||||
height="28"
|
||||
width="auto"
|
||||
/>
|
||||
<img
|
||||
className="h-5 w-fit dark:invert"
|
||||
src="/svg/vercel.svg"
|
||||
alt="Vercel Logo"
|
||||
height="20"
|
||||
width="auto"
|
||||
/>
|
||||
<img
|
||||
className="h-4 w-fit dark:invert"
|
||||
src="/svg/github.svg"
|
||||
alt="GitHub Logo"
|
||||
height="16"
|
||||
width="auto"
|
||||
/>
|
||||
<img
|
||||
className="h-5 w-fit dark:invert"
|
||||
src="/svg/cursor_wordmark_light.svg"
|
||||
alt="Cursor Logo"
|
||||
height="20"
|
||||
width="auto"
|
||||
/>
|
||||
<img
|
||||
className="h-5 w-fit dark:invert"
|
||||
src="/svg/lemonsqueezy.svg"
|
||||
alt="Lemon Squeezy Logo"
|
||||
height="16"
|
||||
width="auto"
|
||||
/>
|
||||
<img
|
||||
className="h-6 w-fit dark:invert"
|
||||
src="/svg/openai.svg"
|
||||
alt="OpenAI Logo"
|
||||
height="24"
|
||||
width="auto"
|
||||
/>
|
||||
<img
|
||||
className="h-4 w-fit dark:invert"
|
||||
src="/svg/zapier.svg"
|
||||
alt="Zapier Logo"
|
||||
height="20"
|
||||
width="auto"
|
||||
/>
|
||||
<img
|
||||
className="h-4 w-fit dark:invert"
|
||||
src="/svg/nvidia.svg"
|
||||
alt="NVIDIA Logo"
|
||||
height="20"
|
||||
width="auto"
|
||||
/>
|
||||
</div>
|
||||
const t = useTranslations('HomePage.logocloud');
|
||||
|
||||
return (
|
||||
<section className="py-16">
|
||||
<div className="mx-auto max-w-5xl px-6">
|
||||
<h2 className="text-center text-xl font-medium">{t('title')}</h2>
|
||||
<div className="mx-auto mt-20 flex max-w-4xl flex-wrap items-center justify-center gap-x-12 gap-y-8 sm:gap-x-16 sm:gap-y-12">
|
||||
<img
|
||||
className="h-4 w-fit dark:invert"
|
||||
src="/svg/nextjs_logo_light.svg"
|
||||
alt="Nextjs Logo"
|
||||
height="20"
|
||||
width="auto"
|
||||
/>
|
||||
<img
|
||||
className="h-4 w-fit dark:invert"
|
||||
src="/svg/tailwindcss.svg"
|
||||
alt="Tailwind CSS Logo"
|
||||
height="16"
|
||||
width="auto"
|
||||
/>
|
||||
<img
|
||||
className="h-6 w-fit dark:invert"
|
||||
src="/svg/resend-wordmark-black.svg"
|
||||
alt="Resend Logo"
|
||||
height="28"
|
||||
width="auto"
|
||||
/>
|
||||
<img
|
||||
className="h-5 w-fit dark:invert"
|
||||
src="/svg/vercel.svg"
|
||||
alt="Vercel Logo"
|
||||
height="20"
|
||||
width="auto"
|
||||
/>
|
||||
<img
|
||||
className="h-4 w-fit dark:invert"
|
||||
src="/svg/github.svg"
|
||||
alt="GitHub Logo"
|
||||
height="16"
|
||||
width="auto"
|
||||
/>
|
||||
<img
|
||||
className="h-5 w-fit dark:invert"
|
||||
src="/svg/cursor_wordmark_light.svg"
|
||||
alt="Cursor Logo"
|
||||
height="20"
|
||||
width="auto"
|
||||
/>
|
||||
<img
|
||||
className="h-5 w-fit dark:invert"
|
||||
src="/svg/lemonsqueezy.svg"
|
||||
alt="Lemon Squeezy Logo"
|
||||
height="16"
|
||||
width="auto"
|
||||
/>
|
||||
<img
|
||||
className="h-6 w-fit dark:invert"
|
||||
src="/svg/openai.svg"
|
||||
alt="OpenAI Logo"
|
||||
height="24"
|
||||
width="auto"
|
||||
/>
|
||||
<img
|
||||
className="h-4 w-fit dark:invert"
|
||||
src="/svg/zapier.svg"
|
||||
alt="Zapier Logo"
|
||||
height="20"
|
||||
width="auto"
|
||||
/>
|
||||
<img
|
||||
className="h-4 w-fit dark:invert"
|
||||
src="/svg/nvidia.svg"
|
||||
alt="NVIDIA Logo"
|
||||
height="20"
|
||||
width="auto"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { PricingTable } from "@/components/pricing/pricing-table";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { PricingTable } from '@/components/pricing/pricing-table';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
export default function PricingSection() {
|
||||
const t = useTranslations('HomePage.pricing');
|
||||
@ -11,11 +11,9 @@ export default function PricingSection() {
|
||||
<h2 className="text-balance text-4xl lg:text-5xl font-semibold">
|
||||
{t('title')}
|
||||
</h2>
|
||||
<p className="mt-4">
|
||||
{t('description')}
|
||||
</p>
|
||||
<p className="mt-4">{t('description')}</p>
|
||||
</div>
|
||||
|
||||
|
||||
<PricingTable />
|
||||
</div>
|
||||
</section>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
export default function StatsSection() {
|
||||
const t = useTranslations('HomePage.stats');
|
||||
@ -7,12 +7,8 @@ export default function StatsSection() {
|
||||
<section className="py-12 md:py-20 w-full bg-muted dark:bg-background">
|
||||
<div className="mx-auto max-w-5xl space-y-8 px-6 md:space-y-16">
|
||||
<div className="relative z-10 mx-auto max-w-xl space-y-6 text-center">
|
||||
<h2 className="text-4xl font-medium lg:text-5xl">
|
||||
{t('title')}
|
||||
</h2>
|
||||
<p>
|
||||
{t('description')}
|
||||
</p>
|
||||
<h2 className="text-4xl font-medium lg:text-5xl">{t('title')}</h2>
|
||||
<p>{t('description')}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-12 divide-y-0 *:text-center md:grid-cols-3 md:gap-2 md:divide-x">
|
||||
|
@ -95,7 +95,7 @@ export default function TestimonialsSection() {
|
||||
role: t('items.item-12.role'),
|
||||
image: t('items.item-12.image'),
|
||||
quote: t('items.item-12.quote'),
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
const testimonialChunks = chunkArray(
|
||||
@ -111,9 +111,7 @@ export default function TestimonialsSection() {
|
||||
<h2 className="text-title text-4xl lg:text-5xl font-semibold">
|
||||
{t('title')}
|
||||
</h2>
|
||||
<p className="text-body mt-6">
|
||||
{t('description')}
|
||||
</p>
|
||||
<p className="text-body mt-6">{t('description')}</p>
|
||||
</div>
|
||||
<div className="mt-8 grid gap-3 sm:grid-cols-2 md:mt-12 lg:grid-cols-3">
|
||||
{testimonialChunks.map((chunk, chunkIndex) => (
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { PLACEHOLDER_IMAGE } from '@/lib/constants';
|
||||
import { LocaleLink } from '@/i18n/navigation';
|
||||
import { PLACEHOLDER_IMAGE } from '@/lib/constants';
|
||||
import { formatDate } from '@/lib/formatter';
|
||||
import { Post } from 'content-collections';
|
||||
import type { Post } from 'content-collections';
|
||||
import Image from 'next/image';
|
||||
|
||||
interface BlogCardProps {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import Container from '@/components/layout/container';
|
||||
import { Category } from 'content-collections';
|
||||
import type { Category } from 'content-collections';
|
||||
import { BlogCategoryListDesktop } from './blog-category-list-desktop';
|
||||
import { BlogCategoryListMobile } from './blog-category-list-mobile';
|
||||
|
||||
|
@ -1,11 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Category } from 'content-collections';
|
||||
import { LocaleLink } from '@/i18n/navigation';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { Category } from 'content-collections';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useParams } from 'next/navigation';
|
||||
|
||||
export type BlogCategoryListDesktopProps = {
|
||||
categoryList: Category[];
|
||||
@ -52,7 +52,10 @@ export function BlogCategoryListDesktop({
|
||||
)}
|
||||
aria-label={`Toggle blog category of ${category.name}`}
|
||||
>
|
||||
<LocaleLink href={`/blog/category/${category.slug}`} className="px-4">
|
||||
<LocaleLink
|
||||
href={`/blog/category/${category.slug}`}
|
||||
className="px-4"
|
||||
>
|
||||
<h2>{category.name}</h2>
|
||||
</LocaleLink>
|
||||
</ToggleGroupItem>
|
||||
|
@ -9,7 +9,7 @@ import {
|
||||
DrawerTitle,
|
||||
DrawerTrigger,
|
||||
} from '@/components/ui/drawer';
|
||||
import { Category } from 'content-collections';
|
||||
import type { Category } from 'content-collections';
|
||||
import { LayoutListIcon } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useParams } from 'next/navigation';
|
||||
|
@ -1,6 +1,6 @@
|
||||
import BlogCard, { BlogCardSkeleton } from '@/components/blog/blog-card';
|
||||
import { websiteConfig } from '@/config/website';
|
||||
import { Post } from 'content-collections';
|
||||
import type { Post } from 'content-collections';
|
||||
|
||||
interface BlogGridProps {
|
||||
posts: Post[];
|
||||
|
@ -1,4 +1,4 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import { sendMessageAction } from '@/actions/send-message';
|
||||
import { FormError } from '@/components/shared/form-error';
|
||||
@ -9,7 +9,7 @@ import {
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import {
|
||||
Form,
|
||||
@ -17,13 +17,13 @@ import {
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useTransition, useState } from 'react';
|
||||
import { useState, useTransition } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import { z } from 'zod';
|
||||
@ -39,13 +39,8 @@ export function ContactFormCard() {
|
||||
|
||||
// Create a schema for contact form validation
|
||||
const formSchema = z.object({
|
||||
name: z
|
||||
.string()
|
||||
.min(3, t('nameMinLength'))
|
||||
.max(30, t('nameMaxLength')),
|
||||
email: z
|
||||
.string()
|
||||
.email(t('emailValidation')),
|
||||
name: z.string().min(3, t('nameMinLength')).max(30, t('nameMaxLength')),
|
||||
email: z.string().email(t('emailValidation')),
|
||||
message: z
|
||||
.string()
|
||||
.min(10, t('messageMinLength'))
|
||||
@ -70,10 +65,10 @@ export function ContactFormCard() {
|
||||
startTransition(async () => {
|
||||
try {
|
||||
setError('');
|
||||
|
||||
|
||||
// Submit form data using the contact server action
|
||||
const result = await sendMessageAction(values);
|
||||
|
||||
|
||||
if (result && result.data?.success) {
|
||||
toast.success(t('success'));
|
||||
form.reset();
|
||||
@ -93,12 +88,8 @@ export function ContactFormCard() {
|
||||
return (
|
||||
<Card className="mx-auto max-w-lg overflow-hidden pt-6 pb-0">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg font-semibold">
|
||||
{t('title')}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{t('description')}
|
||||
</CardDescription>
|
||||
<CardTitle className="text-lg font-semibold">{t('title')}</CardTitle>
|
||||
<CardDescription>{t('description')}</CardDescription>
|
||||
</CardHeader>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col">
|
||||
@ -110,10 +101,7 @@ export function ContactFormCard() {
|
||||
<FormItem>
|
||||
<FormLabel>{t('name')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={t('name')}
|
||||
{...field}
|
||||
/>
|
||||
<Input placeholder={t('name')} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -127,11 +115,7 @@ export function ContactFormCard() {
|
||||
<FormItem>
|
||||
<FormLabel>{t('email')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="email"
|
||||
placeholder={t('email')}
|
||||
{...field}
|
||||
/>
|
||||
<Input type="email" placeholder={t('email')} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -145,11 +129,7 @@ export function ContactFormCard() {
|
||||
<FormItem>
|
||||
<FormLabel>{t('message')}</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder={t('message')}
|
||||
rows={3}
|
||||
{...field}
|
||||
/>
|
||||
<Textarea placeholder={t('message')} rows={3} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -159,8 +139,8 @@ export function ContactFormCard() {
|
||||
<FormError message={error} />
|
||||
</CardContent>
|
||||
<CardFooter className="mt-6 px-6 py-4 flex justify-between items-center bg-muted rounded-none">
|
||||
<Button
|
||||
type="submit"
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isPending}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
@ -171,4 +151,4 @@ export function ContactFormCard() {
|
||||
</Form>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,5 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import * as React from "react";
|
||||
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts";
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
@ -9,159 +7,158 @@ import {
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
} from '@/components/ui/card';
|
||||
import {
|
||||
ChartConfig,
|
||||
type ChartConfig,
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from "@/components/ui/chart";
|
||||
} from '@/components/ui/chart';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
ToggleGroup,
|
||||
ToggleGroupItem,
|
||||
} from "@/components/ui/toggle-group";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
} from '@/components/ui/select';
|
||||
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
||||
import { useIsMobile } from '@/hooks/use-mobile';
|
||||
import * as React from 'react';
|
||||
import { Area, AreaChart, CartesianGrid, XAxis } from 'recharts';
|
||||
|
||||
export const description = "An interactive area chart"
|
||||
export const description = 'An interactive area chart';
|
||||
|
||||
const chartData = [
|
||||
{ date: "2024-04-01", desktop: 222, mobile: 150 },
|
||||
{ date: "2024-04-02", desktop: 97, mobile: 180 },
|
||||
{ date: "2024-04-03", desktop: 167, mobile: 120 },
|
||||
{ date: "2024-04-04", desktop: 242, mobile: 260 },
|
||||
{ date: "2024-04-05", desktop: 373, mobile: 290 },
|
||||
{ date: "2024-04-06", desktop: 301, mobile: 340 },
|
||||
{ date: "2024-04-07", desktop: 245, mobile: 180 },
|
||||
{ date: "2024-04-08", desktop: 409, mobile: 320 },
|
||||
{ date: "2024-04-09", desktop: 59, mobile: 110 },
|
||||
{ date: "2024-04-10", desktop: 261, mobile: 190 },
|
||||
{ date: "2024-04-11", desktop: 327, mobile: 350 },
|
||||
{ date: "2024-04-12", desktop: 292, mobile: 210 },
|
||||
{ date: "2024-04-13", desktop: 342, mobile: 380 },
|
||||
{ date: "2024-04-14", desktop: 137, mobile: 220 },
|
||||
{ date: "2024-04-15", desktop: 120, mobile: 170 },
|
||||
{ date: "2024-04-16", desktop: 138, mobile: 190 },
|
||||
{ date: "2024-04-17", desktop: 446, mobile: 360 },
|
||||
{ date: "2024-04-18", desktop: 364, mobile: 410 },
|
||||
{ date: "2024-04-19", desktop: 243, mobile: 180 },
|
||||
{ date: "2024-04-20", desktop: 89, mobile: 150 },
|
||||
{ date: "2024-04-21", desktop: 137, mobile: 200 },
|
||||
{ date: "2024-04-22", desktop: 224, mobile: 170 },
|
||||
{ date: "2024-04-23", desktop: 138, mobile: 230 },
|
||||
{ date: "2024-04-24", desktop: 387, mobile: 290 },
|
||||
{ date: "2024-04-25", desktop: 215, mobile: 250 },
|
||||
{ date: "2024-04-26", desktop: 75, mobile: 130 },
|
||||
{ date: "2024-04-27", desktop: 383, mobile: 420 },
|
||||
{ date: "2024-04-28", desktop: 122, mobile: 180 },
|
||||
{ date: "2024-04-29", desktop: 315, mobile: 240 },
|
||||
{ date: "2024-04-30", desktop: 454, mobile: 380 },
|
||||
{ date: "2024-05-01", desktop: 165, mobile: 220 },
|
||||
{ date: "2024-05-02", desktop: 293, mobile: 310 },
|
||||
{ date: "2024-05-03", desktop: 247, mobile: 190 },
|
||||
{ date: "2024-05-04", desktop: 385, mobile: 420 },
|
||||
{ date: "2024-05-05", desktop: 481, mobile: 390 },
|
||||
{ date: "2024-05-06", desktop: 498, mobile: 520 },
|
||||
{ date: "2024-05-07", desktop: 388, mobile: 300 },
|
||||
{ date: "2024-05-08", desktop: 149, mobile: 210 },
|
||||
{ date: "2024-05-09", desktop: 227, mobile: 180 },
|
||||
{ date: "2024-05-10", desktop: 293, mobile: 330 },
|
||||
{ date: "2024-05-11", desktop: 335, mobile: 270 },
|
||||
{ date: "2024-05-12", desktop: 197, mobile: 240 },
|
||||
{ date: "2024-05-13", desktop: 197, mobile: 160 },
|
||||
{ date: "2024-05-14", desktop: 448, mobile: 490 },
|
||||
{ date: "2024-05-15", desktop: 473, mobile: 380 },
|
||||
{ date: "2024-05-16", desktop: 338, mobile: 400 },
|
||||
{ date: "2024-05-17", desktop: 499, mobile: 420 },
|
||||
{ date: "2024-05-18", desktop: 315, mobile: 350 },
|
||||
{ date: "2024-05-19", desktop: 235, mobile: 180 },
|
||||
{ date: "2024-05-20", desktop: 177, mobile: 230 },
|
||||
{ date: "2024-05-21", desktop: 82, mobile: 140 },
|
||||
{ date: "2024-05-22", desktop: 81, mobile: 120 },
|
||||
{ date: "2024-05-23", desktop: 252, mobile: 290 },
|
||||
{ date: "2024-05-24", desktop: 294, mobile: 220 },
|
||||
{ date: "2024-05-25", desktop: 201, mobile: 250 },
|
||||
{ date: "2024-05-26", desktop: 213, mobile: 170 },
|
||||
{ date: "2024-05-27", desktop: 420, mobile: 460 },
|
||||
{ date: "2024-05-28", desktop: 233, mobile: 190 },
|
||||
{ date: "2024-05-29", desktop: 78, mobile: 130 },
|
||||
{ date: "2024-05-30", desktop: 340, mobile: 280 },
|
||||
{ date: "2024-05-31", desktop: 178, mobile: 230 },
|
||||
{ date: "2024-06-01", desktop: 178, mobile: 200 },
|
||||
{ date: "2024-06-02", desktop: 470, mobile: 410 },
|
||||
{ date: "2024-06-03", desktop: 103, mobile: 160 },
|
||||
{ date: "2024-06-04", desktop: 439, mobile: 380 },
|
||||
{ date: "2024-06-05", desktop: 88, mobile: 140 },
|
||||
{ date: "2024-06-06", desktop: 294, mobile: 250 },
|
||||
{ date: "2024-06-07", desktop: 323, mobile: 370 },
|
||||
{ date: "2024-06-08", desktop: 385, mobile: 320 },
|
||||
{ date: "2024-06-09", desktop: 438, mobile: 480 },
|
||||
{ date: "2024-06-10", desktop: 155, mobile: 200 },
|
||||
{ date: "2024-06-11", desktop: 92, mobile: 150 },
|
||||
{ date: "2024-06-12", desktop: 492, mobile: 420 },
|
||||
{ date: "2024-06-13", desktop: 81, mobile: 130 },
|
||||
{ date: "2024-06-14", desktop: 426, mobile: 380 },
|
||||
{ date: "2024-06-15", desktop: 307, mobile: 350 },
|
||||
{ date: "2024-06-16", desktop: 371, mobile: 310 },
|
||||
{ date: "2024-06-17", desktop: 475, mobile: 520 },
|
||||
{ date: "2024-06-18", desktop: 107, mobile: 170 },
|
||||
{ date: "2024-06-19", desktop: 341, mobile: 290 },
|
||||
{ date: "2024-06-20", desktop: 408, mobile: 450 },
|
||||
{ date: "2024-06-21", desktop: 169, mobile: 210 },
|
||||
{ date: "2024-06-22", desktop: 317, mobile: 270 },
|
||||
{ date: "2024-06-23", desktop: 480, mobile: 530 },
|
||||
{ date: "2024-06-24", desktop: 132, mobile: 180 },
|
||||
{ date: "2024-06-25", desktop: 141, mobile: 190 },
|
||||
{ date: "2024-06-26", desktop: 434, mobile: 380 },
|
||||
{ date: "2024-06-27", desktop: 448, mobile: 490 },
|
||||
{ date: "2024-06-28", desktop: 149, mobile: 200 },
|
||||
{ date: "2024-06-29", desktop: 103, mobile: 160 },
|
||||
{ date: "2024-06-30", desktop: 446, mobile: 400 },
|
||||
]
|
||||
{ date: '2024-04-01', desktop: 222, mobile: 150 },
|
||||
{ date: '2024-04-02', desktop: 97, mobile: 180 },
|
||||
{ date: '2024-04-03', desktop: 167, mobile: 120 },
|
||||
{ date: '2024-04-04', desktop: 242, mobile: 260 },
|
||||
{ date: '2024-04-05', desktop: 373, mobile: 290 },
|
||||
{ date: '2024-04-06', desktop: 301, mobile: 340 },
|
||||
{ date: '2024-04-07', desktop: 245, mobile: 180 },
|
||||
{ date: '2024-04-08', desktop: 409, mobile: 320 },
|
||||
{ date: '2024-04-09', desktop: 59, mobile: 110 },
|
||||
{ date: '2024-04-10', desktop: 261, mobile: 190 },
|
||||
{ date: '2024-04-11', desktop: 327, mobile: 350 },
|
||||
{ date: '2024-04-12', desktop: 292, mobile: 210 },
|
||||
{ date: '2024-04-13', desktop: 342, mobile: 380 },
|
||||
{ date: '2024-04-14', desktop: 137, mobile: 220 },
|
||||
{ date: '2024-04-15', desktop: 120, mobile: 170 },
|
||||
{ date: '2024-04-16', desktop: 138, mobile: 190 },
|
||||
{ date: '2024-04-17', desktop: 446, mobile: 360 },
|
||||
{ date: '2024-04-18', desktop: 364, mobile: 410 },
|
||||
{ date: '2024-04-19', desktop: 243, mobile: 180 },
|
||||
{ date: '2024-04-20', desktop: 89, mobile: 150 },
|
||||
{ date: '2024-04-21', desktop: 137, mobile: 200 },
|
||||
{ date: '2024-04-22', desktop: 224, mobile: 170 },
|
||||
{ date: '2024-04-23', desktop: 138, mobile: 230 },
|
||||
{ date: '2024-04-24', desktop: 387, mobile: 290 },
|
||||
{ date: '2024-04-25', desktop: 215, mobile: 250 },
|
||||
{ date: '2024-04-26', desktop: 75, mobile: 130 },
|
||||
{ date: '2024-04-27', desktop: 383, mobile: 420 },
|
||||
{ date: '2024-04-28', desktop: 122, mobile: 180 },
|
||||
{ date: '2024-04-29', desktop: 315, mobile: 240 },
|
||||
{ date: '2024-04-30', desktop: 454, mobile: 380 },
|
||||
{ date: '2024-05-01', desktop: 165, mobile: 220 },
|
||||
{ date: '2024-05-02', desktop: 293, mobile: 310 },
|
||||
{ date: '2024-05-03', desktop: 247, mobile: 190 },
|
||||
{ date: '2024-05-04', desktop: 385, mobile: 420 },
|
||||
{ date: '2024-05-05', desktop: 481, mobile: 390 },
|
||||
{ date: '2024-05-06', desktop: 498, mobile: 520 },
|
||||
{ date: '2024-05-07', desktop: 388, mobile: 300 },
|
||||
{ date: '2024-05-08', desktop: 149, mobile: 210 },
|
||||
{ date: '2024-05-09', desktop: 227, mobile: 180 },
|
||||
{ date: '2024-05-10', desktop: 293, mobile: 330 },
|
||||
{ date: '2024-05-11', desktop: 335, mobile: 270 },
|
||||
{ date: '2024-05-12', desktop: 197, mobile: 240 },
|
||||
{ date: '2024-05-13', desktop: 197, mobile: 160 },
|
||||
{ date: '2024-05-14', desktop: 448, mobile: 490 },
|
||||
{ date: '2024-05-15', desktop: 473, mobile: 380 },
|
||||
{ date: '2024-05-16', desktop: 338, mobile: 400 },
|
||||
{ date: '2024-05-17', desktop: 499, mobile: 420 },
|
||||
{ date: '2024-05-18', desktop: 315, mobile: 350 },
|
||||
{ date: '2024-05-19', desktop: 235, mobile: 180 },
|
||||
{ date: '2024-05-20', desktop: 177, mobile: 230 },
|
||||
{ date: '2024-05-21', desktop: 82, mobile: 140 },
|
||||
{ date: '2024-05-22', desktop: 81, mobile: 120 },
|
||||
{ date: '2024-05-23', desktop: 252, mobile: 290 },
|
||||
{ date: '2024-05-24', desktop: 294, mobile: 220 },
|
||||
{ date: '2024-05-25', desktop: 201, mobile: 250 },
|
||||
{ date: '2024-05-26', desktop: 213, mobile: 170 },
|
||||
{ date: '2024-05-27', desktop: 420, mobile: 460 },
|
||||
{ date: '2024-05-28', desktop: 233, mobile: 190 },
|
||||
{ date: '2024-05-29', desktop: 78, mobile: 130 },
|
||||
{ date: '2024-05-30', desktop: 340, mobile: 280 },
|
||||
{ date: '2024-05-31', desktop: 178, mobile: 230 },
|
||||
{ date: '2024-06-01', desktop: 178, mobile: 200 },
|
||||
{ date: '2024-06-02', desktop: 470, mobile: 410 },
|
||||
{ date: '2024-06-03', desktop: 103, mobile: 160 },
|
||||
{ date: '2024-06-04', desktop: 439, mobile: 380 },
|
||||
{ date: '2024-06-05', desktop: 88, mobile: 140 },
|
||||
{ date: '2024-06-06', desktop: 294, mobile: 250 },
|
||||
{ date: '2024-06-07', desktop: 323, mobile: 370 },
|
||||
{ date: '2024-06-08', desktop: 385, mobile: 320 },
|
||||
{ date: '2024-06-09', desktop: 438, mobile: 480 },
|
||||
{ date: '2024-06-10', desktop: 155, mobile: 200 },
|
||||
{ date: '2024-06-11', desktop: 92, mobile: 150 },
|
||||
{ date: '2024-06-12', desktop: 492, mobile: 420 },
|
||||
{ date: '2024-06-13', desktop: 81, mobile: 130 },
|
||||
{ date: '2024-06-14', desktop: 426, mobile: 380 },
|
||||
{ date: '2024-06-15', desktop: 307, mobile: 350 },
|
||||
{ date: '2024-06-16', desktop: 371, mobile: 310 },
|
||||
{ date: '2024-06-17', desktop: 475, mobile: 520 },
|
||||
{ date: '2024-06-18', desktop: 107, mobile: 170 },
|
||||
{ date: '2024-06-19', desktop: 341, mobile: 290 },
|
||||
{ date: '2024-06-20', desktop: 408, mobile: 450 },
|
||||
{ date: '2024-06-21', desktop: 169, mobile: 210 },
|
||||
{ date: '2024-06-22', desktop: 317, mobile: 270 },
|
||||
{ date: '2024-06-23', desktop: 480, mobile: 530 },
|
||||
{ date: '2024-06-24', desktop: 132, mobile: 180 },
|
||||
{ date: '2024-06-25', desktop: 141, mobile: 190 },
|
||||
{ date: '2024-06-26', desktop: 434, mobile: 380 },
|
||||
{ date: '2024-06-27', desktop: 448, mobile: 490 },
|
||||
{ date: '2024-06-28', desktop: 149, mobile: 200 },
|
||||
{ date: '2024-06-29', desktop: 103, mobile: 160 },
|
||||
{ date: '2024-06-30', desktop: 446, mobile: 400 },
|
||||
];
|
||||
|
||||
const chartConfig = {
|
||||
visitors: {
|
||||
label: "Visitors",
|
||||
label: 'Visitors',
|
||||
},
|
||||
desktop: {
|
||||
label: "Desktop",
|
||||
color: "var(--primary)",
|
||||
label: 'Desktop',
|
||||
color: 'var(--primary)',
|
||||
},
|
||||
mobile: {
|
||||
label: "Mobile",
|
||||
color: "var(--primary)",
|
||||
label: 'Mobile',
|
||||
color: 'var(--primary)',
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
} satisfies ChartConfig;
|
||||
|
||||
export function ChartAreaInteractive() {
|
||||
const isMobile = useIsMobile()
|
||||
const [timeRange, setTimeRange] = React.useState("90d")
|
||||
const isMobile = useIsMobile();
|
||||
const [timeRange, setTimeRange] = React.useState('90d');
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isMobile) {
|
||||
setTimeRange("7d")
|
||||
setTimeRange('7d');
|
||||
}
|
||||
}, [isMobile])
|
||||
}, [isMobile]);
|
||||
|
||||
const filteredData = chartData.filter((item) => {
|
||||
const date = new Date(item.date)
|
||||
const referenceDate = new Date("2024-06-30")
|
||||
let daysToSubtract = 90
|
||||
if (timeRange === "30d") {
|
||||
daysToSubtract = 30
|
||||
} else if (timeRange === "7d") {
|
||||
daysToSubtract = 7
|
||||
const date = new Date(item.date);
|
||||
const referenceDate = new Date('2024-06-30');
|
||||
let daysToSubtract = 90;
|
||||
if (timeRange === '30d') {
|
||||
daysToSubtract = 30;
|
||||
} else if (timeRange === '7d') {
|
||||
daysToSubtract = 7;
|
||||
}
|
||||
const startDate = new Date(referenceDate)
|
||||
startDate.setDate(startDate.getDate() - daysToSubtract)
|
||||
return date >= startDate
|
||||
})
|
||||
const startDate = new Date(referenceDate);
|
||||
startDate.setDate(startDate.getDate() - daysToSubtract);
|
||||
return date >= startDate;
|
||||
});
|
||||
|
||||
return (
|
||||
<Card className="@container/card">
|
||||
@ -247,11 +244,11 @@ export function ChartAreaInteractive() {
|
||||
tickMargin={8}
|
||||
minTickGap={32}
|
||||
tickFormatter={(value) => {
|
||||
const date = new Date(value)
|
||||
return date.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
})
|
||||
const date = new Date(value);
|
||||
return date.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<ChartTooltip
|
||||
@ -260,10 +257,10 @@ export function ChartAreaInteractive() {
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
labelFormatter={(value) => {
|
||||
return new Date(value).toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
})
|
||||
return new Date(value).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
}}
|
||||
indicator="dot"
|
||||
/>
|
||||
@ -287,5 +284,5 @@ export function ChartAreaInteractive() {
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user