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