refactor: run biome format to src folder

This commit is contained in:
javayhu 2025-03-09 22:13:59 +08:00
parent 92ec1b14c5
commit 34006556c8
155 changed files with 2343 additions and 2161 deletions

View File

@ -1,5 +1,5 @@
import localFont from "next/font/local";
import { Source_Serif_4 } from "next/font/google";
import localFont from 'next/font/local';
import { Source_Serif_4 } from 'next/font/google';
/**
* This file shows how to customize the font by using local font or google font
@ -12,8 +12,8 @@ import { Source_Serif_4 } from "next/font/google";
*/
// https://gwfh.mranftl.com/fonts/source-sans-3?subsets=latin
export const fontSourceSans = localFont({
src: "./source-sans-3-v15-latin-regular.woff2",
variable: "--font-source-sans",
src: './source-sans-3-v15-latin-regular.woff2',
variable: '--font-source-sans',
});
/**
@ -28,5 +28,5 @@ export const fontSourceSans = localFont({
export const fontSourceSerif4 = Source_Serif_4({
subsets: ['latin'],
display: 'swap',
variable: "--font-source-serif",
})
variable: '--font-source-serif',
});

View File

@ -1,6 +1,6 @@
"use client";
'use client';
import * as React from "react";
import * as React from 'react';
import {
BookOpen,
Bot,
@ -12,11 +12,11 @@ import {
Send,
Settings2,
SquareTerminal,
} from "lucide-react";
import { NavMain } from "@/components/nav-main";
import { NavProjects } from "@/components/nav-projects";
import { NavSecondary } from "@/components/nav-secondary";
import { NavUser } from "@/components/nav-user";
} from 'lucide-react';
import { NavMain } from '@/components/nav-main';
import { NavProjects } from '@/components/nav-projects';
import { NavSecondary } from '@/components/nav-secondary';
import { NavUser } from '@/components/nav-user';
import {
Sidebar,
SidebarContent,
@ -25,129 +25,129 @@ import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar";
import { siteConfig } from "@/config/site";
import { Logo } from "./logo";
} from '@/components/ui/sidebar';
import { siteConfig } from '@/config/site';
import { Logo } from './logo';
const data = {
user: {
name: "shadcn",
email: "m@example.com",
avatar: "/avatars/shadcn.jpg",
name: 'shadcn',
email: 'm@example.com',
avatar: '/avatars/shadcn.jpg',
},
navMain: [
{
title: "Playground",
url: "#",
title: 'Playground',
url: '#',
icon: SquareTerminal,
isActive: true,
items: [
{
title: "History",
url: "#",
title: 'History',
url: '#',
},
{
title: "Starred",
url: "#",
title: 'Starred',
url: '#',
},
{
title: "Settings",
url: "#",
title: 'Settings',
url: '#',
},
],
},
{
title: "Models",
url: "#",
title: 'Models',
url: '#',
icon: Bot,
items: [
{
title: "Genesis",
url: "#",
title: 'Genesis',
url: '#',
},
{
title: "Explorer",
url: "#",
title: 'Explorer',
url: '#',
},
{
title: "Quantum",
url: "#",
title: 'Quantum',
url: '#',
},
],
},
{
title: "Documentation",
url: "#",
title: 'Documentation',
url: '#',
icon: BookOpen,
items: [
{
title: "Introduction",
url: "#",
title: 'Introduction',
url: '#',
},
{
title: "Get Started",
url: "#",
title: 'Get Started',
url: '#',
},
{
title: "Tutorials",
url: "#",
title: 'Tutorials',
url: '#',
},
{
title: "Changelog",
url: "#",
title: 'Changelog',
url: '#',
},
],
},
{
title: "Settings",
url: "#",
title: 'Settings',
url: '#',
icon: Settings2,
items: [
{
title: "General",
url: "#",
title: 'General',
url: '#',
},
{
title: "Team",
url: "#",
title: 'Team',
url: '#',
},
{
title: "Billing",
url: "#",
title: 'Billing',
url: '#',
},
{
title: "Limits",
url: "#",
title: 'Limits',
url: '#',
},
],
},
],
navSecondary: [
{
title: "Support",
url: "#",
title: 'Support',
url: '#',
icon: LifeBuoy,
},
{
title: "Feedback",
url: "#",
title: 'Feedback',
url: '#',
icon: Send,
},
],
projects: [
{
name: "Design Engineering",
url: "#",
name: 'Design Engineering',
url: '#',
icon: Frame,
},
{
name: "Sales & Marketing",
url: "#",
name: 'Sales & Marketing',
url: '#',
icon: PieChart,
},
{
name: "Travel",
url: "#",
name: 'Travel',
url: '#',
icon: Map,
},
],
@ -163,7 +163,9 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
<a href="/">
<Logo className="size-8" />
<div className="grid flex-1 text-left leading-tight">
<span className="truncate font-semibold text-lg">{siteConfig.name}</span>
<span className="truncate font-semibold text-lg">
{siteConfig.name}
</span>
{/* <span className="truncate text-xs">{siteConfig.description}</span> */}
</div>
</a>

View File

@ -1,18 +1,18 @@
"use client";
'use client';
import { BottomButton } from "@/components/auth/bottom-button";
import { SocialLoginButton } from "@/components/auth/social-login-button";
import { BottomButton } from '@/components/auth/bottom-button';
import { SocialLoginButton } from '@/components/auth/social-login-button';
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader
} from "@/components/ui/card";
import { cn } from "@/lib/utils";
import Link from "next/link";
import { Logo } from "@/components/logo";
import { LocaleLink } from "@/i18n/navigation";
CardHeader,
} from '@/components/ui/card';
import { cn } from '@/lib/utils';
import Link from 'next/link';
import { Logo } from '@/components/logo';
import { LocaleLink } from '@/i18n/navigation';
interface AuthCardProps {
children: React.ReactNode;
@ -32,16 +32,14 @@ export const AuthCard = ({
className,
}: AuthCardProps) => {
return (
<Card className={cn("shadow-sm border border-border", className)}>
<Card className={cn('shadow-sm border border-border', className)}>
<CardHeader className="items-center">
<LocaleLink href="/" prefetch={false}>
<Logo className="mb-2" />
</LocaleLink>
<CardDescription>{headerLabel}</CardDescription>
</CardHeader>
<CardContent>
{children}
</CardContent>
<CardContent>{children}</CardContent>
{showSocialLoginButton && (
<CardFooter>
<SocialLoginButton />

View File

@ -1,11 +1,11 @@
"use client";
'use client';
import { buttonVariants } from "@/components/ui/button";
import { LocaleLink } from "@/i18n/navigation";
import { cn } from "@/lib/utils";
import { buttonVariants } from '@/components/ui/button';
import { LocaleLink } from '@/i18n/navigation';
import { cn } from '@/lib/utils';
interface BottomButtonProps {
href: string
href: string;
label: string;
}
@ -14,8 +14,8 @@ export const BottomButton = ({ href, label }: BottomButtonProps) => {
<LocaleLink
href={href}
className={cn(
buttonVariants({ variant: "link", size: "sm" }),
"font-normal w-full text-muted-foreground hover:underline underline-offset-4 hover:text-primary"
buttonVariants({ variant: 'link', size: 'sm' }),
'font-normal w-full text-muted-foreground hover:underline underline-offset-4 hover:text-primary'
)}
>
{label}

View File

@ -1,21 +1,21 @@
import { AuthCard } from "@/components/auth/auth-card";
import { Routes } from "@/routes";
import { TriangleAlertIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { AuthCard } from '@/components/auth/auth-card';
import { Routes } from '@/routes';
import { TriangleAlertIcon } from 'lucide-react';
import { useTranslations } from 'next-intl';
export const ErrorCard = () => {
const t = useTranslations("AuthPage.error");
const t = useTranslations('AuthPage.error');
return (
<AuthCard
headerLabel={t("title")}
headerLabel={t('title')}
bottomButtonHref={`${Routes.Login}`}
bottomButtonLabel={t("backToLogin")}
bottomButtonLabel={t('backToLogin')}
className="border-none"
>
<div className="w-full flex justify-center items-center py-4 gap-2">
<TriangleAlertIcon className="text-destructive size-4" />
<p className="font-medium text-destructive">{t("tryAgain")}</p>
<p className="font-medium text-destructive">{t('tryAgain')}</p>
</div>
</AuthCard>
);

View File

@ -1,9 +1,9 @@
"use client";
'use client';
import { AuthCard } from "@/components/auth/auth-card";
import { FormError } from "@/components/shared/form-error";
import { FormSuccess } from "@/components/shared/form-success";
import { Button } from "@/components/ui/button";
import { AuthCard } from '@/components/auth/auth-card';
import { FormError } from '@/components/shared/form-error';
import { FormSuccess } from '@/components/shared/form-success';
import { Button } from '@/components/ui/button';
import {
Form,
FormControl,
@ -11,65 +11,68 @@ import {
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { ForgotPasswordSchema } from "@/lib/schemas";
import { zodResolver } from "@hookform/resolvers/zod";
import { useState, useTransition } from "react";
import { useForm } from "react-hook-form";
import type * as z from "zod";
import { Icons } from "@/components/icons/icons";
import { authClient } from "@/lib/auth-client";
import { Routes } from "@/routes";
import { useTranslations } from "next-intl";
import { cn } from "@/lib/utils";
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { ForgotPasswordSchema } from '@/lib/schemas';
import { zodResolver } from '@hookform/resolvers/zod';
import { useState, useTransition } from 'react';
import { useForm } from 'react-hook-form';
import type * as z from 'zod';
import { Icons } from '@/components/icons/icons';
import { authClient } from '@/lib/auth-client';
import { Routes } from '@/routes';
import { useTranslations } from 'next-intl';
import { cn } from '@/lib/utils';
export const ForgotPasswordForm = ({ className }: { className?: string }) => {
const [error, setError] = useState<string | undefined>("");
const [success, setSuccess] = useState<string | undefined>("");
const [error, setError] = useState<string | undefined>('');
const [success, setSuccess] = useState<string | undefined>('');
const [isPending, setIsPending] = useState(false);
const t = useTranslations("AuthPage.forgotPassword");
const t = useTranslations('AuthPage.forgotPassword');
const form = useForm<z.infer<typeof ForgotPasswordSchema>>({
resolver: zodResolver(ForgotPasswordSchema),
defaultValues: {
email: "",
email: '',
},
});
const onSubmit = async (values: z.infer<typeof ForgotPasswordSchema>) => {
console.log("forgotPassword, values:", values);
const { data, error } = await authClient.forgetPassword({
email: values.email,
redirectTo: `${Routes.ResetPassword}`,
}, {
onRequest: (ctx) => {
console.log("forgotPassword, request:", ctx.url);
setIsPending(true);
setError("");
setSuccess("");
console.log('forgotPassword, values:', values);
const { data, error } = await authClient.forgetPassword(
{
email: values.email,
redirectTo: `${Routes.ResetPassword}`,
},
onResponse: (ctx) => {
console.log("forgotPassword, response:", ctx.response);
setIsPending(false);
},
onSuccess: (ctx) => {
console.log("forgotPassword, success:", ctx.data);
setSuccess(t("checkEmail"));
},
onError: (ctx) => {
console.error("forgotPassword, error:", ctx.error);
setError(ctx.error.message);
},
});
{
onRequest: (ctx) => {
console.log('forgotPassword, request:', ctx.url);
setIsPending(true);
setError('');
setSuccess('');
},
onResponse: (ctx) => {
console.log('forgotPassword, response:', ctx.response);
setIsPending(false);
},
onSuccess: (ctx) => {
console.log('forgotPassword, success:', ctx.data);
setSuccess(t('checkEmail'));
},
onError: (ctx) => {
console.error('forgotPassword, error:', ctx.error);
setError(ctx.error.message);
},
}
);
};
return (
<AuthCard
headerLabel={t("title")}
bottomButtonLabel={t("backToLogin")}
headerLabel={t('title')}
bottomButtonLabel={t('backToLogin')}
bottomButtonHref={`${Routes.Login}`}
className={cn("border-none", className)}
className={cn('border-none', className)}
>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
@ -79,7 +82,7 @@ export const ForgotPasswordForm = ({ className }: { className?: string }) => {
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>{t("email")}</FormLabel>
<FormLabel>{t('email')}</FormLabel>
<FormControl>
<Input
{...field}
@ -104,9 +107,9 @@ export const ForgotPasswordForm = ({ className }: { className?: string }) => {
{isPending ? (
<Icons.spinner className="w-4 h-4 animate-spin" />
) : (
""
''
)}
<span>{t("send")}</span>
<span>{t('send')}</span>
</Button>
</form>
</Form>

View File

@ -1,4 +1,4 @@
"use client";
'use client';
import {
Dialog,
@ -6,22 +6,22 @@ import {
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { LoginForm } from "@/components/auth/login-form";
import { useMediaQuery } from "@/hooks/use-media-query";
import { Routes, authRoutes } from "@/routes";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useEffect, useState } from "react";
} from '@/components/ui/dialog';
import { LoginForm } from '@/components/auth/login-form';
import { useMediaQuery } from '@/hooks/use-media-query';
import { Routes, authRoutes } from '@/routes';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import { useEffect, useState } from 'react';
interface LoginWrapperProps {
children: React.ReactNode;
mode?: "modal" | "redirect";
mode?: 'modal' | 'redirect';
asChild?: boolean;
}
export const LoginWrapper = ({
children,
mode = "redirect",
mode = 'redirect',
asChild,
}: LoginWrapperProps) => {
const router = useRouter();
@ -43,12 +43,10 @@ export const LoginWrapper = ({
// 2. keep isTablet or isDesktop open, if user resizes the window
// 3. TODO: pathname as Routes ???
const isAuthRoute = authRoutes.includes(pathname as Routes);
if (mode === "modal" && !isAuthRoute && (isTablet || isDesktop)) {
if (mode === 'modal' && !isAuthRoute && (isTablet || isDesktop)) {
return (
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
<DialogTrigger asChild={asChild}>
{children}
</DialogTrigger>
<DialogTrigger asChild={asChild}>{children}</DialogTrigger>
<DialogContent className="sm:max-w-[400px] p-0">
<DialogHeader>
{/* `DialogContent` requires a `DialogTitle` for the component to be accessible for screen reader users. */}

View File

@ -1,10 +1,10 @@
"use client";
'use client';
import { AuthCard } from "@/components/auth/auth-card";
import { Icons } from "@/components/icons/icons";
import { FormError } from "@/components/shared/form-error";
import { FormSuccess } from "@/components/shared/form-success";
import { Button } from "@/components/ui/button";
import { AuthCard } from '@/components/auth/auth-card';
import { Icons } from '@/components/icons/icons';
import { FormError } from '@/components/shared/form-error';
import { FormSuccess } from '@/components/shared/form-success';
import { Button } from '@/components/ui/button';
import {
Form,
FormControl,
@ -12,35 +12,35 @@ import {
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { LocaleLink } from "@/i18n/navigation";
import { authClient } from "@/lib/auth-client";
import { LoginSchema } from "@/lib/schemas";
import { cn } from "@/lib/utils";
import { Routes } from "@/routes";
import { zodResolver } from "@hookform/resolvers/zod";
import { useTranslations } from "next-intl";
import { useSearchParams } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
import type * as z from "zod";
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { LocaleLink } from '@/i18n/navigation';
import { authClient } from '@/lib/auth-client';
import { LoginSchema } from '@/lib/schemas';
import { cn } from '@/lib/utils';
import { Routes } from '@/routes';
import { zodResolver } from '@hookform/resolvers/zod';
import { useTranslations } from 'next-intl';
import { useSearchParams } from 'next/navigation';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import type * as z from 'zod';
export const LoginForm = ({ className }: { className?: string }) => {
const searchParams = useSearchParams();
const callbackUrl = searchParams.get("callbackUrl");
const urlError = searchParams.get("error");
const t = useTranslations("AuthPage.login");
const callbackUrl = searchParams.get('callbackUrl');
const urlError = searchParams.get('error');
const t = useTranslations('AuthPage.login');
const [error, setError] = useState<string | undefined>("");
const [success, setSuccess] = useState<string | undefined>("");
const [error, setError] = useState<string | undefined>('');
const [success, setSuccess] = useState<string | undefined>('');
const [isPending, setIsPending] = useState(false);
const form = useForm<z.infer<typeof LoginSchema>>({
resolver: zodResolver(LoginSchema),
defaultValues: {
email: "",
password: "",
email: '',
password: '',
},
});
@ -48,40 +48,43 @@ export const LoginForm = ({ className }: { className?: string }) => {
// 1. if callbackUrl is provided, user will be redirected to the callbackURL after login successfully.
// if user email is not verified, a new verification email will be sent to the user with the callbackURL.
// 2. if callbackUrl is not provided, we should redirect manually in the onSuccess callback.
const { data, error } = await authClient.signIn.email({
email: values.email,
password: values.password,
callbackURL: callbackUrl || Routes.DefaultLoginRedirect,
}, {
onRequest: (ctx) => {
// console.log("login, request:", ctx.url);
setIsPending(true);
setError("");
setSuccess("");
const { data, error } = await authClient.signIn.email(
{
email: values.email,
password: values.password,
callbackURL: callbackUrl || Routes.DefaultLoginRedirect,
},
onResponse: (ctx) => {
// console.log("login, response:", ctx.response);
setIsPending(false);
},
onSuccess: (ctx) => {
// console.log("login, success:", ctx.data);
// setSuccess("Login successful");
// router.push(callbackUrl || "/dashboard");
},
onError: (ctx) => {
console.error("login, error:", ctx.error);
setError(ctx.error.message);
},
});
{
onRequest: (ctx) => {
// console.log("login, request:", ctx.url);
setIsPending(true);
setError('');
setSuccess('');
},
onResponse: (ctx) => {
// console.log("login, response:", ctx.response);
setIsPending(false);
},
onSuccess: (ctx) => {
// console.log("login, success:", ctx.data);
// setSuccess("Login successful");
// router.push(callbackUrl || "/dashboard");
},
onError: (ctx) => {
console.error('login, error:', ctx.error);
setError(ctx.error.message);
},
}
);
};
return (
<AuthCard
headerLabel={t("welcomeBack")}
bottomButtonLabel={t("signUpHint")}
headerLabel={t('welcomeBack')}
bottomButtonLabel={t('signUpHint')}
bottomButtonHref={`${Routes.Register}`}
showSocialLoginButton
className={cn("border-none", className)}
className={cn('border-none', className)}
>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
@ -91,7 +94,7 @@ export const LoginForm = ({ className }: { className?: string }) => {
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>{t("email")}</FormLabel>
<FormLabel>{t('email')}</FormLabel>
<FormControl>
<Input
{...field}
@ -110,7 +113,7 @@ export const LoginForm = ({ className }: { className?: string }) => {
render={({ field }) => (
<FormItem>
<div className="flex justify-between items-center">
<FormLabel>{t("password")}</FormLabel>
<FormLabel>{t('password')}</FormLabel>
<Button
size="sm"
variant="link"
@ -121,7 +124,7 @@ export const LoginForm = ({ className }: { className?: string }) => {
href={`${Routes.ForgotPassword}`}
className="text-xs hover:underline hover:underline-offset-4 hover:text-primary"
>
{t("forgotPassword")}
{t('forgotPassword')}
</LocaleLink>
</Button>
</div>
@ -149,9 +152,9 @@ export const LoginForm = ({ className }: { className?: string }) => {
{isPending ? (
<Icons.spinner className="w-4 h-4 animate-spin" />
) : (
""
''
)}
<span>{t("signIn")}</span>
<span>{t('signIn')}</span>
</Button>
</form>
</Form>

View File

@ -1,10 +1,10 @@
"use client";
'use client';
import { AuthCard } from "@/components/auth/auth-card";
import { Icons } from "@/components/icons/icons";
import { FormError } from "@/components/shared/form-error";
import { FormSuccess } from "@/components/shared/form-success";
import { Button } from "@/components/ui/button";
import { AuthCard } from '@/components/auth/auth-card';
import { Icons } from '@/components/icons/icons';
import { FormError } from '@/components/shared/form-error';
import { FormSuccess } from '@/components/shared/form-success';
import { Button } from '@/components/ui/button';
import {
Form,
FormControl,
@ -12,74 +12,77 @@ import {
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { authClient } from "@/lib/auth-client";
import { RegisterSchema } from "@/lib/schemas";
import { Routes } from "@/routes";
import { zodResolver } from "@hookform/resolvers/zod";
import { useTranslations } from "next-intl";
import { useSearchParams } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
import type * as z from "zod";
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { authClient } from '@/lib/auth-client';
import { RegisterSchema } from '@/lib/schemas';
import { Routes } from '@/routes';
import { zodResolver } from '@hookform/resolvers/zod';
import { useTranslations } from 'next-intl';
import { useSearchParams } from 'next/navigation';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import type * as z from 'zod';
export const RegisterForm = () => {
const searchParams = useSearchParams();
const callbackUrl = searchParams.get("callbackUrl");
const t = useTranslations("AuthPage.register");
const [error, setError] = useState<string | undefined>("");
const [success, setSuccess] = useState<string | undefined>("");
const callbackUrl = searchParams.get('callbackUrl');
const t = useTranslations('AuthPage.register');
const [error, setError] = useState<string | undefined>('');
const [success, setSuccess] = useState<string | undefined>('');
const [isPending, setIsPending] = useState(false);
const form = useForm<z.infer<typeof RegisterSchema>>({
resolver: zodResolver(RegisterSchema),
defaultValues: {
email: "",
password: "",
name: "",
email: '',
password: '',
name: '',
},
});
const onSubmit = async (values: z.infer<typeof RegisterSchema>) => {
// 1. if requireEmailVerification is true, callbackURL will be used in the verification email,
// 1. if requireEmailVerification is true, callbackURL will be used in the verification email,
// the user will be redirected to the callbackURL after the email is verified.
// 2. if requireEmailVerification is false, the user will not be redirected to the callbackURL,
// we should redirect to the callbackURL manually in the onSuccess callback.
const { data, error } = await authClient.signUp.email({
email: values.email,
password: values.password,
name: values.name,
callbackURL: callbackUrl || Routes.DefaultLoginRedirect,
}, {
onRequest: (ctx) => {
console.log("register, request:", ctx.url);
setIsPending(true);
setError("");
setSuccess("");
const { data, error } = await authClient.signUp.email(
{
email: values.email,
password: values.password,
name: values.name,
callbackURL: callbackUrl || Routes.DefaultLoginRedirect,
},
onResponse: (ctx) => {
console.log("register, response:", ctx.response);
setIsPending(false);
},
onSuccess: (ctx) => {
// sign up success, user information stored in ctx.data
// console.log("register, success:", ctx.data);
setSuccess(t("checkEmail"));
},
onError: (ctx) => {
// sign up fail, display the error message
console.error("register, error:", ctx.error);
setError(ctx.error.message);
},
});
{
onRequest: (ctx) => {
console.log('register, request:', ctx.url);
setIsPending(true);
setError('');
setSuccess('');
},
onResponse: (ctx) => {
console.log('register, response:', ctx.response);
setIsPending(false);
},
onSuccess: (ctx) => {
// sign up success, user information stored in ctx.data
// console.log("register, success:", ctx.data);
setSuccess(t('checkEmail'));
},
onError: (ctx) => {
// sign up fail, display the error message
console.error('register, error:', ctx.error);
setError(ctx.error.message);
},
}
);
};
return (
<AuthCard
headerLabel={t("createAccount")}
bottomButtonLabel={t("signInHint")}
headerLabel={t('createAccount')}
bottomButtonLabel={t('signInHint')}
bottomButtonHref={`${Routes.Login}`}
showSocialLoginButton
className="border-none"
@ -92,7 +95,7 @@ export const RegisterForm = () => {
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>{t("name")}</FormLabel>
<FormLabel>{t('name')}</FormLabel>
<FormControl>
<Input {...field} disabled={isPending} placeholder="name" />
</FormControl>
@ -105,7 +108,7 @@ export const RegisterForm = () => {
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>{t("email")}</FormLabel>
<FormLabel>{t('email')}</FormLabel>
<FormControl>
<Input
{...field}
@ -123,7 +126,7 @@ export const RegisterForm = () => {
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>{t("password")}</FormLabel>
<FormLabel>{t('password')}</FormLabel>
<FormControl>
<Input
{...field}
@ -148,9 +151,9 @@ export const RegisterForm = () => {
{isPending ? (
<Icons.spinner className="w-4 h-4 animate-spin" />
) : (
""
''
)}
<span>{t("signUp")}</span>
<span>{t('signUp')}</span>
</Button>
</form>
</Form>

View File

@ -1,10 +1,10 @@
"use client";
'use client';
import { AuthCard } from "@/components/auth/auth-card";
import { Icons } from "@/components/icons/icons";
import { FormError } from "@/components/shared/form-error";
import { FormSuccess } from "@/components/shared/form-success";
import { Button } from "@/components/ui/button";
import { AuthCard } from '@/components/auth/auth-card';
import { Icons } from '@/components/icons/icons';
import { FormError } from '@/components/shared/form-error';
import { FormSuccess } from '@/components/shared/form-success';
import { Button } from '@/components/ui/button';
import {
Form,
FormControl,
@ -12,79 +12,82 @@ import {
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { authClient } from "@/lib/auth-client";
import { ResetPasswordSchema } from "@/lib/schemas";
import { Routes } from "@/routes";
import { zodResolver } from "@hookform/resolvers/zod";
import { useTranslations } from "next-intl";
import { notFound, useRouter, useSearchParams } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
import type * as z from "zod";
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { authClient } from '@/lib/auth-client';
import { ResetPasswordSchema } from '@/lib/schemas';
import { Routes } from '@/routes';
import { zodResolver } from '@hookform/resolvers/zod';
import { useTranslations } from 'next-intl';
import { notFound, useRouter, useSearchParams } from 'next/navigation';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import type * as z from 'zod';
/**
* https://www.better-auth.com/docs/authentication/email-password#forget-password
*/
export const ResetPasswordForm = () => {
const searchParams = useSearchParams();
const token = searchParams.get("token");
const token = searchParams.get('token');
if (!token) {
notFound();
}
// If the token is valid, the user will be redirected to this URL with the token in the query string.
// If the token is valid, the user will be redirected to this URL with the token in the query string.
// If the token is invalid, the user will be redirected to this URL with an error message in the query string ?error=invalid_token.
// TODO: check if the token is valid, show error message instead of redirecting to the 404 page
if (searchParams.get("error") === "invalid_token") {
if (searchParams.get('error') === 'invalid_token') {
notFound();
}
const router = useRouter();
const [error, setError] = useState<string | undefined>("");
const [success, setSuccess] = useState<string | undefined>("");
const [error, setError] = useState<string | undefined>('');
const [success, setSuccess] = useState<string | undefined>('');
const [isPending, setIsPending] = useState(false);
const t = useTranslations("AuthPage.resetPassword");
const t = useTranslations('AuthPage.resetPassword');
const form = useForm<z.infer<typeof ResetPasswordSchema>>({
resolver: zodResolver(ResetPasswordSchema),
defaultValues: {
password: "",
password: '',
},
});
const onSubmit = async (values: z.infer<typeof ResetPasswordSchema>) => {
const { data, error } = await authClient.resetPassword({
newPassword: values.password,
token,
}, {
onRequest: (ctx) => {
// console.log("resetPassword, request:", ctx.url);
setIsPending(true);
setError("");
setSuccess("");
const { data, error } = await authClient.resetPassword(
{
newPassword: values.password,
token,
},
onResponse: (ctx) => {
// console.log("resetPassword, response:", ctx.response);
setIsPending(false);
},
onSuccess: (ctx) => {
// console.log("resetPassword, success:", ctx.data);
// setSuccess("Password reset successfully");
router.push(`${Routes.Login}`);
},
onError: (ctx) => {
console.error("resetPassword, error:", ctx.error);
setError(ctx.error.message);
},
});
{
onRequest: (ctx) => {
// console.log("resetPassword, request:", ctx.url);
setIsPending(true);
setError('');
setSuccess('');
},
onResponse: (ctx) => {
// console.log("resetPassword, response:", ctx.response);
setIsPending(false);
},
onSuccess: (ctx) => {
// console.log("resetPassword, success:", ctx.data);
// setSuccess("Password reset successfully");
router.push(`${Routes.Login}`);
},
onError: (ctx) => {
console.error('resetPassword, error:', ctx.error);
setError(ctx.error.message);
},
}
);
};
return (
<AuthCard
headerLabel={t("title")}
bottomButtonLabel={t("backToLogin")}
headerLabel={t('title')}
bottomButtonLabel={t('backToLogin')}
bottomButtonHref={`${Routes.Login}`}
className="border-none"
>
@ -96,7 +99,7 @@ export const ResetPasswordForm = () => {
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>{t("password")}</FormLabel>
<FormLabel>{t('password')}</FormLabel>
<FormControl>
<Input
{...field}
@ -121,9 +124,9 @@ export const ResetPasswordForm = () => {
{isPending ? (
<Icons.spinner className="w-4 h-4 animate-spin" />
) : (
""
''
)}
<span>{t("reset")}</span>
<span>{t('reset')}</span>
</Button>
</form>
</Form>

View File

@ -1,67 +1,70 @@
"use client";
'use client';
import { Icons } from "@/components/icons/icons";
import { Button } from "@/components/ui/button";
import { useSearchParams } from "next/navigation";
import { useState } from "react";
import { GitHubIcon } from "@/components/icons/github";
import { GoogleIcon } from "@/components/icons/google";
import { authClient } from "@/lib/auth-client";
import { Routes } from "@/routes";
import { useTranslations } from "next-intl";
import { Icons } from '@/components/icons/icons';
import { Button } from '@/components/ui/button';
import { useSearchParams } from 'next/navigation';
import { useState } from 'react';
import { GitHubIcon } from '@/components/icons/github';
import { GoogleIcon } from '@/components/icons/google';
import { authClient } from '@/lib/auth-client';
import { Routes } from '@/routes';
import { useTranslations } from 'next-intl';
/**
* social login buttons
*/
export const SocialLoginButton = () => {
const searchParams = useSearchParams();
const callbackUrl = searchParams.get("callbackUrl");
const [isLoading, setIsLoading] = useState<"google" | "github" | null>(null);
const t = useTranslations("AuthPage.login");
const callbackUrl = searchParams.get('callbackUrl');
const [isLoading, setIsLoading] = useState<'google' | 'github' | null>(null);
const t = useTranslations('AuthPage.login');
const onClick = async (provider: "google" | "github") => {
await authClient.signIn.social({
/**
* The social provider id
* @example "github", "google"
*/
provider: provider,
/**
* a url to redirect after the user authenticates with the provider
* @default "/"
*/
callbackURL: callbackUrl || Routes.DefaultLoginRedirect,
/**
* a url to redirect if an error occurs during the sign in process
*/
errorCallbackURL: Routes.AuthError,
/**
* a url to redirect if the user is newly registered
*/
// newUserCallbackURL: "/auth/welcome",
/**
* disable the automatic redirect to the provider.
* @default false
*/
// disableRedirect: true,
}, {
onRequest: (ctx) => {
// console.log("onRequest", ctx);
setIsLoading(provider);
const onClick = async (provider: 'google' | 'github') => {
await authClient.signIn.social(
{
/**
* The social provider id
* @example "github", "google"
*/
provider: provider,
/**
* a url to redirect after the user authenticates with the provider
* @default "/"
*/
callbackURL: callbackUrl || Routes.DefaultLoginRedirect,
/**
* a url to redirect if an error occurs during the sign in process
*/
errorCallbackURL: Routes.AuthError,
/**
* a url to redirect if the user is newly registered
*/
// newUserCallbackURL: "/auth/welcome",
/**
* disable the automatic redirect to the provider.
* @default false
*/
// disableRedirect: true,
},
onResponse: (ctx) => {
// console.log("onResponse", ctx.response);
setIsLoading(null);
},
onSuccess: (ctx) => {
// console.log("onSuccess", ctx.data);
setIsLoading(null);
},
onError: (ctx) => {
console.log("onError", ctx.error.message);
setIsLoading(null);
},
});
{
onRequest: (ctx) => {
// console.log("onRequest", ctx);
setIsLoading(provider);
},
onResponse: (ctx) => {
// console.log("onResponse", ctx.response);
setIsLoading(null);
},
onSuccess: (ctx) => {
// console.log("onSuccess", ctx.data);
setIsLoading(null);
},
onError: (ctx) => {
console.log('onError', ctx.error.message);
setIsLoading(null);
},
}
);
};
return (
@ -70,29 +73,29 @@ export const SocialLoginButton = () => {
size="lg"
className="w-full"
variant="outline"
onClick={() => onClick("google")}
disabled={isLoading === "google"}
onClick={() => onClick('google')}
disabled={isLoading === 'google'}
>
{isLoading === "google" ? (
{isLoading === 'google' ? (
<Icons.spinner className="mr-2 size-4 animate-spin" />
) : (
<GoogleIcon className="size-5 mr-2" />
)}
<span>{t("signInWithGoogle")}</span>
<span>{t('signInWithGoogle')}</span>
</Button>
<Button
size="lg"
className="w-full"
variant="outline"
onClick={() => onClick("github")}
disabled={isLoading === "github"}
onClick={() => onClick('github')}
disabled={isLoading === 'github'}
>
{isLoading === "github" ? (
{isLoading === 'github' ? (
<Icons.spinner className="mr-2 size-4 animate-spin" />
) : (
<GitHubIcon className="size-5 mr-2" />
)}
<span>{t("signInWithGitHub")}</span>
<span>{t('signInWithGitHub')}</span>
</Button>
</div>
);

View File

@ -1,5 +1,5 @@
import { Button } from "@/components/ui/button";
import Link from "next/link";
import { Button } from '@/components/ui/button';
import Link from 'next/link';
/**
* https://nsui.irung.me/call-to-action

View File

@ -1,5 +1,5 @@
import { Button } from "@/components/ui/button";
import { Mail, SendHorizonal } from "lucide-react";
import { Button } from '@/components/ui/button';
import { Mail, SendHorizonal } from 'lucide-react';
/**
* https://nsui.irung.me/call-to-action

View File

@ -1,5 +1,5 @@
import { Button } from "@/components/ui/button";
import Link from "next/link";
import { Button } from '@/components/ui/button';
import Link from 'next/link';
/**
* https://nsui.irung.me/call-to-action

View File

@ -1,5 +1,5 @@
import { Cpu, Zap } from "lucide-react";
import Image from "next/image";
import { Cpu, Zap } from 'lucide-react';
import Image from 'next/image';
/**
* https://nsui.irung.me/content
@ -16,10 +16,10 @@ export default function Content2() {
<div className="relative">
<div className="relative z-10 space-y-4 md:w-1/2">
<p className="text-body">
Lyra is evolving to be more than just the models.{" "}
Lyra is evolving to be more than just the models.{' '}
<span className="text-title font-medium">
It supports an entire ecosystem
</span>{" "}
</span>{' '}
from products innovate.
</p>
<p>

View File

@ -1,6 +1,6 @@
import { Button } from "@/components/ui/button";
import { ChevronRight } from "lucide-react";
import Link from "next/link";
import { Button } from '@/components/ui/button';
import { ChevronRight } from 'lucide-react';
import Link from 'next/link';
/**
* https://nsui.irung.me/content-3

View File

@ -1,6 +1,6 @@
import { Button } from "@/components/ui/button";
import { ChevronRight } from "lucide-react";
import Link from "next/link";
import { Button } from '@/components/ui/button';
import { ChevronRight } from 'lucide-react';
import Link from 'next/link';
/**
* https://nsui.irung.me/content-4
@ -23,8 +23,8 @@ export default function Content4() {
developers and businesses innovate.
</p>
<p>
Tailus UI.{" "}
<span className="font-bold">It supports an entire ecosystem</span>{" "}
Tailus UI.{' '}
<span className="font-bold">It supports an entire ecosystem</span>{' '}
from products innovate. Sit minus, quod debitis autem quia
aspernatur delectus impedit modi, neque non id ad dignissimos?
Saepe deleniti perferendis beatae.

View File

@ -1,4 +1,4 @@
import { Cpu, Lock, Sparkles, Zap } from "lucide-react";
import { Cpu, Lock, Sparkles, Zap } from 'lucide-react';
/**
* https://nsui.irung.me/content-5

View File

@ -1,4 +1,4 @@
import Link from "next/link";
import Link from 'next/link';
/**
* https://nsui.irung.me/content-6

View File

@ -1,4 +1,4 @@
import Image from "next/image";
import Image from 'next/image';
/**
* https://nsui.irung.me/content
@ -34,10 +34,10 @@ export default function Content() {
<div className="relative space-y-4">
<p className="text-muted-foreground">
Gemini is evolving to be more than just the models.{" "}
Gemini is evolving to be more than just the models.{' '}
<span className="text-accent-foreground font-bold">
It supports an entire ecosystem
</span>{" "}
</span>{' '}
from products innovate.
</p>
<p className="text-muted-foreground">

View File

@ -10,7 +10,7 @@ export default function FAQs() {
<div className="grid gap-y-12 px-2 lg:[grid-template-columns:1fr_auto]">
<div className="text-center lg:text-left">
<h2 className="mb-4 text-3xl font-semibold md:text-4xl">
Frequently <br className="hidden lg:block" /> Asked{" "}
Frequently <br className="hidden lg:block" /> Asked{' '}
<br className="hidden lg:block" />
Questions
</h2>

View File

@ -1,6 +1,6 @@
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { Settings2, Sparkles, Zap } from "lucide-react";
import { ReactNode } from "react";
import { Card, CardContent, CardHeader } from '@/components/ui/card';
import { Settings2, Sparkles, Zap } from 'lucide-react';
import { ReactNode } from 'react';
/**
* https://nsui.irung.me/features

View File

@ -5,7 +5,7 @@ import {
Settings2,
Sparkles,
Zap,
} from "lucide-react";
} from 'lucide-react';
/**
* https://nsui.irung.me/features-4

View File

@ -1,5 +1,5 @@
import { Activity, DraftingCompass, Mail, Zap } from "lucide-react";
import Image from "next/image";
import { Activity, DraftingCompass, Mail, Zap } from 'lucide-react';
import Image from 'next/image';
/**
* https://nsui.irung.me/features-5

View File

@ -1,5 +1,5 @@
import { Cpu, Lock, Sparkles, Zap } from "lucide-react";
import Image from "next/image";
import { Cpu, Lock, Sparkles, Zap } from 'lucide-react';
import Image from 'next/image';
/**
* https://nsui.irung.me/features-6

View File

@ -1,5 +1,5 @@
import { Cpu, Lock, Sparkles, Zap } from "lucide-react";
import Image from "next/image";
import { Cpu, Lock, Sparkles, Zap } from 'lucide-react';
import Image from 'next/image';
/**
* https://nsui.irung.me/features-7

View File

@ -1,5 +1,5 @@
import { Card, CardContent } from "@/components/ui/card";
import { Shield, Users } from "lucide-react";
import { Card, CardContent } from '@/components/ui/card';
import { Shield, Users } from 'lucide-react';
/**
* https://nsui.irung.me/features-8

View File

@ -1,14 +1,14 @@
"use client";
import { Logo } from "@/components/logo";
import { Activity, Map as MapIcon, MessageCircle } from "lucide-react";
import DottedMap from "dotted-map";
import { Area, AreaChart, CartesianGrid } from "recharts";
'use client';
import { Logo } from '@/components/logo';
import { Activity, Map as MapIcon, MessageCircle } from 'lucide-react';
import DottedMap from 'dotted-map';
import { Area, AreaChart, CartesianGrid } from 'recharts';
import {
type ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart";
} from '@/components/ui/chart';
/**
* https://nsui.irung.me/features-9
@ -96,9 +96,9 @@ export default function Features9() {
</span>
<p className="my-8 text-2xl font-semibold">
Monitor your application's activity in real-time.{" "}
Monitor your application's activity in real-time.{' '}
<span className="text-muted-foreground">
{" "}
{' '}
Instantly identify and resolve issues.
</span>
</p>
@ -110,13 +110,13 @@ export default function Features9() {
);
}
const map = new DottedMap({ height: 55, grid: "diagonal" });
const map = new DottedMap({ height: 55, grid: 'diagonal' });
const points = map.getPoints();
const svgOptions = {
backgroundColor: "var(--color-background)",
color: "currentColor",
backgroundColor: 'var(--color-background)',
color: 'currentColor',
radius: 0.15,
};
@ -139,22 +139,22 @@ const Map = () => {
const chartConfig = {
desktop: {
label: "Desktop",
color: "#2563eb",
label: 'Desktop',
color: '#2563eb',
},
mobile: {
label: "Mobile",
color: "#60a5fa",
label: 'Mobile',
color: '#60a5fa',
},
} satisfies ChartConfig;
const chartData = [
{ month: "May", desktop: 56, mobile: 224 },
{ month: "June", desktop: 56, mobile: 224 },
{ month: "January", desktop: 126, mobile: 252 },
{ month: "February", desktop: 205, mobile: 410 },
{ month: "March", desktop: 200, mobile: 126 },
{ month: "April", desktop: 400, mobile: 800 },
{ month: 'May', desktop: 56, mobile: 224 },
{ month: 'June', desktop: 56, mobile: 224 },
{ month: 'January', desktop: 126, mobile: 252 },
{ month: 'February', desktop: 205, mobile: 410 },
{ month: 'March', desktop: 200, mobile: 126 },
{ month: 'April', desktop: 400, mobile: 800 },
];
const MonitoringChart = () => {

View File

@ -1,6 +1,6 @@
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { Settings2, Sparkles, Zap } from "lucide-react";
import { ReactNode } from "react";
import { Card, CardContent, CardHeader } from '@/components/ui/card';
import { Settings2, Sparkles, Zap } from 'lucide-react';
import { ReactNode } from 'react';
/**
* https://nsui.irung.me/features

View File

@ -1,16 +1,16 @@
"use client";
import { Logo } from "@/components/logo";
import { Button } from "@/components/ui/button";
import { ArrowRight, Mail, Menu, SendHorizonal, X } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { useState } from "react";
'use client';
import { Logo } from '@/components/logo';
import { Button } from '@/components/ui/button';
import { ArrowRight, Mail, Menu, SendHorizonal, X } from 'lucide-react';
import Image from 'next/image';
import Link from 'next/link';
import { useState } from 'react';
const menuItems = [
{ name: "Features", href: "#" },
{ name: "Solution", href: "#" },
{ name: "Pricing", href: "#" },
{ name: "About", href: "#" },
{ name: 'Features', href: '#' },
{ name: 'Solution', href: '#' },
{ name: 'Pricing', href: '#' },
{ name: 'About', href: '#' },
];
export default function HeroSection2() {

View File

@ -1,22 +1,22 @@
"use client";
import React from "react";
import { Swiper, SwiperSlide } from "swiper/react";
import { Autoplay, EffectCoverflow } from "swiper/modules";
import "swiper/css";
import "swiper/css/autoplay";
import "swiper/css/navigation";
import "swiper/css/pagination";
import "swiper/css/effect-coverflow";
import Link from "next/link";
import { Logo } from "@/components/logo";
import { ArrowRight, Menu, Rocket, X } from "lucide-react";
import { Button } from "@/components/ui/button";
'use client';
import React from 'react';
import { Swiper, SwiperSlide } from 'swiper/react';
import { Autoplay, EffectCoverflow } from 'swiper/modules';
import 'swiper/css';
import 'swiper/css/autoplay';
import 'swiper/css/navigation';
import 'swiper/css/pagination';
import 'swiper/css/effect-coverflow';
import Link from 'next/link';
import { Logo } from '@/components/logo';
import { ArrowRight, Menu, Rocket, X } from 'lucide-react';
import { Button } from '@/components/ui/button';
const menuItems = [
{ name: "Features", href: "#" },
{ name: "Solution", href: "#" },
{ name: "Pricing", href: "#" },
{ name: "About", href: "#" },
{ name: 'Features', href: '#' },
{ name: 'Solution', href: '#' },
{ name: 'Pricing', href: '#' },
{ name: 'About', href: '#' },
];
export default function HeroSection3() {

View File

@ -1,16 +1,16 @@
"use client";
import React from "react";
import Link from "next/link";
import { Logo } from "@/components/logo";
import { ArrowRight, Menu, Rocket, X } from "lucide-react";
import { Button } from "@/components/ui/button";
import Image from "next/image";
'use client';
import React from 'react';
import Link from 'next/link';
import { Logo } from '@/components/logo';
import { ArrowRight, Menu, Rocket, X } from 'lucide-react';
import { Button } from '@/components/ui/button';
import Image from 'next/image';
const menuItems = [
{ name: "Features", href: "#" },
{ name: "Solution", href: "#" },
{ name: "Pricing", href: "#" },
{ name: "About", href: "#" },
{ name: 'Features', href: '#' },
{ name: 'Solution', href: '#' },
{ name: 'Pricing', href: '#' },
{ name: 'About', href: '#' },
];
export default function HeroSection4() {

View File

@ -1,16 +1,16 @@
"use client";
import { Logo } from "@/components/logo";
import Link from "next/link";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Menu, X } from "lucide-react";
import Image from "next/image";
'use client';
import { Logo } from '@/components/logo';
import Link from 'next/link';
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Menu, X } from 'lucide-react';
import Image from 'next/image';
const menuItems = [
{ name: "Features", href: "#" },
{ name: "Solution", href: "#" },
{ name: "Pricing", href: "#" },
{ name: "About", href: "#" },
{ name: 'Features', href: '#' },
{ name: 'Solution', href: '#' },
{ name: 'Pricing', href: '#' },
{ name: 'About', href: '#' },
];
export default function HeroSection() {

View File

@ -1,13 +1,13 @@
import Link from "next/link";
import { Button } from "@/components/ui/button";
import Link from 'next/link';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Check } from "lucide-react";
} from '@/components/ui/card';
import { Check } from 'lucide-react';
/**
* https://nsui.irung.me/pricing
@ -45,9 +45,9 @@ export default function Pricing3() {
<ul className="list-outside space-y-3 text-sm">
{[
"Basic Analytics Dashboard",
"5GB Cloud Storage",
"Email and Chat Support",
'Basic Analytics Dashboard',
'5GB Cloud Storage',
'Email and Chat Support',
].map((item, index) => (
<li key={index} className="flex items-center gap-2">
<Check className="size-3" />
@ -82,16 +82,16 @@ export default function Pricing3() {
<ul className="list-outside space-y-3 text-sm">
{[
"Everything in Free Plan",
"5GB Cloud Storage",
"Email and Chat Support",
"Access to Community Forum",
"Single User Access",
"Access to Basic Templates",
"Mobile App Access",
"1 Custom Report Per Month",
"Monthly Product Updates",
"Standard Security Features",
'Everything in Free Plan',
'5GB Cloud Storage',
'Email and Chat Support',
'Access to Community Forum',
'Single User Access',
'Access to Basic Templates',
'Mobile App Access',
'1 Custom Report Per Month',
'Monthly Product Updates',
'Standard Security Features',
].map((item, index) => (
<li key={index} className="flex items-center gap-2">
<Check className="size-3" />
@ -122,9 +122,9 @@ export default function Pricing3() {
<ul className="list-outside space-y-3 text-sm">
{[
"Everything in Pro Plan",
"5GB Cloud Storage",
"Email and Chat Support",
'Everything in Pro Plan',
'5GB Cloud Storage',
'Email and Chat Support',
].map((item, index) => (
<li key={index} className="flex items-center gap-2">
<Check className="size-3" />

View File

@ -1,6 +1,6 @@
import { Button } from "@/components/ui/button";
import { Check } from "lucide-react";
import Link from "next/link";
import { Button } from '@/components/ui/button';
import { Check } from 'lucide-react';
import Link from 'next/link';
/**
* https://nsui.irung.me/pricing
@ -39,9 +39,9 @@ export default function Pricing4() {
<ul className="list-outside space-y-3 text-sm">
{[
"Basic Analytics Dashboard",
"5GB Cloud Storage",
"Email and Chat Support",
'Basic Analytics Dashboard',
'5GB Cloud Storage',
'Email and Chat Support',
].map((item, index) => (
<li key={index} className="flex items-center gap-2">
<Check className="size-3" />
@ -75,16 +75,16 @@ export default function Pricing4() {
<ul className="mt-4 list-outside space-y-3 text-sm">
{[
"Everything in Free Plan",
"5GB Cloud Storage",
"Email and Chat Support",
"Access to Community Forum",
"Single User Access",
"Access to Basic Templates",
"Mobile App Access",
"1 Custom Report Per Month",
"Monthly Product Updates",
"Standard Security Features",
'Everything in Free Plan',
'5GB Cloud Storage',
'Email and Chat Support',
'Access to Community Forum',
'Single User Access',
'Access to Basic Templates',
'Mobile App Access',
'1 Custom Report Per Month',
'Monthly Product Updates',
'Standard Security Features',
].map((item, index) => (
<li key={index} className="flex items-center gap-2">
<Check className="size-3" />

View File

@ -1,6 +1,6 @@
import { Button } from "@/components/ui/button";
import { Check } from "lucide-react";
import Link from "next/link";
import { Button } from '@/components/ui/button';
import { Check } from 'lucide-react';
import Link from 'next/link';
/**
* https://nsui.irung.me/pricing
@ -39,10 +39,10 @@ export default function Pricing5() {
<div className="relative">
<ul role="list" className="space-y-4">
{[
"First premium advantage",
"Second advantage weekly",
"Third advantage donate to project",
"Fourth, access to all components weekly",
'First premium advantage',
'Second advantage weekly',
'Third advantage donate to project',
'Fourth, access to all components weekly',
].map((item, index) => (
<li key={index} className="flex items-center gap-2">
<Check className="size-3" />

View File

@ -1,13 +1,13 @@
import Link from "next/link";
import { Button } from "@/components/ui/button";
import Link from 'next/link';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Check } from "lucide-react";
} from '@/components/ui/card';
import { Check } from 'lucide-react';
export default function Pricing() {
return (
@ -42,9 +42,9 @@ export default function Pricing() {
<ul className="list-outside space-y-3 text-sm">
{[
"Basic Analytics Dashboard",
"5GB Cloud Storage",
"Email and Chat Support",
'Basic Analytics Dashboard',
'5GB Cloud Storage',
'Email and Chat Support',
].map((item, index) => (
<li key={index} className="flex items-center gap-2">
<Check className="size-3" />
@ -79,16 +79,16 @@ export default function Pricing() {
<ul className="list-outside space-y-3 text-sm">
{[
"Everything in Free Plan",
"5GB Cloud Storage",
"Email and Chat Support",
"Access to Community Forum",
"Single User Access",
"Access to Basic Templates",
"Mobile App Access",
"1 Custom Report Per Month",
"Monthly Product Updates",
"Standard Security Features",
'Everything in Free Plan',
'5GB Cloud Storage',
'Email and Chat Support',
'Access to Community Forum',
'Single User Access',
'Access to Basic Templates',
'Mobile App Access',
'1 Custom Report Per Month',
'Monthly Product Updates',
'Standard Security Features',
].map((item, index) => (
<li key={index} className="flex items-center gap-2">
<Check className="size-3" />
@ -119,9 +119,9 @@ export default function Pricing() {
<ul className="list-outside space-y-3 text-sm">
{[
"Everything in Pro Plan",
"5GB Cloud Storage",
"Email and Chat Support",
'Everything in Pro Plan',
'5GB Cloud Storage',
'Email and Chat Support',
].map((item, index) => (
<li key={index} className="flex items-center gap-2">
<Check className="size-3" />

View File

@ -14,8 +14,8 @@ export default function Stats4() {
The Gemini ecosystem brings together our models.
</h2>
<p>
Gemini is evolving to be more than just the models.{" "}
<span className="font-medium">It supports an entire ecosystem</span>{" "}
Gemini is evolving to be more than just the models.{' '}
<span className="font-medium">It supports an entire ecosystem</span>{' '}
from products innovate.
</p>
</div>

View File

@ -1,5 +1,5 @@
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Card, CardContent, CardHeader } from '@/components/ui/card';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
/**
* https://nsui.irung.me/testimonials

View File

@ -1,4 +1,4 @@
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
/**
* https://nsui.irung.me/testimonials

View File

@ -1,5 +1,5 @@
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Card, CardContent } from "@/components/ui/card";
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Card, CardContent } from '@/components/ui/card';
type Testimonial = {
name: string;
@ -10,88 +10,88 @@ type Testimonial = {
const testimonials: Testimonial[] = [
{
name: "Jonathan Yombo",
role: "Software Engineer",
image: "https://randomuser.me/api/portraits/men/1.jpg",
name: 'Jonathan Yombo',
role: 'Software Engineer',
image: 'https://randomuser.me/api/portraits/men/1.jpg',
quote:
"Tailus is really extraordinary and very practical, no need to break your head. A real gold mine.",
'Tailus is really extraordinary and very practical, no need to break your head. A real gold mine.',
},
{
name: "Yves Kalume",
role: "GDE - Android",
image: "https://randomuser.me/api/portraits/men/6.jpg",
name: 'Yves Kalume',
role: 'GDE - Android',
image: 'https://randomuser.me/api/portraits/men/6.jpg',
quote:
"With no experience in webdesign I just redesigned my entire website in a few minutes with tailwindcss thanks to Tailus.",
'With no experience in webdesign I just redesigned my entire website in a few minutes with tailwindcss thanks to Tailus.',
},
{
name: "Yucel Faruksahan",
role: "Tailkits Creator",
image: "https://randomuser.me/api/portraits/men/7.jpg",
name: 'Yucel Faruksahan',
role: 'Tailkits Creator',
image: 'https://randomuser.me/api/portraits/men/7.jpg',
quote:
"Great work on tailfolio template. This is one of the best personal website that I have seen so far :)",
'Great work on tailfolio template. This is one of the best personal website that I have seen so far :)',
},
{
name: "Anonymous author",
role: "Doing something",
image: "https://randomuser.me/api/portraits/men/8.jpg",
name: 'Anonymous author',
role: 'Doing something',
image: 'https://randomuser.me/api/portraits/men/8.jpg',
quote:
"I am really new to Tailwind and I want to give a go to make some page on my own. I searched a lot of hero pages and blocks online. However, most of them are not giving me a clear view or needed some HTML/CSS coding background to make some changes from the original or too expensive to have. I downloaded the one of Tailus template which is very clear to understand at the start and you could modify the codes/blocks to fit perfectly on your purpose of the page.",
'I am really new to Tailwind and I want to give a go to make some page on my own. I searched a lot of hero pages and blocks online. However, most of them are not giving me a clear view or needed some HTML/CSS coding background to make some changes from the original or too expensive to have. I downloaded the one of Tailus template which is very clear to understand at the start and you could modify the codes/blocks to fit perfectly on your purpose of the page.',
},
{
name: "Shekinah Tshiokufila",
role: "Senior Software Engineer",
image: "https://randomuser.me/api/portraits/men/4.jpg",
name: 'Shekinah Tshiokufila',
role: 'Senior Software Engineer',
image: 'https://randomuser.me/api/portraits/men/4.jpg',
quote:
"Tailus is redefining the standard of web design, with these blocks it provides an easy and efficient way for those who love beauty but may lack the time to implement it. I can only recommend this incredible wonder.",
'Tailus is redefining the standard of web design, with these blocks it provides an easy and efficient way for those who love beauty but may lack the time to implement it. I can only recommend this incredible wonder.',
},
{
name: "Oketa Fred",
role: "Fullstack Developer",
image: "https://randomuser.me/api/portraits/men/2.jpg",
name: 'Oketa Fred',
role: 'Fullstack Developer',
image: 'https://randomuser.me/api/portraits/men/2.jpg',
quote:
"I absolutely love Tailus! The component blocks are beautifully designed and easy to use, which makes creating a great-looking website a breeze.",
'I absolutely love Tailus! The component blocks are beautifully designed and easy to use, which makes creating a great-looking website a breeze.',
},
{
name: "Zeki",
role: "Founder of ChatExtend",
image: "https://randomuser.me/api/portraits/men/5.jpg",
name: 'Zeki',
role: 'Founder of ChatExtend',
image: 'https://randomuser.me/api/portraits/men/5.jpg',
quote:
"Using TailsUI has been like unlocking a secret design superpower. It's the perfect fusion of simplicity and versatility, enabling us to create UIs that are as stunning as they are user-friendly.",
},
{
name: "Joseph Kitheka",
role: "Fullstack Developer",
image: "https://randomuser.me/api/portraits/men/9.jpg",
name: 'Joseph Kitheka',
role: 'Fullstack Developer',
image: 'https://randomuser.me/api/portraits/men/9.jpg',
quote:
"Tailus has transformed the way I develop web applications. Their extensive collection of UI components, blocks, and templates has significantly accelerated my workflow. The flexibility to customize every aspect allows me to create unique user experiences. Tailus is a game-changer for modern web development!",
'Tailus has transformed the way I develop web applications. Their extensive collection of UI components, blocks, and templates has significantly accelerated my workflow. The flexibility to customize every aspect allows me to create unique user experiences. Tailus is a game-changer for modern web development!',
},
{
name: "Khatab Wedaa",
role: "MerakiUI Creator",
image: "https://randomuser.me/api/portraits/men/10.jpg",
name: 'Khatab Wedaa',
role: 'MerakiUI Creator',
image: 'https://randomuser.me/api/portraits/men/10.jpg',
quote:
"Tailus is an elegant, clean, and responsive tailwind css components it's very helpful to start fast with your project.",
},
{
name: "Rodrigo Aguilar",
role: "TailwindAwesome Creator",
image: "https://randomuser.me/api/portraits/men/11.jpg",
name: 'Rodrigo Aguilar',
role: 'TailwindAwesome Creator',
image: 'https://randomuser.me/api/portraits/men/11.jpg',
quote:
"I love Tailus ❤️. The component blocks are well-structured, simple to use, and beautifully designed. It makes it really easy to have a good-looking website in no time.",
'I love Tailus ❤️. The component blocks are well-structured, simple to use, and beautifully designed. It makes it really easy to have a good-looking website in no time.',
},
{
name: "Eric Ampire",
role: "Mobile Engineer at @BRPNews • @GoogleDevExpert for Android",
image: "https://randomuser.me/api/portraits/men/12.jpg",
name: 'Eric Ampire',
role: 'Mobile Engineer at @BRPNews • @GoogleDevExpert for Android',
image: 'https://randomuser.me/api/portraits/men/12.jpg',
quote:
"Tailus templates are the perfect solution for anyone who wants to create a beautiful and functional website without any web design experience. The templates are easy to use, customizable, and responsive, and the support team is always available to help. I highly recommend Tailus templates to anyone who is looking to create a website.",
'Tailus templates are the perfect solution for anyone who wants to create a beautiful and functional website without any web design experience. The templates are easy to use, customizable, and responsive, and the support team is always available to help. I highly recommend Tailus templates to anyone who is looking to create a website.',
},
{
name: "Roland Tubonge",
role: "Software Engineer",
image: "https://randomuser.me/api/portraits/men/13.jpg",
name: 'Roland Tubonge',
role: 'Software Engineer',
image: 'https://randomuser.me/api/portraits/men/13.jpg',
quote:
"Tailus is so well designed that even with a very poor knowledge of web design you can do miracles. Let yourself be seduced!",
'Tailus is so well designed that even with a very poor knowledge of web design you can do miracles. Let yourself be seduced!',
},
];

View File

@ -1,5 +1,5 @@
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Card, CardContent } from "@/components/ui/card";
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Card, CardContent } from '@/components/ui/card';
type Testimonial = {
name: string;
@ -10,88 +10,88 @@ type Testimonial = {
const testimonials: Testimonial[] = [
{
name: "Jonathan Yombo",
role: "Software Engineer",
image: "https://randomuser.me/api/portraits/men/1.jpg",
name: 'Jonathan Yombo',
role: 'Software Engineer',
image: 'https://randomuser.me/api/portraits/men/1.jpg',
quote:
"Tailus is really extraordinary and very practical, no need to break your head. A real gold mine.",
'Tailus is really extraordinary and very practical, no need to break your head. A real gold mine.',
},
{
name: "Yves Kalume",
role: "GDE - Android",
image: "https://randomuser.me/api/portraits/men/6.jpg",
name: 'Yves Kalume',
role: 'GDE - Android',
image: 'https://randomuser.me/api/portraits/men/6.jpg',
quote:
"With no experience in webdesign I just redesigned my entire website in a few minutes with tailwindcss thanks to Tailus.",
'With no experience in webdesign I just redesigned my entire website in a few minutes with tailwindcss thanks to Tailus.',
},
{
name: "Yucel Faruksahan",
role: "Tailkits Creator",
image: "https://randomuser.me/api/portraits/men/7.jpg",
name: 'Yucel Faruksahan',
role: 'Tailkits Creator',
image: 'https://randomuser.me/api/portraits/men/7.jpg',
quote:
"Great work on tailfolio template. This is one of the best personal website that I have seen so far :)",
'Great work on tailfolio template. This is one of the best personal website that I have seen so far :)',
},
{
name: "Anonymous author",
role: "Doing something",
image: "https://randomuser.me/api/portraits/men/8.jpg",
name: 'Anonymous author',
role: 'Doing something',
image: 'https://randomuser.me/api/portraits/men/8.jpg',
quote:
"I am really new to Tailwind and I want to give a go to make some page on my own. I searched a lot of hero pages and blocks online. However, most of them are not giving me a clear view or needed some HTML/CSS coding background to make some changes from the original or too expensive to have. I downloaded the one of Tailus template which is very clear to understand at the start and you could modify the codes/blocks to fit perfectly on your purpose of the page.",
'I am really new to Tailwind and I want to give a go to make some page on my own. I searched a lot of hero pages and blocks online. However, most of them are not giving me a clear view or needed some HTML/CSS coding background to make some changes from the original or too expensive to have. I downloaded the one of Tailus template which is very clear to understand at the start and you could modify the codes/blocks to fit perfectly on your purpose of the page.',
},
{
name: "Shekinah Tshiokufila",
role: "Senior Software Engineer",
image: "https://randomuser.me/api/portraits/men/4.jpg",
name: 'Shekinah Tshiokufila',
role: 'Senior Software Engineer',
image: 'https://randomuser.me/api/portraits/men/4.jpg',
quote:
"Tailus is redefining the standard of web design, with these blocks it provides an easy and efficient way for those who love beauty but may lack the time to implement it. I can only recommend this incredible wonder.",
'Tailus is redefining the standard of web design, with these blocks it provides an easy and efficient way for those who love beauty but may lack the time to implement it. I can only recommend this incredible wonder.',
},
{
name: "Oketa Fred",
role: "Fullstack Developer",
image: "https://randomuser.me/api/portraits/men/2.jpg",
name: 'Oketa Fred',
role: 'Fullstack Developer',
image: 'https://randomuser.me/api/portraits/men/2.jpg',
quote:
"I absolutely love Tailus! The component blocks are beautifully designed and easy to use, which makes creating a great-looking website a breeze.",
'I absolutely love Tailus! The component blocks are beautifully designed and easy to use, which makes creating a great-looking website a breeze.',
},
{
name: "Zeki",
role: "Founder of ChatExtend",
image: "https://randomuser.me/api/portraits/men/5.jpg",
name: 'Zeki',
role: 'Founder of ChatExtend',
image: 'https://randomuser.me/api/portraits/men/5.jpg',
quote:
"Using TailsUI has been like unlocking a secret design superpower. It's the perfect fusion of simplicity and versatility, enabling us to create UIs that are as stunning as they are user-friendly.",
},
{
name: "Joseph Kitheka",
role: "Fullstack Developer",
image: "https://randomuser.me/api/portraits/men/9.jpg",
name: 'Joseph Kitheka',
role: 'Fullstack Developer',
image: 'https://randomuser.me/api/portraits/men/9.jpg',
quote:
"Tailus has transformed the way I develop web applications. Their extensive collection of UI components, blocks, and templates has significantly accelerated my workflow. The flexibility to customize every aspect allows me to create unique user experiences. Tailus is a game-changer for modern web development!",
'Tailus has transformed the way I develop web applications. Their extensive collection of UI components, blocks, and templates has significantly accelerated my workflow. The flexibility to customize every aspect allows me to create unique user experiences. Tailus is a game-changer for modern web development!',
},
{
name: "Khatab Wedaa",
role: "MerakiUI Creator",
image: "https://randomuser.me/api/portraits/men/10.jpg",
name: 'Khatab Wedaa',
role: 'MerakiUI Creator',
image: 'https://randomuser.me/api/portraits/men/10.jpg',
quote:
"Tailus is an elegant, clean, and responsive tailwind css components it's very helpful to start fast with your project.",
},
{
name: "Rodrigo Aguilar",
role: "TailwindAwesome Creator",
image: "https://randomuser.me/api/portraits/men/11.jpg",
name: 'Rodrigo Aguilar',
role: 'TailwindAwesome Creator',
image: 'https://randomuser.me/api/portraits/men/11.jpg',
quote:
"I love Tailus ❤️. The component blocks are well-structured, simple to use, and beautifully designed. It makes it really easy to have a good-looking website in no time.",
'I love Tailus ❤️. The component blocks are well-structured, simple to use, and beautifully designed. It makes it really easy to have a good-looking website in no time.',
},
{
name: "Eric Ampire",
role: "Mobile Engineer at @BRPNews • @GoogleDevExpert for Android",
image: "https://randomuser.me/api/portraits/men/12.jpg",
name: 'Eric Ampire',
role: 'Mobile Engineer at @BRPNews • @GoogleDevExpert for Android',
image: 'https://randomuser.me/api/portraits/men/12.jpg',
quote:
"Tailus templates are the perfect solution for anyone who wants to create a beautiful and functional website without any web design experience. The templates are easy to use, customizable, and responsive, and the support team is always available to help. I highly recommend Tailus templates to anyone who is looking to create a website.",
'Tailus templates are the perfect solution for anyone who wants to create a beautiful and functional website without any web design experience. The templates are easy to use, customizable, and responsive, and the support team is always available to help. I highly recommend Tailus templates to anyone who is looking to create a website.',
},
{
name: "Roland Tubonge",
role: "Software Engineer",
image: "https://randomuser.me/api/portraits/men/13.jpg",
name: 'Roland Tubonge',
role: 'Software Engineer',
image: 'https://randomuser.me/api/portraits/men/13.jpg',
quote:
"Tailus is so well designed that even with a very poor knowledge of web design you can do miracles. Let yourself be seduced!",
'Tailus is so well designed that even with a very poor knowledge of web design you can do miracles. Let yourself be seduced!',
},
];

View File

@ -1,5 +1,5 @@
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Card, CardContent, CardHeader } from '@/components/ui/card';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
/**
* https://nsui.irung.me/testimonials

View File

@ -1,12 +1,12 @@
"use client";
'use client';
import { Button } from "@/components/ui/button";
import { LocaleLink } from "@/i18n/navigation";
import { ArrowLeftIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { Button } from '@/components/ui/button';
import { LocaleLink } from '@/i18n/navigation';
import { ArrowLeftIcon } from 'lucide-react';
import { useTranslations } from 'next-intl';
export default function AllPostsButton() {
const t = useTranslations("BlogPage");
const t = useTranslations('BlogPage');
return (
<Button
size="lg"
@ -15,10 +15,11 @@ export default function AllPostsButton() {
asChild
>
<LocaleLink href="/blog">
<ArrowLeftIcon className="w-5 h-5
<ArrowLeftIcon
className="w-5 h-5
transition-transform duration-200 group-hover:-translate-x-1"
/>
<span>{t("allPosts")}</span>
<span>{t('allPosts')}</span>
</LocaleLink>
</Button>
);

View File

@ -1,8 +1,8 @@
import { Skeleton } from "@/components/ui/skeleton";
import { getLocaleDate } from "@/lib/utils";
import { Post } from "content-collections";
import Image from "next/image";
import { LocaleLink } from "@/i18n/navigation";
import { Skeleton } from '@/components/ui/skeleton';
import { getLocaleDate } from '@/lib/utils';
import { Post } from 'content-collections';
import Image from 'next/image';
import { LocaleLink } from '@/i18n/navigation';
interface BlogCardProps {
post: Post;
@ -11,15 +11,12 @@ interface BlogCardProps {
export default function BlogCard({ post }: BlogCardProps) {
const publishDate = post.date;
const date = getLocaleDate(publishDate);
// Extract the slug parts for the Link component
const slugParts = post.slugAsParams.split('/');
return (
<LocaleLink
href={`/blog/${slugParts.join('/')}`}
className="block h-full"
>
<LocaleLink href={`/blog/${slugParts.join('/')}`} className="block h-full">
<div className="group flex flex-col border border-gray-200 dark:border-gray-800 rounded-lg overflow-hidden h-full">
{/* Image container - fixed aspect ratio */}
<div className="group overflow-hidden relative aspect-[16/9] w-full">
@ -27,8 +24,8 @@ export default function BlogCard({ post }: BlogCardProps) {
<div className="relative w-full h-full">
<Image
src={post.image}
alt={post.title || "image for blog post"}
title={post.title || "image for blog post"}
alt={post.title || 'image for blog post'}
title={post.title || 'image for blog post'}
className="object-cover hover:scale-105 transition-transform duration-300"
fill
/>

View File

@ -1,7 +1,7 @@
import Container from "@/components/container";
import { Category } from "content-collections";
import { BlogCategoryListDesktop } from "./blog-category-list-desktop";
import { BlogCategoryListMobile } from "./blog-category-list-mobile";
import Container from '@/components/container';
import { Category } from 'content-collections';
import { BlogCategoryListDesktop } from './blog-category-list-desktop';
import { BlogCategoryListMobile } from './blog-category-list-mobile';
interface BlogCategoryFilterProps {
categoryList: Category[];

View File

@ -1,11 +1,11 @@
"use client";
'use client';
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import { cn } from "@/lib/utils";
import { Category } from "content-collections";
import { LocaleLink } from "@/i18n/navigation";
import { useParams } from "next/navigation";
import { useTranslations } from "next-intl";
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
import { cn } from '@/lib/utils';
import { Category } from 'content-collections';
import { LocaleLink } from '@/i18n/navigation';
import { useParams } from 'next/navigation';
import { useTranslations } from 'next-intl';
export type BlogCategoryListDesktopProps = {
categoryList: Category[];
@ -15,14 +15,14 @@ export function BlogCategoryListDesktop({
categoryList,
}: BlogCategoryListDesktopProps) {
const { slug } = useParams() as { slug?: string };
const t = useTranslations("BlogPage");
const t = useTranslations('BlogPage');
return (
<div className="flex items-center justify-center">
<ToggleGroup
size="sm"
type="single"
value={slug || "All"}
value={slug || 'All'}
aria-label="Toggle blog category"
className="h-9 overflow-hidden rounded-full border bg-background p-1 *:h-7 *:text-muted-foreground"
>
@ -30,14 +30,14 @@ export function BlogCategoryListDesktop({
key="All"
value="All"
className={cn(
"rounded-full px-5",
"data-[state=on]:bg-primary data-[state=on]:text-primary-foreground",
"hover:bg-muted hover:text-muted-foreground",
'rounded-full px-5',
'data-[state=on]:bg-primary data-[state=on]:text-primary-foreground',
'hover:bg-muted hover:text-muted-foreground'
)}
aria-label={"Toggle all blog categories"}
aria-label={'Toggle all blog categories'}
>
<LocaleLink href={"/blog"}>
<h2>{t("all")}</h2>
<LocaleLink href={'/blog'}>
<h2>{t('all')}</h2>
</LocaleLink>
</ToggleGroupItem>
@ -46,9 +46,9 @@ export function BlogCategoryListDesktop({
key={category.slug}
value={category.slug}
className={cn(
"rounded-full px-5",
"data-[state=on]:bg-primary data-[state=on]:text-primary-foreground",
"hover:bg-muted hover:text-muted-foreground",
'rounded-full px-5',
'data-[state=on]:bg-primary data-[state=on]:text-primary-foreground',
'hover:bg-muted hover:text-muted-foreground'
)}
aria-label={`Toggle blog category of ${category.name}`}
>

View File

@ -1,6 +1,6 @@
"use client";
'use client';
import FilterItemMobile from "@/components/shared/filter-item-mobile";
import FilterItemMobile from '@/components/shared/filter-item-mobile';
import {
Drawer,
DrawerContent,
@ -8,12 +8,12 @@ import {
DrawerPortal,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
import { Category } from "content-collections";
import { LayoutListIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useParams } from "next/navigation";
import { useState } from "react";
} from '@/components/ui/drawer';
import { Category } from 'content-collections';
import { LayoutListIcon } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { useParams } from 'next/navigation';
import { useState } from 'react';
export type BlogCategoryListMobileProps = {
categoryList: Category[];
@ -24,10 +24,10 @@ export function BlogCategoryListMobile({
}: BlogCategoryListMobileProps) {
const { slug } = useParams() as { slug?: string };
const selectedCategory = categoryList.find(
(category) => category.slug === slug,
(category) => category.slug === slug
);
const [open, setOpen] = useState(false);
const t = useTranslations("BlogPage");
const t = useTranslations('BlogPage');
const closeDrawer = () => {
setOpen(false);
@ -42,20 +42,20 @@ export function BlogCategoryListMobile({
<div className="flex items-center justify-between w-full gap-4">
<div className="flex items-center gap-2">
<LayoutListIcon className="size-5" />
<span className="text-sm">{t("categories")}</span>
<span className="text-sm">{t('categories')}</span>
</div>
<span className="text-sm">
{selectedCategory?.name ? `${selectedCategory?.name}` : t("all")}
{selectedCategory?.name ? `${selectedCategory?.name}` : t('all')}
</span>
</div>
</DrawerTrigger>
<DrawerPortal>
<DrawerOverlay className="fixed inset-0 z-40 bg-background/50" />
<DrawerContent className="fixed inset-x-0 bottom-0 z-50 mt-24 overflow-hidden rounded-t-[10px] border bg-background">
<DrawerTitle className="sr-only">{t("categories")}</DrawerTitle>
<DrawerTitle className="sr-only">{t('categories')}</DrawerTitle>
<ul className="mb-14 w-full p-4 text-muted-foreground">
<FilterItemMobile
title={t("all")}
title={t('all')}
href="/blog"
active={!slug}
clickAction={closeDrawer}

View File

@ -1,6 +1,6 @@
import BlogCard, { BlogCardSkeleton } from "@/components/blog/blog-card";
import { POSTS_PER_PAGE } from "@/lib/constants";
import { Post } from "content-collections";
import BlogCard, { BlogCardSkeleton } from '@/components/blog/blog-card';
import { POSTS_PER_PAGE } from '@/lib/constants';
import { Post } from 'content-collections';
interface BlogGridProps {
posts: Post[];

View File

@ -1,9 +1,9 @@
"use client";
'use client';
import { useMounted } from "@/hooks/use-mounted";
import type { TableOfContents } from "@/lib/toc";
import { cn } from "@/lib/utils";
import * as React from "react";
import { useMounted } from '@/hooks/use-mounted';
import type { TableOfContents } from '@/lib/toc';
import { cn } from '@/lib/utils';
import * as React from 'react';
interface TocProps {
toc: TableOfContents;
@ -17,9 +17,9 @@ export function BlogToc({ toc }: TocProps) {
.flatMap((item) => [item.url, item?.items?.map((item) => item.url)])
.flat()
.filter(Boolean)
.map((id) => id?.split("#")[1])
.map((id) => id?.split('#')[1])
: [],
[toc],
[toc]
);
const activeHeading = useActiveItem(itemIds);
const mounted = useMounted();
@ -36,7 +36,7 @@ export function BlogToc({ toc }: TocProps) {
}
function useActiveItem(itemIds: (string | undefined)[]) {
const [activeId, setActiveId] = React.useState<string>("");
const [activeId, setActiveId] = React.useState<string>('');
React.useEffect(() => {
const observer = new IntersectionObserver(
@ -47,7 +47,7 @@ function useActiveItem(itemIds: (string | undefined)[]) {
}
}
},
{ rootMargin: "0% 0% -80% 0%" },
{ rootMargin: '0% 0% -80% 0%' }
);
for (const id of itemIds) {
@ -86,17 +86,17 @@ interface TreeProps {
function Tree({ tree, level = 1, activeItem }: TreeProps) {
return tree?.items?.length && level < 3 ? (
<ul className={cn("m-0 list-none", { "pl-4": level !== 1 })}>
<ul className={cn('m-0 list-none', { 'pl-4': level !== 1 })}>
{tree.items.map((item, index) => {
return (
<li key={index} className={cn("mt-0 pt-1")}>
<li key={index} className={cn('mt-0 pt-1')}>
<a
href={item.url}
className={cn(
"inline-block text-sm no-underline hover:text-primary",
'inline-block text-sm no-underline hover:text-primary',
item.url === `#${activeItem}`
? "font-medium text-primary"
: "text-muted-foreground",
? 'font-medium text-primary'
: 'text-muted-foreground'
)}
>
{item.title}

View File

@ -1,5 +1,5 @@
import { cn } from "@/lib/utils";
import type { ReactNode } from "react";
import { cn } from '@/lib/utils';
import type { ReactNode } from 'react';
export default function Container({
className,
@ -11,7 +11,7 @@ export default function Container({
}) {
// add mx-auto to make it center
return (
<div className={cn("container", "mx-auto max-w-7xl", className)}>
<div className={cn('container', 'mx-auto max-w-7xl', className)}>
{children}
</div>
);

View File

@ -1,18 +1,18 @@
"use client";
'use client';
import { Logo } from "@/components/logo";
import { Button } from "@/components/ui/button";
import { Loader2Icon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { useTransition } from "react";
import { Logo } from '@/components/logo';
import { Button } from '@/components/ui/button';
import { Loader2Icon } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { useRouter } from 'next/navigation';
import { useTransition } from 'react';
/**
* 1. Note that error.tsx is loaded right after your app has initialized.
* If your app is performance-sensitive and you want to avoid loading translation functionality
* 1. Note that error.tsx is loaded right after your app has initialized.
* If your app is performance-sensitive and you want to avoid loading translation functionality
* from next-intl as part of this bundle, you can export a lazy reference from your error file.
* https://next-intl.dev/docs/environments/error-files#errorjs
*
*
* 2. Learned how to recover from a server component error in Next.js from @asidorenko_
* https://x.com/asidorenko_/status/1841547623712407994
*/
@ -41,7 +41,7 @@ export default function Error({ reset }: { reset: () => void }) {
{isPending ? (
<Loader2Icon className="mr-2 size-4 animate-spin" />
) : (
""
''
)}
{t('tryAgain')}
</Button>
@ -49,7 +49,7 @@ export default function Error({ reset }: { reset: () => void }) {
<Button
type="submit"
variant="outline"
onClick={() => router.push("/")}
onClick={() => router.push('/')}
>
{t('backToHome')}
</Button>

View File

@ -1,10 +1,5 @@
import * as React from 'react';
export function FillRemainingSpace(): React.JSX.Element {
return (
<div
aria-hidden="true"
className="flex-1 shrink"
/>
);
return <div aria-hidden="true" className="flex-1 shrink" />;
}

View File

@ -1,5 +1,5 @@
import React from "react";
import type { SVGProps } from "react";
import React from 'react';
import type { SVGProps } from 'react';
/**
* https://icon-sets.iconify.design/fa6-brands/bluesky/

View File

@ -1,14 +1,22 @@
import React from "react";
import type { SVGProps } from "react";
import React from 'react';
import type { SVGProps } from 'react';
/**
* https://icon-sets.iconify.design/fa6-brands/facebook/
*/
export function FacebookIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width={48} height={48} viewBox="0 0 512 512" {...props}>
<path fill="currentColor" d="M512 256C512 114.6 397.4 0 256 0S0 114.6 0 256c0 120 82.7 220.8 194.2 248.5V334.2h-52.8V256h52.8v-33.7c0-87.1 39.4-127.5 125-127.5c16.2 0 44.2 3.2 55.7 6.4V172c-6-.6-16.5-1-29.6-1c-42 0-58.2 15.9-58.2 57.2V256h83.6l-14.4 78.2H287v175.9C413.8 494.8 512 386.9 512 256">
</path>
<svg
xmlns="http://www.w3.org/2000/svg"
width={48}
height={48}
viewBox="0 0 512 512"
{...props}
>
<path
fill="currentColor"
d="M512 256C512 114.6 397.4 0 256 0S0 114.6 0 256c0 120 82.7 220.8 194.2 248.5V334.2h-52.8V256h52.8v-33.7c0-87.1 39.4-127.5 125-127.5c16.2 0 44.2 3.2 55.7 6.4V172c-6-.6-16.5-1-29.6-1c-42 0-58.2 15.9-58.2 57.2V256h83.6l-14.4 78.2H287v175.9C413.8 494.8 512 386.9 512 256"
></path>
</svg>
);
}

View File

@ -1,5 +1,5 @@
import React from "react";
import type { SVGProps } from "react";
import React from 'react';
import type { SVGProps } from 'react';
/**
* https://icon-sets.iconify.design/fa6-brands/github/

View File

@ -1,5 +1,5 @@
import React from "react";
import type { SVGProps } from "react";
import React from 'react';
import type { SVGProps } from 'react';
/**
* https://icon-sets.iconify.design/fa6-brands/google/

View File

@ -44,25 +44,25 @@ import {
VideoIcon,
WandSparklesIcon,
WorkflowIcon,
WrenchIcon
} from "lucide-react";
import { GitHubIcon } from "../icons/github";
import { GoogleIcon } from "../icons/google";
import { ProductHuntIcon } from "../icons/product-hunt";
import { TwitterIcon } from "../icons/twitter";
import { XTwitterIcon } from "../icons/x";
import { BlueskyIcon } from "./bluesky";
import { NextjsIcon } from "./nextjs";
import { ResendIcon } from "./resend";
import { ShadcnuiIcon } from "./shadcnui";
import { StripeIcon } from "./stripe";
import { TailwindcssIcon } from "./tailwindcss";
import { VercelIcon } from "./vercel";
import { YouTubeIcon } from "./youtube";
import { TikTokIcon } from "./tiktok";
import { LinkedInIcon } from "./linkedin";
import { InstagramIcon } from "./instagram";
import { FacebookIcon } from "./facebook";
WrenchIcon,
} from 'lucide-react';
import { GitHubIcon } from '../icons/github';
import { GoogleIcon } from '../icons/google';
import { ProductHuntIcon } from '../icons/product-hunt';
import { TwitterIcon } from '../icons/twitter';
import { XTwitterIcon } from '../icons/x';
import { BlueskyIcon } from './bluesky';
import { NextjsIcon } from './nextjs';
import { ResendIcon } from './resend';
import { ShadcnuiIcon } from './shadcnui';
import { StripeIcon } from './stripe';
import { TailwindcssIcon } from './tailwindcss';
import { VercelIcon } from './vercel';
import { YouTubeIcon } from './youtube';
import { TikTokIcon } from './tiktok';
import { LinkedInIcon } from './linkedin';
import { InstagramIcon } from './instagram';
import { FacebookIcon } from './facebook';
export type Icon = LucideIcon;

View File

@ -1,14 +1,22 @@
import React from "react";
import type { SVGProps } from "react";
import React from 'react';
import type { SVGProps } from 'react';
/**
* https://icon-sets.iconify.design/fa6-brands/instagram/
*/
export function InstagramIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width={42} height={48} viewBox="0 0 448 512" {...props}>
<path fill="currentColor" d="M224.1 141c-63.6 0-114.9 51.3-114.9 114.9s51.3 114.9 114.9 114.9S339 319.5 339 255.9S287.7 141 224.1 141m0 189.6c-41.1 0-74.7-33.5-74.7-74.7s33.5-74.7 74.7-74.7s74.7 33.5 74.7 74.7s-33.6 74.7-74.7 74.7m146.4-194.3c0 14.9-12 26.8-26.8 26.8c-14.9 0-26.8-12-26.8-26.8s12-26.8 26.8-26.8s26.8 12 26.8 26.8m76.1 27.2c-1.7-35.9-9.9-67.7-36.2-93.9c-26.2-26.2-58-34.4-93.9-36.2c-37-2.1-147.9-2.1-184.9 0c-35.8 1.7-67.6 9.9-93.9 36.1s-34.4 58-36.2 93.9c-2.1 37-2.1 147.9 0 184.9c1.7 35.9 9.9 67.7 36.2 93.9s58 34.4 93.9 36.2c37 2.1 147.9 2.1 184.9 0c35.9-1.7 67.7-9.9 93.9-36.2c26.2-26.2 34.4-58 36.2-93.9c2.1-37 2.1-147.8 0-184.8M398.8 388c-7.8 19.6-22.9 34.7-42.6 42.6c-29.5 11.7-99.5 9-132.1 9s-102.7 2.6-132.1-9c-19.6-7.8-34.7-22.9-42.6-42.6c-11.7-29.5-9-99.5-9-132.1s-2.6-102.7 9-132.1c7.8-19.6 22.9-34.7 42.6-42.6c29.5-11.7 99.5-9 132.1-9s102.7-2.6 132.1 9c19.6 7.8 34.7 22.9 42.6 42.6c11.7 29.5 9 99.5 9 132.1s2.7 102.7-9 132.1">
</path>
<svg
xmlns="http://www.w3.org/2000/svg"
width={42}
height={48}
viewBox="0 0 448 512"
{...props}
>
<path
fill="currentColor"
d="M224.1 141c-63.6 0-114.9 51.3-114.9 114.9s51.3 114.9 114.9 114.9S339 319.5 339 255.9S287.7 141 224.1 141m0 189.6c-41.1 0-74.7-33.5-74.7-74.7s33.5-74.7 74.7-74.7s74.7 33.5 74.7 74.7s-33.6 74.7-74.7 74.7m146.4-194.3c0 14.9-12 26.8-26.8 26.8c-14.9 0-26.8-12-26.8-26.8s12-26.8 26.8-26.8s26.8 12 26.8 26.8m76.1 27.2c-1.7-35.9-9.9-67.7-36.2-93.9c-26.2-26.2-58-34.4-93.9-36.2c-37-2.1-147.9-2.1-184.9 0c-35.8 1.7-67.6 9.9-93.9 36.1s-34.4 58-36.2 93.9c-2.1 37-2.1 147.9 0 184.9c1.7 35.9 9.9 67.7 36.2 93.9s58 34.4 93.9 36.2c37 2.1 147.9 2.1 184.9 0c35.9-1.7 67.7-9.9 93.9-36.2c26.2-26.2 34.4-58 36.2-93.9c2.1-37 2.1-147.8 0-184.8M398.8 388c-7.8 19.6-22.9 34.7-42.6 42.6c-29.5 11.7-99.5 9-132.1 9s-102.7 2.6-132.1-9c-19.6-7.8-34.7-22.9-42.6-42.6c-11.7-29.5-9-99.5-9-132.1s-2.6-102.7 9-132.1c7.8-19.6 22.9-34.7 42.6-42.6c29.5-11.7 99.5-9 132.1-9s102.7-2.6 132.1 9c19.6 7.8 34.7 22.9 42.6 42.6c11.7 29.5 9 99.5 9 132.1s2.7 102.7-9 132.1"
></path>
</svg>
);
}

View File

@ -1,14 +1,22 @@
import React from "react";
import type { SVGProps } from "react";
import React from 'react';
import type { SVGProps } from 'react';
/**
* https://icon-sets.iconify.design/fa6-brands/linkedin/
*/
export function LinkedInIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width={42} height={48} viewBox="0 0 448 512" {...props}>
<path fill="currentColor" d="M416 32H31.9C14.3 32 0 46.5 0 64.3v383.4C0 465.5 14.3 480 31.9 480H416c17.6 0 32-14.5 32-32.3V64.3c0-17.8-14.4-32.3-32-32.3M135.4 416H69V202.2h66.5V416zm-33.2-243c-21.3 0-38.5-17.3-38.5-38.5S80.9 96 102.2 96c21.2 0 38.5 17.3 38.5 38.5c0 21.3-17.2 38.5-38.5 38.5m282.1 243h-66.4V312c0-24.8-.5-56.7-34.5-56.7c-34.6 0-39.9 27-39.9 54.9V416h-66.4V202.2h63.7v29.2h.9c8.9-16.8 30.6-34.5 62.9-34.5c67.2 0 79.7 44.3 79.7 101.9z">
</path>
<svg
xmlns="http://www.w3.org/2000/svg"
width={42}
height={48}
viewBox="0 0 448 512"
{...props}
>
<path
fill="currentColor"
d="M416 32H31.9C14.3 32 0 46.5 0 64.3v383.4C0 465.5 14.3 480 31.9 480H416c17.6 0 32-14.5 32-32.3V64.3c0-17.8-14.4-32.3-32-32.3M135.4 416H69V202.2h66.5V416zm-33.2-243c-21.3 0-38.5-17.3-38.5-38.5S80.9 96 102.2 96c21.2 0 38.5 17.3 38.5 38.5c0 21.3-17.2 38.5-38.5 38.5m282.1 243h-66.4V312c0-24.8-.5-56.7-34.5-56.7c-34.6 0-39.9 27-39.9 54.9V416h-66.4V202.2h63.7v29.2h.9c8.9-16.8 30.6-34.5 62.9-34.5c67.2 0 79.7 44.3 79.7 101.9z"
></path>
</svg>
);
}

View File

@ -1,5 +1,5 @@
import * as React from "react";
import type { SVGProps } from "react";
import * as React from 'react';
import type { SVGProps } from 'react';
/**
* https://svgl.app/
@ -8,71 +8,71 @@ export function NextjsIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
width="1em"
height="1em"
viewBox="0 0 180 180"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
height="1em"
viewBox="0 0 180 180"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<title>Next.js</title>
<mask
id="mask0_408_139"
style={{
maskType: "alpha",
}}
maskUnits="userSpaceOnUse"
x={0}
y={0}
width={180}
height={180}
>
<circle cx={90} cy={90} r={90} fill="black" />
</mask>
<g mask="url(#mask0_408_139)">
<circle
cx={90}
cy={90}
r={87}
fill="black"
stroke="white"
strokeWidth={6}
/>
<path
d="M149.508 157.52L69.142 54H54V125.97H66.1136V69.3836L139.999 164.845C143.333 162.614 146.509 160.165 149.508 157.52Z"
fill="url(#paint0_linear_408_139)"
/>
<rect
x={115}
y={54}
width={12}
height={72}
fill="url(#paint1_linear_408_139)"
/>
</g>
<defs>
<linearGradient
id="paint0_linear_408_139"
x1={109}
y1={116.5}
x2={144.5}
y2={160.5}
gradientUnits="userSpaceOnUse"
style={{
maskType: 'alpha',
}}
maskUnits="userSpaceOnUse"
x={0}
y={0}
width={180}
height={180}
>
<stop stopColor="white" />
<stop offset={1} stopColor="white" stopOpacity={0} />
</linearGradient>
<linearGradient
id="paint1_linear_408_139"
x1={121}
y1={54}
x2={120.799}
y2={106.875}
gradientUnits="userSpaceOnUse"
>
<stop stopColor="white" />
<stop offset={1} stopColor="white" stopOpacity={0} />
</linearGradient>
</defs>
</svg>
<circle cx={90} cy={90} r={90} fill="black" />
</mask>
<g mask="url(#mask0_408_139)">
<circle
cx={90}
cy={90}
r={87}
fill="black"
stroke="white"
strokeWidth={6}
/>
<path
d="M149.508 157.52L69.142 54H54V125.97H66.1136V69.3836L139.999 164.845C143.333 162.614 146.509 160.165 149.508 157.52Z"
fill="url(#paint0_linear_408_139)"
/>
<rect
x={115}
y={54}
width={12}
height={72}
fill="url(#paint1_linear_408_139)"
/>
</g>
<defs>
<linearGradient
id="paint0_linear_408_139"
x1={109}
y1={116.5}
x2={144.5}
y2={160.5}
gradientUnits="userSpaceOnUse"
>
<stop stopColor="white" />
<stop offset={1} stopColor="white" stopOpacity={0} />
</linearGradient>
<linearGradient
id="paint1_linear_408_139"
x1={121}
y1={54}
x2={120.799}
y2={106.875}
gradientUnits="userSpaceOnUse"
>
<stop stopColor="white" />
<stop offset={1} stopColor="white" stopOpacity={0} />
</linearGradient>
</defs>
</svg>
);
}

View File

@ -1,4 +1,4 @@
import type { SVGProps } from "react";
import type { SVGProps } from 'react';
/**
* https://icon-sets.iconify.design/logos/producthunt/

View File

@ -1,5 +1,5 @@
import React from "react";
import type { SVGProps } from "react";
import React from 'react';
import type { SVGProps } from 'react';
/**
* https://icon-sets.iconify.design/simple-icons/resend/

View File

@ -1,5 +1,5 @@
import React from "react";
import type { SVGProps } from "react";
import React from 'react';
import type { SVGProps } from 'react';
/**
* https://icon-sets.iconify.design/simple-icons/shadcnui/

View File

@ -1,5 +1,5 @@
import React from "react";
import type { SVGProps } from "react";
import React from 'react';
import type { SVGProps } from 'react';
/**
* https://icon-sets.iconify.design/simple-icons/stripe/

View File

@ -1,5 +1,5 @@
import React from "react";
import type { SVGProps } from "react";
import React from 'react';
import type { SVGProps } from 'react';
/**
* https://icon-sets.iconify.design/simple-icons/tailwindcss/

View File

@ -1,14 +1,22 @@
import React from "react";
import type { SVGProps } from "react";
import React from 'react';
import type { SVGProps } from 'react';
/**
* https://icon-sets.iconify.design/fa6-brands/tiktok/
*/
export function TikTokIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width={42} height={48} viewBox="0 0 448 512" {...props}>
<path fill="currentColor" d="M448 209.91a210.06 210.06 0 0 1-122.77-39.25v178.72A162.55 162.55 0 1 1 185 188.31v89.89a74.62 74.62 0 1 0 52.23 71.18V0h88a121 121 0 0 0 1.86 22.17A122.18 122.18 0 0 0 381 102.39a121.43 121.43 0 0 0 67 20.14Z">
</path>
<svg
xmlns="http://www.w3.org/2000/svg"
width={42}
height={48}
viewBox="0 0 448 512"
{...props}
>
<path
fill="currentColor"
d="M448 209.91a210.06 210.06 0 0 1-122.77-39.25v178.72A162.55 162.55 0 1 1 185 188.31v89.89a74.62 74.62 0 1 0 52.23 71.18V0h88a121 121 0 0 0 1.86 22.17A122.18 122.18 0 0 0 381 102.39a121.43 121.43 0 0 0 67 20.14Z"
></path>
</svg>
);
}

View File

@ -1,5 +1,5 @@
import React from "react";
import type { SVGProps } from "react";
import React from 'react';
import type { SVGProps } from 'react';
/**
* https://icon-sets.iconify.design/fa6-brands/twitter/
@ -13,8 +13,10 @@ export function TwitterIcon(props: SVGProps<SVGSVGElement>) {
viewBox="0 0 512 512"
{...props}
>
<path fill="currentColor" d="M459.37 151.716c.325 4.548.325 9.097.325 13.645c0 138.72-105.583 298.558-298.558 298.558c-59.452 0-114.68-17.219-161.137-47.106c8.447.974 16.568 1.299 25.34 1.299c49.055 0 94.213-16.568 130.274-44.832c-46.132-.975-84.792-31.188-98.112-72.772c6.498.974 12.995 1.624 19.818 1.624c9.421 0 18.843-1.3 27.614-3.573c-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319c-28.264-18.843-46.781-51.005-46.781-87.391c0-19.492 5.197-37.36 14.294-52.954c51.655 63.675 129.3 105.258 216.365 109.807c-1.624-7.797-2.599-15.918-2.599-24.04c0-57.828 46.782-104.934 104.934-104.934c30.213 0 57.502 12.67 76.67 33.137c23.715-4.548 46.456-13.32 66.599-25.34c-7.798 24.366-24.366 44.833-46.132 57.827c21.117-2.273 41.584-8.122 60.426-16.243c-14.292 20.791-32.161 39.308-52.628 54.253">
</path>
<path
fill="currentColor"
d="M459.37 151.716c.325 4.548.325 9.097.325 13.645c0 138.72-105.583 298.558-298.558 298.558c-59.452 0-114.68-17.219-161.137-47.106c8.447.974 16.568 1.299 25.34 1.299c49.055 0 94.213-16.568 130.274-44.832c-46.132-.975-84.792-31.188-98.112-72.772c6.498.974 12.995 1.624 19.818 1.624c9.421 0 18.843-1.3 27.614-3.573c-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319c-28.264-18.843-46.781-51.005-46.781-87.391c0-19.492 5.197-37.36 14.294-52.954c51.655 63.675 129.3 105.258 216.365 109.807c-1.624-7.797-2.599-15.918-2.599-24.04c0-57.828 46.782-104.934 104.934-104.934c30.213 0 57.502 12.67 76.67 33.137c23.715-4.548 46.456-13.32 66.599-25.34c-7.798 24.366-24.366 44.833-46.132 57.827c21.117-2.273 41.584-8.122 60.426-16.243c-14.292 20.791-32.161 39.308-52.628 54.253"
></path>
</svg>
);
}

View File

@ -1,5 +1,5 @@
import React from "react";
import type { SVGProps } from "react";
import React from 'react';
import type { SVGProps } from 'react';
/**
* https://icon-sets.iconify.design/simple-icons/vercel/

View File

@ -1,5 +1,5 @@
import React from "react";
import type { SVGProps } from "react";
import React from 'react';
import type { SVGProps } from 'react';
/**
* https://icon-sets.iconify.design/fa6-brands/x-twitter/

View File

@ -1,5 +1,5 @@
import React from "react";
import type { SVGProps } from "react";
import React from 'react';
import type { SVGProps } from 'react';
/**
* https://icon-sets.iconify.design/ion/logo-youtube/

View File

@ -1,15 +1,19 @@
"use client";
'use client';
import Container from "@/components/container";
import { ThemeSwitcherHorizontal } from "@/components/layout/theme-switcher-horizontal";
import { Logo } from "@/components/logo";
import BuiltWithButton from "@/components/shared/built-with-button";
import { createTranslator, getFooterLinks, getSocialLinks } from "@/config/marketing";
import { siteConfig } from "@/config/site";
import { LocaleLink } from "@/i18n/navigation";
import { cn } from "@/lib/utils";
import { useTranslations } from "next-intl";
import React from "react";
import Container from '@/components/container';
import { ThemeSwitcherHorizontal } from '@/components/layout/theme-switcher-horizontal';
import { Logo } from '@/components/logo';
import BuiltWithButton from '@/components/shared/built-with-button';
import {
createTranslator,
getFooterLinks,
getSocialLinks,
} from '@/config/marketing';
import { siteConfig } from '@/config/site';
import { LocaleLink } from '@/i18n/navigation';
import { cn } from '@/lib/utils';
import { useTranslations } from 'next-intl';
import React from 'react';
export function Footer({ className }: React.HTMLAttributes<HTMLElement>) {
const t = useTranslations();
@ -18,7 +22,7 @@ export function Footer({ className }: React.HTMLAttributes<HTMLElement>) {
const socialLinks = getSocialLinks();
return (
<footer className={cn("border-t", className)}>
<footer className={cn('border-t', className)}>
<Container className="px-4">
<div className="grid grid-cols-2 gap-8 py-16 md:grid-cols-6">
<div className="flex flex-col items-start col-span-full md:col-span-2">
@ -37,19 +41,20 @@ export function Footer({ className }: React.HTMLAttributes<HTMLElement>) {
{/* social links */}
<div className="flex items-center gap-4 py-2">
<div className="flex items-center gap-2">
{socialLinks && socialLinks.map((link) => (
<a
key={link.title}
href={link.href || "#"}
target="_blank"
rel="noreferrer"
aria-label={link.title}
className="border border-border inline-flex h-8 w-8 items-center justify-center rounded-full hover:bg-accent hover:text-accent-foreground"
>
<span className="sr-only">{link.title}</span>
{link.icon ? link.icon : null}
</a>
))}
{socialLinks &&
socialLinks.map((link) => (
<a
key={link.title}
href={link.href || '#'}
target="_blank"
rel="noreferrer"
aria-label={link.title}
className="border border-border inline-flex h-8 w-8 items-center justify-center rounded-full hover:bg-accent hover:text-accent-foreground"
>
<span className="sr-only">{link.title}</span>
{link.icon ? link.icon : null}
</a>
))}
</div>
</div>
@ -59,39 +64,41 @@ export function Footer({ className }: React.HTMLAttributes<HTMLElement>) {
</div>
{/* footer links */}
{footerLinks && footerLinks.map((section) => (
<div
key={section.title}
className="col-span-1 md:col-span-1 items-start"
>
<span className="text-sm font-semibold uppercase">
{section.title}
</span>
<ul className="mt-4 list-inside space-y-3">
{section.items?.map(
(item) =>
item.href && (
<li key={item.title}>
<LocaleLink
href={item.href || "#"}
target={item.external ? "_blank" : undefined}
className="text-sm text-muted-foreground hover:text-primary"
>
{item.title}
</LocaleLink>
</li>
),
)}
</ul>
</div>
))}
{footerLinks &&
footerLinks.map((section) => (
<div
key={section.title}
className="col-span-1 md:col-span-1 items-start"
>
<span className="text-sm font-semibold uppercase">
{section.title}
</span>
<ul className="mt-4 list-inside space-y-3">
{section.items?.map(
(item) =>
item.href && (
<li key={item.title}>
<LocaleLink
href={item.href || '#'}
target={item.external ? '_blank' : undefined}
className="text-sm text-muted-foreground hover:text-primary"
>
{item.title}
</LocaleLink>
</li>
)
)}
</ul>
</div>
))}
</div>
</Container>
<div className="border-t py-8">
<Container className="px-4 flex items-center justify-between">
<span className="text-muted-foreground text-sm">
&copy; {new Date().getFullYear()} {siteConfig.name} All Rights Reserved.
&copy; {new Date().getFullYear()} {siteConfig.name} All Rights
Reserved.
</span>
<ThemeSwitcherHorizontal />

View File

@ -1,4 +1,4 @@
"use client";
'use client';
import {
Select,
@ -6,30 +6,27 @@ import {
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { useLocalePathname, useLocaleRouter } from "@/i18n/navigation";
import {
DEFAULT_LOCALE,
Locale,
LOCALE_LIST,
routing,
} from "@/i18n/routing";
import { useLocaleStore } from "@/stores/locale-store";
import { useLocale } from "next-intl";
import { useParams } from "next/navigation";
import { useEffect, useTransition } from "react";
} from '@/components/ui/select';
import { useLocalePathname, useLocaleRouter } from '@/i18n/navigation';
import { DEFAULT_LOCALE, Locale, LOCALE_LIST, routing } from '@/i18n/routing';
import { useLocaleStore } from '@/stores/locale-store';
import { useLocale } from 'next-intl';
import { useParams } from 'next/navigation';
import { useEffect, useTransition } from 'react';
/**
* 1. LocaleSelector
*
* By combining usePathname with useRouter, you can change the locale for the current page
*
* By combining usePathname with useRouter, you can change the locale for the current page
* programmatically by navigating to the same pathname, while overriding the locale.
* Depending on if you're using the pathnames setting, you optionally have to forward params
* Depending on if you're using the pathnames setting, you optionally have to forward params
* to potentially resolve an internal pathname.
*
*
* https://next-intl.dev/docs/routing/navigation#userouter
*/
export default function LocaleSelector({showLocaleName = true}: {showLocaleName?: boolean}) {
export default function LocaleSelector({
showLocaleName = true,
}: { showLocaleName?: boolean }) {
const router = useLocaleRouter();
const pathname = useLocalePathname();
const params = useParams();
@ -62,7 +59,11 @@ export default function LocaleSelector({showLocaleName = true}: {showLocaleName?
onValueChange={onSelectChange}
>
<SelectTrigger className="w-fit">
<SelectValue placeholder={<span className="text-lg">{LOCALE_LIST[DEFAULT_LOCALE].flag}</span>}>
<SelectValue
placeholder={
<span className="text-lg">{LOCALE_LIST[DEFAULT_LOCALE].flag}</span>
}
>
{currentLocale && (
<div className="flex items-center gap-2">
<span className="text-lg">{LOCALE_LIST[currentLocale].flag}</span>
@ -73,7 +74,11 @@ export default function LocaleSelector({showLocaleName = true}: {showLocaleName?
</SelectTrigger>
<SelectContent>
{routing.locales.map((cur) => (
<SelectItem key={cur} value={cur} className="cursor-pointer flex items-center gap-2">
<SelectItem
key={cur}
value={cur}
className="cursor-pointer flex items-center gap-2"
>
<div className="flex items-center gap-2">
<span className="text-md">{LOCALE_LIST[cur].flag}</span>
<span>{LOCALE_LIST[cur].name}</span>
@ -83,4 +88,4 @@ export default function LocaleSelector({showLocaleName = true}: {showLocaleName?
</SelectContent>
</Select>
);
}
}

View File

@ -7,7 +7,7 @@ import { Button, buttonVariants } from '@/components/ui/button';
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger
CollapsibleTrigger,
} from '@/components/ui/collapsible';
import { createTranslator, getMenuLinks } from '@/config/marketing';
import { siteConfig } from '@/config/site';
@ -21,9 +21,9 @@ import {
ChevronDownIcon,
ChevronUpIcon,
MenuIcon,
XIcon
XIcon,
} from 'lucide-react';
import { useTranslations } from "next-intl";
import { useTranslations } from 'next-intl';
import * as React from 'react';
import { RemoveScroll } from 'react-remove-scroll';
import { UserButton } from './user-button';
@ -106,7 +106,10 @@ export function NavbarMobile({
{/* if we don't add RemoveScroll component, the underlying
page will scroll when we scroll the mobile menu */}
<RemoveScroll allowPinchZoom enabled>
<MainMobileMenu userLoggedIn={!!user} onLinkClicked={handleToggleMobileMenu} />
<MainMobileMenu
userLoggedIn={!!user}
onLinkClicked={handleToggleMobileMenu}
/>
</RemoveScroll>
</Portal>
)}
@ -124,12 +127,14 @@ function MainMobileMenu({ userLoggedIn, onLinkClicked }: MainMobileMenuProps) {
const t = useTranslations();
const translator = createTranslator(t);
const menuLinks = getMenuLinks(translator);
const commonTranslations = useTranslations("Common");
const commonTranslations = useTranslations('Common');
const localePathname = useLocalePathname();
return (
<div className="fixed w-full inset-0 z-50 mt-[72px] overflow-y-auto
bg-background backdrop-blur-md animate-in fade-in-0">
<div
className="fixed w-full inset-0 z-50 mt-[72px] overflow-y-auto
bg-background backdrop-blur-md animate-in fade-in-0"
>
<div className="size-full flex flex-col items-start space-y-4 p-4">
{/* action buttons */}
{userLoggedIn ? null : (
@ -140,154 +145,173 @@ function MainMobileMenu({ userLoggedIn, onLinkClicked }: MainMobileMenuProps) {
className={cn(
buttonVariants({
variant: 'outline',
size: 'lg'
size: 'lg',
}),
'w-full'
)}
>
{commonTranslations("login")}
{commonTranslations('login')}
</LocaleLink>
<LocaleLink
href={Routes.Register}
className={cn(
buttonVariants({
variant: 'default',
size: 'lg'
size: 'lg',
}),
'w-full'
)}
onClick={onLinkClicked}
>
{commonTranslations("signUp")}
{commonTranslations('signUp')}
</LocaleLink>
</div>
)}
{/* main menu */}
<ul className="w-full">
{menuLinks && menuLinks.map((item) => {
const isActive = item.href ? localePathname.startsWith(item.href) :
item.items?.some(subItem =>
subItem.href && localePathname.startsWith(subItem.href)
);
{menuLinks &&
menuLinks.map((item) => {
const isActive = item.href
? localePathname.startsWith(item.href)
: item.items?.some(
(subItem) =>
subItem.href && localePathname.startsWith(subItem.href)
);
return (
<li key={item.title} className="py-2">
{item.items ? (
<Collapsible
open={expanded[item.title.toLowerCase()]}
onOpenChange={(isOpen) =>
setExpanded((prev) => ({
...prev,
[item.title.toLowerCase()]: isOpen
}))
}
>
<CollapsibleTrigger asChild>
<Button
type="button"
variant="ghost"
className={cn(
"flex w-full items-center justify-between text-left",
"bg-transparent text-muted-foreground",
"hover:bg-transparent hover:text-primary focus:bg-transparent focus:text-primary",
isActive && "font-semibold bg-transparent text-primary"
)}
>
<span className="text-base">
{item.title}
</span>
{expanded[item.title.toLowerCase()] ? (
<ChevronUpIcon className="size-4" />
) : (
<ChevronDownIcon className="size-4" />
)}
</Button>
</CollapsibleTrigger>
<CollapsibleContent>
<ul className="mt-2 pl-4 space-y-2">
{item.items.map((subItem) => {
const isSubItemActive = subItem.href && localePathname.startsWith(subItem.href);
return (
<li key={item.title} className="py-2">
{item.items ? (
<Collapsible
open={expanded[item.title.toLowerCase()]}
onOpenChange={(isOpen) =>
setExpanded((prev) => ({
...prev,
[item.title.toLowerCase()]: isOpen,
}))
}
>
<CollapsibleTrigger asChild>
<Button
type="button"
variant="ghost"
className={cn(
'flex w-full items-center justify-between text-left',
'bg-transparent text-muted-foreground',
'hover:bg-transparent hover:text-primary focus:bg-transparent focus:text-primary',
isActive &&
'font-semibold bg-transparent text-primary'
)}
>
<span className="text-base">{item.title}</span>
{expanded[item.title.toLowerCase()] ? (
<ChevronUpIcon className="size-4" />
) : (
<ChevronDownIcon className="size-4" />
)}
</Button>
</CollapsibleTrigger>
<CollapsibleContent>
<ul className="mt-2 pl-4 space-y-2">
{item.items.map((subItem) => {
const isSubItemActive =
subItem.href &&
localePathname.startsWith(subItem.href);
return (
<li key={subItem.title}>
<LocaleLink
href={subItem.href || '#'}
target={subItem.external ? '_blank' : undefined}
rel={
subItem.external
? 'noopener noreferrer'
: undefined
}
className={cn(
buttonVariants({ variant: 'ghost' }),
'group h-auto w-full justify-start gap-4 p-2',
"bg-transparent text-muted-foreground",
"hover:bg-transparent hover:text-primary focus:bg-transparent focus:text-primary",
isSubItemActive && "font-semibold bg-transparent text-primary"
)}
onClick={onLinkClicked}
>
<div className={cn(
"flex size-8 shrink-0 items-center justify-center transition-colors",
"bg-transparent text-muted-foreground",
"group-hover:bg-transparent group-hover:text-primary group-focus:bg-transparent group-focus:text-primary",
isSubItemActive && "bg-transparent text-primary"
)}>
{subItem.icon ? subItem.icon : null}
</div>
<div className="flex-1">
<span className={cn(
"text-sm text-muted-foreground",
"group-hover:bg-transparent group-hover:text-primary group-focus:bg-transparent group-focus:text-primary",
isSubItemActive && "font-semibold bg-transparent text-primary"
)}>
{subItem.title}
</span>
{subItem.description && (
<p className={cn(
"text-xs text-muted-foreground",
"group-hover:bg-transparent group-hover:text-primary/80 group-focus:bg-transparent group-focus:text-primary/80",
isSubItemActive && "bg-transparent text-primary/80"
)}>
{subItem.description}
</p>
return (
<li key={subItem.title}>
<LocaleLink
href={subItem.href || '#'}
target={
subItem.external ? '_blank' : undefined
}
rel={
subItem.external
? 'noopener noreferrer'
: undefined
}
className={cn(
buttonVariants({ variant: 'ghost' }),
'group h-auto w-full justify-start gap-4 p-2',
'bg-transparent text-muted-foreground',
'hover:bg-transparent hover:text-primary focus:bg-transparent focus:text-primary',
isSubItemActive &&
'font-semibold bg-transparent text-primary'
)}
</div>
{subItem.external && (
<ArrowUpRightIcon className={cn(
"size-4 shrink-0 text-muted-foreground",
"group-hover:bg-transparent group-hover:text-primary group-focus:bg-transparent group-focus:text-primary",
isSubItemActive && "bg-transparent text-primary"
)} />
)}
</LocaleLink>
</li>
);
})}
</ul>
</CollapsibleContent>
</Collapsible>
) : (
<LocaleLink
href={item.href || '#'}
target={item.external ? '_blank' : undefined}
rel={item.external ? 'noopener noreferrer' : undefined}
className={cn(
buttonVariants({ variant: 'ghost' }),
'w-full justify-start',
"bg-transparent text-muted-foreground",
"hover:bg-transparent hover:text-primary focus:bg-transparent focus:text-primary",
isActive && "font-semibold bg-transparent text-primary"
)}
onClick={onLinkClicked}
>
<span className="text-base">{item.title}</span>
</LocaleLink>
)}
</li>
);
})}
onClick={onLinkClicked}
>
<div
className={cn(
'flex size-8 shrink-0 items-center justify-center transition-colors',
'bg-transparent text-muted-foreground',
'group-hover:bg-transparent group-hover:text-primary group-focus:bg-transparent group-focus:text-primary',
isSubItemActive &&
'bg-transparent text-primary'
)}
>
{subItem.icon ? subItem.icon : null}
</div>
<div className="flex-1">
<span
className={cn(
'text-sm text-muted-foreground',
'group-hover:bg-transparent group-hover:text-primary group-focus:bg-transparent group-focus:text-primary',
isSubItemActive &&
'font-semibold bg-transparent text-primary'
)}
>
{subItem.title}
</span>
{subItem.description && (
<p
className={cn(
'text-xs text-muted-foreground',
'group-hover:bg-transparent group-hover:text-primary/80 group-focus:bg-transparent group-focus:text-primary/80',
isSubItemActive &&
'bg-transparent text-primary/80'
)}
>
{subItem.description}
</p>
)}
</div>
{subItem.external && (
<ArrowUpRightIcon
className={cn(
'size-4 shrink-0 text-muted-foreground',
'group-hover:bg-transparent group-hover:text-primary group-focus:bg-transparent group-focus:text-primary',
isSubItemActive &&
'bg-transparent text-primary'
)}
/>
)}
</LocaleLink>
</li>
);
})}
</ul>
</CollapsibleContent>
</Collapsible>
) : (
<LocaleLink
href={item.href || '#'}
target={item.external ? '_blank' : undefined}
rel={item.external ? 'noopener noreferrer' : undefined}
className={cn(
buttonVariants({ variant: 'ghost' }),
'w-full justify-start',
'bg-transparent text-muted-foreground',
'hover:bg-transparent hover:text-primary focus:bg-transparent focus:text-primary',
isActive && 'font-semibold bg-transparent text-primary'
)}
onClick={onLinkClicked}
>
<span className="text-base">{item.title}</span>
</LocaleLink>
)}
</li>
);
})}
</ul>
{/* bottom buttons */}

View File

@ -15,17 +15,17 @@ import {
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
navigationMenuTriggerStyle
navigationMenuTriggerStyle,
} from '@/components/ui/navigation-menu';
import { createTranslator, getMenuLinks } from '@/config/marketing';
import { siteConfig } from '@/config/site';
import { useScroll } from "@/hooks/use-scroll";
import { useScroll } from '@/hooks/use-scroll';
import { LocaleLink, useLocalePathname } from '@/i18n/navigation';
import { authClient } from '@/lib/auth-client';
import { cn } from '@/lib/utils';
import { Routes } from '@/routes';
import { ArrowUpRightIcon } from 'lucide-react';
import { useTranslations } from "next-intl";
import { useTranslations } from 'next-intl';
interface NavBarProps {
scroll?: boolean;
@ -33,10 +33,10 @@ interface NavBarProps {
const customNavigationMenuTriggerStyle = cn(
navigationMenuTriggerStyle(),
"relative bg-transparent text-muted-foreground",
"hover:bg-transparent hover:text-primary focus:bg-transparent focus:text-primary",
"data-[active]:font-semibold data-[active]:bg-transparent data-[active]:text-primary",
"data-[state=open]:bg-transparent data-[state=open]:text-primary",
'relative bg-transparent text-muted-foreground',
'hover:bg-transparent hover:text-primary focus:bg-transparent focus:text-primary',
'data-[active]:font-semibold data-[active]:bg-transparent data-[active]:text-primary',
'data-[state=open]:bg-transparent data-[state=open]:text-primary'
);
export function Navbar({ scroll }: NavBarProps) {
@ -46,20 +46,22 @@ export function Navbar({ scroll }: NavBarProps) {
const t = useTranslations();
const translator = createTranslator(t);
const menuLinks = getMenuLinks(translator);
const commonTranslations = useTranslations("Common");
const commonTranslations = useTranslations('Common');
const localePathname = useLocalePathname();
// console.log(`Navbar, user:`, user);
return (
<section className={cn(
"sticky inset-x-0 top-0 z-40 py-4 transition-all duration-300",
scroll ? (
scrolled
? "bg-background/80 backdrop-blur-md border-b supports-[backdrop-filter]:bg-background/60"
: "bg-background"
) : "border-b bg-background"
)}>
<section
className={cn(
'sticky inset-x-0 top-0 z-40 py-4 transition-all duration-300',
scroll
? scrolled
? 'bg-background/80 backdrop-blur-md border-b supports-[backdrop-filter]:bg-background/60'
: 'bg-background'
: 'border-b bg-background'
)}
>
<Container className="px-4">
{/* desktop navbar */}
<nav className="hidden lg:flex">
@ -75,100 +77,128 @@ export function Navbar({ scroll }: NavBarProps) {
<div className="flex-1 flex items-center justify-center space-x-2">
<NavigationMenu className="relative">
<NavigationMenuList className="flex items-center">
{menuLinks && menuLinks.map((item, index) =>
item.items ? (
<NavigationMenuItem key={index} className="relative">
<NavigationMenuTrigger
data-active={
item.items.some((subItem) =>
subItem.href ? localePathname.startsWith(subItem.href) : false
) ? "true" : undefined
}
className={customNavigationMenuTriggerStyle}
>
{item.title}
</NavigationMenuTrigger>
<NavigationMenuContent>
<ul className="grid w-[400px] gap-4 p-4 md:w-[500px] md:grid-cols-2 lg:w-[600px]">
{item.items && item.items.map((subItem, subIndex) => {
const isSubItemActive = subItem.href && localePathname.startsWith(subItem.href);
return (
<li key={subIndex}>
<NavigationMenuLink asChild>
<LocaleLink
href={subItem.href || '#'}
target={subItem.external ? '_blank' : undefined}
rel={
subItem.external
? 'noopener noreferrer'
: undefined
}
className="group flex select-none flex-row items-center gap-4 rounded-md
{menuLinks &&
menuLinks.map((item, index) =>
item.items ? (
<NavigationMenuItem key={index} className="relative">
<NavigationMenuTrigger
data-active={
item.items.some((subItem) =>
subItem.href
? localePathname.startsWith(subItem.href)
: false
)
? 'true'
: undefined
}
className={customNavigationMenuTriggerStyle}
>
{item.title}
</NavigationMenuTrigger>
<NavigationMenuContent>
<ul className="grid w-[400px] gap-4 p-4 md:w-[500px] md:grid-cols-2 lg:w-[600px]">
{item.items &&
item.items.map((subItem, subIndex) => {
const isSubItemActive =
subItem.href &&
localePathname.startsWith(subItem.href);
return (
<li key={subIndex}>
<NavigationMenuLink asChild>
<LocaleLink
href={subItem.href || '#'}
target={
subItem.external
? '_blank'
: undefined
}
rel={
subItem.external
? 'noopener noreferrer'
: undefined
}
className="group flex select-none flex-row items-center gap-4 rounded-md
p-2 leading-none no-underline outline-none transition-colors
hover:bg-accent hover:text-accent-foreground
focus:bg-accent focus:text-accent-foreground"
>
<div className={cn(
"flex size-8 shrink-0 items-center justify-center transition-colors",
"bg-transparent text-muted-foreground",
"group-hover:bg-transparent group-hover:text-primary group-focus:bg-transparent group-focus:text-primary",
isSubItemActive && "bg-transparent text-primary"
)}>
{subItem.icon ? subItem.icon : null}
</div>
<div className="flex-1">
<div className={cn(
"text-sm font-medium text-muted-foreground",
"group-hover:bg-transparent group-hover:text-primary group-focus:bg-transparent group-focus:text-primary",
isSubItemActive && "bg-transparent text-primary"
)}>
{subItem.title}
</div>
{subItem.description && (
<div className={cn(
"text-sm text-muted-foreground",
"group-hover:bg-transparent group-hover:text-primary/80 group-focus:bg-transparent group-focus:text-primary/80",
isSubItemActive && "bg-transparent text-primary/80"
)}>
{subItem.description}
>
<div
className={cn(
'flex size-8 shrink-0 items-center justify-center transition-colors',
'bg-transparent text-muted-foreground',
'group-hover:bg-transparent group-hover:text-primary group-focus:bg-transparent group-focus:text-primary',
isSubItemActive &&
'bg-transparent text-primary'
)}
>
{subItem.icon ? subItem.icon : null}
</div>
)}
</div>
{subItem.external && (
<ArrowUpRightIcon className={cn(
"size-4 shrink-0 text-muted-foreground",
"group-hover:bg-transparent group-hover:text-primary group-focus:bg-transparent group-focus:text-primary",
isSubItemActive && "bg-transparent text-primary"
)} />
)}
</LocaleLink>
</NavigationMenuLink>
</li>
)
})}
</ul>
</NavigationMenuContent>
</NavigationMenuItem>
) : (
<NavigationMenuItem key={index}>
<NavigationMenuLink
asChild
active={item.href ? localePathname.startsWith(item.href) : false}
className={customNavigationMenuTriggerStyle}
>
<LocaleLink
href={item.href || '#'}
target={item.external ? '_blank' : undefined}
rel={
item.external ? 'noopener noreferrer' : undefined
<div className="flex-1">
<div
className={cn(
'text-sm font-medium text-muted-foreground',
'group-hover:bg-transparent group-hover:text-primary group-focus:bg-transparent group-focus:text-primary',
isSubItemActive &&
'bg-transparent text-primary'
)}
>
{subItem.title}
</div>
{subItem.description && (
<div
className={cn(
'text-sm text-muted-foreground',
'group-hover:bg-transparent group-hover:text-primary/80 group-focus:bg-transparent group-focus:text-primary/80',
isSubItemActive &&
'bg-transparent text-primary/80'
)}
>
{subItem.description}
</div>
)}
</div>
{subItem.external && (
<ArrowUpRightIcon
className={cn(
'size-4 shrink-0 text-muted-foreground',
'group-hover:bg-transparent group-hover:text-primary group-focus:bg-transparent group-focus:text-primary',
isSubItemActive &&
'bg-transparent text-primary'
)}
/>
)}
</LocaleLink>
</NavigationMenuLink>
</li>
);
})}
</ul>
</NavigationMenuContent>
</NavigationMenuItem>
) : (
<NavigationMenuItem key={index}>
<NavigationMenuLink
asChild
active={
item.href
? localePathname.startsWith(item.href)
: false
}
className={customNavigationMenuTriggerStyle}
>
{item.title}
</LocaleLink>
</NavigationMenuLink>
</NavigationMenuItem>
)
)}
<LocaleLink
href={item.href || '#'}
target={item.external ? '_blank' : undefined}
rel={
item.external ? 'noopener noreferrer' : undefined
}
>
{item.title}
</LocaleLink>
</NavigationMenuLink>
</NavigationMenuItem>
)
)}
</NavigationMenuList>
</NavigationMenu>
</div>
@ -182,20 +212,14 @@ export function Navbar({ scroll }: NavBarProps) {
) : (
<div className="flex items-center gap-x-4">
<LoginWrapper mode="modal" asChild>
<Button
variant="outline"
size="sm"
>
{commonTranslations("login")}
<Button variant="outline" size="sm">
{commonTranslations('login')}
</Button>
</LoginWrapper>
<Button asChild
size="sm"
variant="default"
>
<Button asChild size="sm" variant="default">
<LocaleLink href={Routes.Register}>
{commonTranslations("signUp")}
{commonTranslations('signUp')}
</LocaleLink>
</Button>
</div>

View File

@ -1,10 +1,10 @@
"use client";
'use client';
import { Button } from "@/components/ui/button";
import { LaptopIcon, MoonIcon, SunIcon } from "lucide-react";
import { useTheme } from "next-themes";
import { cn } from "@/lib/utils";
import { useEffect, useState } from "react";
import { Button } from '@/components/ui/button';
import { LaptopIcon, MoonIcon, SunIcon } from 'lucide-react';
import { useTheme } from 'next-themes';
import { cn } from '@/lib/utils';
import { useEffect, useState } from 'react';
/**
* Theme switcher component, used in the footer, switch theme by theme variable
@ -34,10 +34,10 @@ export function ThemeSwitcherHorizontal() {
variant="ghost"
size="icon"
className={cn(
"size-6 px-0 rounded-full",
theme === "light" && "bg-muted text-foreground"
'size-6 px-0 rounded-full',
theme === 'light' && 'bg-muted text-foreground'
)}
onClick={() => setTheme("light")}
onClick={() => setTheme('light')}
aria-label="Light mode"
>
<SunIcon className="size-4" />
@ -47,10 +47,10 @@ export function ThemeSwitcherHorizontal() {
variant="ghost"
size="icon"
className={cn(
"size-6 px-0 rounded-full",
theme === "dark" && "bg-muted text-foreground"
'size-6 px-0 rounded-full',
theme === 'dark' && 'bg-muted text-foreground'
)}
onClick={() => setTheme("dark")}
onClick={() => setTheme('dark')}
aria-label="Dark mode"
>
<MoonIcon className="size-4" />
@ -60,14 +60,14 @@ export function ThemeSwitcherHorizontal() {
variant="ghost"
size="icon"
className={cn(
"size-6 px-0 rounded-full",
theme === "system" && "bg-muted text-foreground"
'size-6 px-0 rounded-full',
theme === 'system' && 'bg-muted text-foreground'
)}
onClick={() => setTheme("system")}
onClick={() => setTheme('system')}
aria-label="System mode"
>
<LaptopIcon className="size-4" />
</Button>
</div>
);
}
}

View File

@ -1,14 +1,14 @@
"use client";
'use client';
import { Button } from "@/components/ui/button";
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { LaptopIcon, MoonIcon, SunIcon } from "lucide-react";
import { useTheme } from "next-themes";
} from '@/components/ui/dropdown-menu';
import { LaptopIcon, MoonIcon, SunIcon } from 'lucide-react';
import { useTheme } from 'next-themes';
/**
* Theme switcher component, used in the navbar, switch theme by CSS transitions
@ -19,22 +19,35 @@ export function ThemeSwitcher() {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="p-2 border border-border rounded-full text-sm">
<Button
variant="ghost"
size="icon"
className="p-2 border border-border rounded-full text-sm"
>
<SunIcon className="rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<MoonIcon className="absolute rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")} className="cursor-pointer">
<DropdownMenuItem
onClick={() => setTheme('light')}
className="cursor-pointer"
>
<SunIcon className="mr-2 size-4" />
<span>Light</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")} className="cursor-pointer">
<DropdownMenuItem
onClick={() => setTheme('dark')}
className="cursor-pointer"
>
<MoonIcon className="mr-2 size-4" />
<span>Dark</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")} className="cursor-pointer">
<DropdownMenuItem
onClick={() => setTheme('system')}
className="cursor-pointer"
>
<LaptopIcon className="mr-2 size-4" />
<span>System</span>
</DropdownMenuItem>

View File

@ -1,6 +1,6 @@
"use client";
'use client';
import { UserAvatar } from "@/components/shared/user-avatar";
import { UserAvatar } from '@/components/shared/user-avatar';
import {
Drawer,
DrawerContent,
@ -9,21 +9,21 @@ import {
DrawerPortal,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
} from '@/components/ui/drawer';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { createTranslator, getAvatarLinks } from "@/config/marketing";
import { useMediaQuery } from "@/hooks/use-media-query";
import { LocaleLink, useLocaleRouter } from "@/i18n/navigation";
import { authClient } from "@/lib/auth-client";
import { useTranslations } from "next-intl";
import { LogOutIcon } from "lucide-react";
import { useState } from "react";
} from '@/components/ui/dropdown-menu';
import { createTranslator, getAvatarLinks } from '@/config/marketing';
import { useMediaQuery } from '@/hooks/use-media-query';
import { LocaleLink, useLocaleRouter } from '@/i18n/navigation';
import { authClient } from '@/lib/auth-client';
import { useTranslations } from 'next-intl';
import { LogOutIcon } from 'lucide-react';
import { useState } from 'react';
export function UserButton() {
const { data: session, error } = authClient.useSession();
@ -31,17 +31,17 @@ export function UserButton() {
const t = useTranslations();
const translator = createTranslator(t);
const avatarLinks = getAvatarLinks(translator);
const commonTranslations = useTranslations("Common");
const commonTranslations = useTranslations('Common');
const handleSignOut = async () => {
await authClient.signOut({
fetchOptions: {
onSuccess: () => {
console.log("sign out success");
localeRouter.push("/");
console.log('sign out success');
localeRouter.push('/');
},
onError: (error) => {
console.error("sign out error:", error);
console.error('sign out error:', error);
// TODO: show error message
},
},
@ -55,7 +55,7 @@ export function UserButton() {
};
const { isMobile } = useMediaQuery();
// Mobile View, use Drawer
if (isMobile) {
return (
@ -69,8 +69,10 @@ export function UserButton() {
</DrawerTrigger>
<DrawerPortal>
<DrawerOverlay className="fixed inset-0 z-40 bg-background/50" />
<DrawerContent className="fixed inset-x-0 bottom-0 z-50 mt-24
overflow-hidden rounded-t-[10px] border bg-background px-3 text-sm">
<DrawerContent
className="fixed inset-x-0 bottom-0 z-50 mt-24
overflow-hidden rounded-t-[10px] border bg-background px-3 text-sm"
>
<DrawerHeader>
<DrawerTitle />
</DrawerHeader>
@ -91,27 +93,29 @@ export function UserButton() {
</div>
<ul className="mb-14 mt-1 w-full text-muted-foreground">
{avatarLinks && avatarLinks.map((item) => (
<li
key={item.title}
className="rounded-lg text-foreground hover:bg-muted"
>
<LocaleLink
href={item.href || "#"}
onClick={closeDrawer}
className="flex w-full items-center gap-3 px-2.5 py-2"
{avatarLinks &&
avatarLinks.map((item) => (
<li
key={item.title}
className="rounded-lg text-foreground hover:bg-muted"
>
{item.icon ? item.icon : null}
<p className="text-sm">{item.title}</p>
</LocaleLink>
</li>
))}
<LocaleLink
href={item.href || '#'}
onClick={closeDrawer}
className="flex w-full items-center gap-3 px-2.5 py-2"
>
{item.icon ? item.icon : null}
<p className="text-sm">{item.title}</p>
</LocaleLink>
</li>
))}
<li
key="logout"
className="rounded-lg text-foreground hover:bg-muted"
>
<a href="#"
<a
href="#"
onClick={async (event) => {
event.preventDefault();
closeDrawer();
@ -120,7 +124,7 @@ export function UserButton() {
className="flex w-full items-center gap-3 px-2.5 py-2"
>
<LogOutIcon className="size-4" />
<p className="text-sm">{commonTranslations("logout")}</p>
<p className="text-sm">{commonTranslations('logout')}</p>
</a>
</li>
</ul>
@ -181,7 +185,7 @@ export function UserButton() {
>
<div className="flex items-center space-x-2.5">
<LogOutIcon className="size-4" />
<p className="text-sm">{commonTranslations("logout")}</p>
<p className="text-sm">{commonTranslations('logout')}</p>
</div>
</DropdownMenuItem>
</DropdownMenuContent>

View File

@ -1,5 +1,5 @@
import { cn } from "@/lib/utils";
import Image from "next/image";
import { cn } from '@/lib/utils';
import Image from 'next/image';
export function MkSaaSLogo({ className }: { className?: string }) {
return (
@ -9,7 +9,7 @@ export function MkSaaSLogo({ className }: { className?: string }) {
title="Logo of MkSaaS"
width={96}
height={96}
className={cn("size-8 rounded-md", className)}
className={cn('size-8 rounded-md', className)}
/>
);
}

View File

@ -1,5 +1,5 @@
import { cn } from "@/lib/utils";
import Image from "next/image";
import { cn } from '@/lib/utils';
import Image from 'next/image';
export function Logo({ className }: { className?: string }) {
return (
@ -9,7 +9,7 @@ export function Logo({ className }: { className?: string }) {
title="Logo"
width={96}
height={96}
className={cn("size-8 rounded-md", className)}
className={cn('size-8 rounded-md', className)}
/>
);
}

View File

@ -1,11 +1,11 @@
"use client";
'use client';
import { ChevronRight, type LucideIcon } from "lucide-react";
import { ChevronRight, type LucideIcon } from 'lucide-react';
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
} from '@/components/ui/collapsible';
import {
SidebarGroup,
SidebarGroupLabel,
@ -16,7 +16,7 @@ import {
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
} from "@/components/ui/sidebar";
} from '@/components/ui/sidebar';
export function NavMain({
items,

View File

@ -1,4 +1,4 @@
"use client";
'use client';
import {
Folder,
@ -6,14 +6,14 @@ import {
Share,
Trash2,
type LucideIcon,
} from "lucide-react";
} from 'lucide-react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
} from '@/components/ui/dropdown-menu';
import {
SidebarGroup,
SidebarGroupLabel,
@ -22,7 +22,7 @@ import {
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar";
} from '@/components/ui/sidebar';
export function NavProjects({
projects,
@ -56,8 +56,8 @@ export function NavProjects({
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-48"
side={isMobile ? "bottom" : "right"}
align={isMobile ? "end" : "start"}
side={isMobile ? 'bottom' : 'right'}
align={isMobile ? 'end' : 'start'}
>
<DropdownMenuItem>
<Folder className="text-muted-foreground" />

View File

@ -1,12 +1,12 @@
import * as React from "react";
import { type LucideIcon } from "lucide-react";
import * as React from 'react';
import { type LucideIcon } from 'lucide-react';
import {
SidebarGroup,
SidebarGroupContent,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar";
} from '@/components/ui/sidebar';
export function NavSecondary({
items,

View File

@ -1,4 +1,4 @@
"use client";
'use client';
import {
BadgeCheck,
@ -7,12 +7,8 @@ import {
CreditCard,
LogOut,
Sparkles,
} from "lucide-react";
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar";
} from 'lucide-react';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import {
DropdownMenu,
DropdownMenuContent,
@ -21,16 +17,16 @@ import {
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
} from '@/components/ui/dropdown-menu';
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar";
import { authClient } from "@/lib/auth-client";
import { getInitials } from "@/lib/utils";
import { useRouter } from "next/navigation";
} from '@/components/ui/sidebar';
import { authClient } from '@/lib/auth-client';
import { getInitials } from '@/lib/utils';
import { useRouter } from 'next/navigation';
export function NavUser() {
const router = useRouter();
@ -46,11 +42,11 @@ export function NavUser() {
await authClient.signOut({
fetchOptions: {
onSuccess: () => {
console.log("sign out success");
router.push("/");
console.log('sign out success');
router.push('/');
},
onError: (error) => {
console.error("sign out error:", error);
console.error('sign out error:', error);
// TODO: show error message
},
},
@ -68,7 +64,9 @@ export function NavUser() {
>
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src={user.image || undefined} alt={user.name} />
<AvatarFallback className="rounded-lg">{getInitials(user.name)}</AvatarFallback>
<AvatarFallback className="rounded-lg">
{getInitials(user.name)}
</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">{user.name}</span>
@ -79,7 +77,7 @@ export function NavUser() {
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
side={isMobile ? "bottom" : "right"}
side={isMobile ? 'bottom' : 'right'}
align="end"
sideOffset={4}
>
@ -87,7 +85,9 @@ export function NavUser() {
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src={user.image || undefined} alt={user.name} />
<AvatarFallback className="rounded-lg">{getInitials(user.name)}</AvatarFallback>
<AvatarFallback className="rounded-lg">
{getInitials(user.name)}
</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">{user.name}</span>
@ -97,26 +97,22 @@ export function NavUser() {
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
className="cursor-pointer">
<DropdownMenuItem className="cursor-pointer">
<Sparkles />
Upgrade to Pro
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
className="cursor-pointer">
<DropdownMenuItem className="cursor-pointer">
<BadgeCheck />
Account
</DropdownMenuItem>
<DropdownMenuItem
className="cursor-pointer">
<DropdownMenuItem className="cursor-pointer">
<CreditCard />
Billing
</DropdownMenuItem>
<DropdownMenuItem
className="cursor-pointer">
<DropdownMenuItem className="cursor-pointer">
<Bell />
Notifications
</DropdownMenuItem>
@ -127,7 +123,8 @@ export function NavUser() {
onClick={async (event) => {
event.preventDefault();
handleSignOut();
}}>
}}
>
<LogOut />
Log out
</DropdownMenuItem>

View File

@ -1,7 +1,7 @@
"use client";
'use client';
import { HeaderSection } from "@/components/shared/header-section";
import { NewsletterForm } from "@/components/newsletter/newsletter-form";
import { HeaderSection } from '@/components/shared/header-section';
import { NewsletterForm } from '@/components/newsletter/newsletter-form';
export function NewsletterCard() {
return (

View File

@ -1,8 +1,8 @@
"use client";
'use client';
// import { subscribeToNewsletter } from "@/actions/subscribe-to-newsletter";
import { Icons } from "@/components/icons/icons";
import { Button } from "@/components/ui/button";
import { Icons } from '@/components/icons/icons';
import { Button } from '@/components/ui/button';
import {
Form,
FormControl,
@ -10,15 +10,15 @@ import {
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { type NewsletterFormData, NewsletterFormSchema } from "@/lib/schemas";
import { cn } from "@/lib/utils";
import { zodResolver } from "@hookform/resolvers/zod";
import { PaperPlaneIcon } from "@radix-ui/react-icons";
import { useTransition } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { type NewsletterFormData, NewsletterFormSchema } from '@/lib/schemas';
import { cn } from '@/lib/utils';
import { zodResolver } from '@hookform/resolvers/zod';
import { PaperPlaneIcon } from '@radix-ui/react-icons';
import { useTransition } from 'react';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
export function NewsletterForm() {
const [isPending, startTransition] = useTransition();
@ -26,7 +26,7 @@ export function NewsletterForm() {
const form = useForm<NewsletterFormData>({
resolver: zodResolver(NewsletterFormSchema),
defaultValues: {
email: "",
email: '',
},
});
@ -66,8 +66,8 @@ export function NewsletterForm() {
<Input
type="email"
className={cn(
"w-[280px] sm:w-[320px] md:w-[400px] h-12 rounded-r-none",
"focus-visible:ring-0 focus-visible:ring-offset-0 focus:border-primary focus:border-2 focus:border-r-0",
'w-[280px] sm:w-[320px] md:w-[400px] h-12 rounded-r-none',
'focus-visible:ring-0 focus-visible:ring-offset-0 focus:border-primary focus:border-2 focus:border-r-0'
)}
placeholder="Enter your email"
{...field}

View File

@ -12,7 +12,12 @@ interface CustomPageProps {
content: any; // MDX content
}
export function CustomPage({ title, description, date, content }: CustomPageProps) {
export function CustomPage({
title,
description,
date,
content,
}: CustomPageProps) {
const formattedDate = getLocaleDate(date);
return (
@ -41,4 +46,4 @@ export function CustomPage({ title, description, date, content }: CustomPageProp
</Card>
</div>
);
}
}

View File

@ -1,6 +1,6 @@
import { Button } from '@/components/ui/button'
import { Cpu, Sparkles } from 'lucide-react'
import Link from 'next/link'
import { Button } from '@/components/ui/button';
import { Cpu, Sparkles } from 'lucide-react';
import Link from 'next/link';
const tableData = [
{
@ -45,7 +45,7 @@ const tableData = [
pro: '20 Users',
startup: 'Unlimited',
},
]
];
/**
* https://nsui.irung.me/comparator
@ -95,8 +95,17 @@ export default function PricingComparator() {
<td className="text-muted-foreground">{row.feature}</td>
<td>
{row.free === true ? (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="size-4">
<path fillRule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm13.36-1.814a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z" clipRule="evenodd" />
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
className="size-4"
>
<path
fillRule="evenodd"
d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm13.36-1.814a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z"
clipRule="evenodd"
/>
</svg>
) : (
row.free
@ -105,8 +114,17 @@ export default function PricingComparator() {
<td className="bg-muted border-none px-4">
<div className="-mb-3 border-b py-3">
{row.pro === true ? (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="size-4">
<path fillRule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm13.36-1.814a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z" clipRule="evenodd" />
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
className="size-4"
>
<path
fillRule="evenodd"
d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm13.36-1.814a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z"
clipRule="evenodd"
/>
</svg>
) : (
row.pro
@ -115,8 +133,17 @@ export default function PricingComparator() {
</td>
<td>
{row.startup === true ? (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="size-4">
<path fillRule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm13.36-1.814a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z" clipRule="evenodd" />
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
className="size-4"
>
<path
fillRule="evenodd"
d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm13.36-1.814a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z"
clipRule="evenodd"
/>
</svg>
) : (
row.startup
@ -138,8 +165,17 @@ export default function PricingComparator() {
<td className="text-muted-foreground">{row.feature}</td>
<td>
{row.free === true ? (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="size-4">
<path fillRule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm13.36-1.814a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z" clipRule="evenodd" />
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
className="size-4"
>
<path
fillRule="evenodd"
d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm13.36-1.814a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z"
clipRule="evenodd"
/>
</svg>
) : (
row.free
@ -148,8 +184,17 @@ export default function PricingComparator() {
<td className="bg-muted border-none px-4">
<div className="-mb-3 border-b py-3">
{row.pro === true ? (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="size-4">
<path fillRule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm13.36-1.814a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z" clipRule="evenodd" />
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
className="size-4"
>
<path
fillRule="evenodd"
d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm13.36-1.814a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z"
clipRule="evenodd"
/>
</svg>
) : (
row.pro
@ -158,8 +203,17 @@ export default function PricingComparator() {
</td>
<td>
{row.startup === true ? (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="size-4">
<path fillRule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm13.36-1.814a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z" clipRule="evenodd" />
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
className="size-4"
>
<path
fillRule="evenodd"
d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm13.36-1.814a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z"
clipRule="evenodd"
/>
</svg>
) : (
row.startup
@ -178,5 +232,5 @@ export default function PricingComparator() {
</div>
</div>
</section>
)
);
}

View File

@ -13,7 +13,13 @@ interface ReleaseCardProps {
content: any; // MDX content
}
export function ReleaseCard({ title, description, date, version, content }: ReleaseCardProps) {
export function ReleaseCard({
title,
description,
date,
version,
content,
}: ReleaseCardProps) {
const formattedDate = getLocaleDate(date);
return (
@ -40,4 +46,4 @@ export function ReleaseCard({ title, description, date, version, content }: Rele
</CardContent>
</Card>
);
}
}

View File

@ -1,10 +1,10 @@
"use client";
'use client';
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { ArrowLeftIcon } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { ArrowLeftIcon } from 'lucide-react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
interface BackButtonSmallProps {
href?: string;
@ -25,11 +25,11 @@ export default function BackButtonSmall({
<Button
size="sm"
variant="outline"
className={cn("size-8 px-0", className)}
className={cn('size-8 px-0', className)}
asChild
>
{/* if href is provided, use it, otherwise use the router.back() */}
<Link href={href || "#"} onClick={handleBack}>
<Link href={href || '#'} onClick={handleBack}>
<ArrowLeftIcon className="size-5" />
</Link>
</Button>

View File

@ -1,7 +1,7 @@
import { buttonVariants } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import Link from "next/link";
import { MkSaaSLogo } from "@/components/logo-mksaas";
import { buttonVariants } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import Link from 'next/link';
import { MkSaaSLogo } from '@/components/logo-mksaas';
export default function BuiltWithButton() {
return (
@ -9,8 +9,8 @@ export default function BuiltWithButton() {
target="_blank"
href="https://mksaas.com?utm_source=built-with-mksaas"
className={cn(
buttonVariants({ variant: "outline", size: "sm" }),
"border border-border px-4 rounded-md"
buttonVariants({ variant: 'outline', size: 'sm' }),
'border border-border px-4 rounded-md'
)}
>
<span>Built with</span>

View File

@ -6,9 +6,9 @@ import {
FileText,
Info,
Lightbulb,
} from "lucide-react";
} from 'lucide-react';
import { cn } from "@/lib/utils";
import { cn } from '@/lib/utils';
interface CalloutProps {
twClass?: string;
@ -20,49 +20,49 @@ const dataCallout = {
default: {
icon: Info,
classes:
"border-zinc-200 bg-gray-50 text-zinc-900 dark:bg-zinc-800 dark:text-zinc-200",
'border-zinc-200 bg-gray-50 text-zinc-900 dark:bg-zinc-800 dark:text-zinc-200',
},
danger: {
icon: CircleAlert,
classes:
"border-red-200 bg-red-50 text-red-900 dark:bg-red-950 dark:text-red-200",
'border-red-200 bg-red-50 text-red-900 dark:bg-red-950 dark:text-red-200',
},
error: {
icon: Ban,
classes:
"border-red-200 bg-red-50 text-red-900 dark:bg-red-950 dark:text-red-200",
'border-red-200 bg-red-50 text-red-900 dark:bg-red-950 dark:text-red-200',
},
idea: {
icon: Lightbulb,
classes:
"border-blue-200 bg-blue-50 text-blue-800 dark:bg-blue-950 dark:text-blue-200",
'border-blue-200 bg-blue-50 text-blue-800 dark:bg-blue-950 dark:text-blue-200',
},
info: {
icon: Info,
classes:
"border-blue-200 bg-blue-50 text-blue-800 dark:bg-blue-950 dark:text-blue-200",
'border-blue-200 bg-blue-50 text-blue-800 dark:bg-blue-950 dark:text-blue-200',
},
note: {
icon: FileText,
classes:
"border-blue-200 bg-blue-50 text-blue-800 dark:bg-blue-950 dark:text-blue-200",
'border-blue-200 bg-blue-50 text-blue-800 dark:bg-blue-950 dark:text-blue-200',
},
success: {
icon: CircleCheckBig,
classes:
"border-green-200 bg-green-50 text-green-800 dark:bg-green-400/20 dark:text-green-300",
'border-green-200 bg-green-50 text-green-800 dark:bg-green-400/20 dark:text-green-300',
},
warning: {
icon: AlertTriangle,
classes:
"border-orange-200 bg-orange-50 text-orange-800 dark:bg-orange-400/20 dark:text-orange-300",
'border-orange-200 bg-orange-50 text-orange-800 dark:bg-orange-400/20 dark:text-orange-300',
},
};
export function Callout({
children,
twClass,
type = "default",
type = 'default',
...props
}: CalloutProps) {
const { icon: Icon, classes } = dataCallout[type];

View File

@ -1,10 +1,6 @@
import * as React from 'react';
import { ComponentProps } from 'react';
import {
Alert,
AlertDescription,
AlertTitle
} from '@/components/ui/alert';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
type CalloutProps = ComponentProps<typeof Alert> & {
icon?: string;

View File

@ -1,9 +1,9 @@
"use client";
'use client';
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { CheckIcon, CopyIcon } from "lucide-react";
import React, { useEffect } from "react";
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { CheckIcon, CopyIcon } from 'lucide-react';
import React, { useEffect } from 'react';
interface CopyButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
value: string;
@ -29,8 +29,8 @@ export function CopyButton({ value, className, ...props }: CopyButtonProps) {
size="sm"
variant="ghost"
className={cn(
"z-10 size-[30px] border border-white/25 bg-zinc-900 p-1.5 text-primary-foreground hover:text-foreground dark:text-foreground",
className,
'z-10 size-[30px] border border-white/25 bg-zinc-900 p-1.5 text-primary-foreground hover:text-foreground dark:text-foreground',
className
)}
onClick={() => handleCopyValue(value)}
{...props}

View File

@ -1,12 +1,12 @@
import { useTranslations } from "next-intl";
import { useTranslations } from 'next-intl';
export default function EmptyGrid() {
const t = useTranslations("BlogPage");
const t = useTranslations('BlogPage');
return (
<div>
<div className="my-8 h-32 w-full flex items-center justify-center">
<p className="font-medium text-muted-foreground">{t("noPostsFound")}</p>
<p className="font-medium text-muted-foreground">{t('noPostsFound')}</p>
</div>
</div>
);

View File

@ -1,7 +1,7 @@
"use client";
'use client';
import { cn } from "@/lib/utils";
import { LocaleLink } from "@/i18n/navigation";
import { cn } from '@/lib/utils';
import { LocaleLink } from '@/i18n/navigation';
interface FilterItemMobileProps {
title: string;
@ -22,12 +22,12 @@ export default function FilterItemMobile({
href={href}
onClick={clickAction}
className={cn(
"flex w-full items-center justify-between rounded-md px-3 py-2 text-sm hover:bg-muted",
active && "bg-primary text-primary-foreground hover:bg-primary/90"
'flex w-full items-center justify-between rounded-md px-3 py-2 text-sm hover:bg-muted',
active && 'bg-primary text-primary-foreground hover:bg-primary/90'
)}
>
{title}
</LocaleLink>
</li>
);
}
}

View File

@ -1,4 +1,4 @@
import { TriangleAlertIcon } from "lucide-react";
import { TriangleAlertIcon } from 'lucide-react';
interface FormErrorProps {
message?: string;

View File

@ -1,4 +1,4 @@
import { CircleCheckIcon } from "lucide-react";
import { CircleCheckIcon } from 'lucide-react';
interface FormSuccessProps {
message?: string;

View File

@ -1,13 +1,13 @@
import { cn } from "@/lib/utils";
import { cn } from '@/lib/utils';
interface HeaderSectionProps {
id?: string;
label?: string;
labelAs?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "p";
labelAs?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p';
title?: string;
titleAs?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "p";
titleAs?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p';
subtitle?: string;
subtitleAs?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "p";
subtitleAs?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p';
className?: string;
children?: React.ReactNode;
}
@ -18,11 +18,11 @@ interface HeaderSectionProps {
export function HeaderSection({
id,
label,
labelAs = "p",
labelAs = 'p',
title,
titleAs = "p",
titleAs = 'p',
subtitle,
subtitleAs = "p",
subtitleAs = 'p',
className,
children,
}: HeaderSectionProps) {
@ -32,7 +32,7 @@ export function HeaderSection({
return (
<div
id={id}
className={cn("flex flex-col items-center text-center gap-4", className)}
className={cn('flex flex-col items-center text-center gap-4', className)}
>
{label ? (
<LabelComponent className="uppercase tracking-wider text-gradient_indigo-purple font-semibold">

View File

@ -9,14 +9,9 @@ import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger
AccordionTrigger,
} from '@/components/ui/accordion';
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger
} from '@/components/ui/tabs';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { cn } from '@/lib/utils';
const components = {
@ -84,28 +79,16 @@ const components = {
/>
),
p: ({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) => (
<p
className={cn('mt-6 leading-7', className)}
{...props}
/>
<p className={cn('mt-6 leading-7', className)} {...props} />
),
ul: ({ className, ...props }: React.HTMLAttributes<HTMLUListElement>) => (
<ul
className={cn('my-6 ml-6 list-disc', className)}
{...props}
/>
<ul className={cn('my-6 ml-6 list-disc', className)} {...props} />
),
ol: ({ className, ...props }: React.HTMLAttributes<HTMLOListElement>) => (
<ol
className={cn('my-6 ml-6 list-decimal', className)}
{...props}
/>
<ol className={cn('my-6 ml-6 list-decimal', className)} {...props} />
),
li: ({ className, ...props }: React.HTMLAttributes<HTMLElement>) => (
<li
className={cn('mt-2', className)}
{...props}
/>
<li className={cn('mt-2', className)} {...props} />
),
blockquote: ({ className, ...props }: React.HTMLAttributes<HTMLElement>) => (
<blockquote
@ -118,17 +101,10 @@ const components = {
alt,
...props
}: React.ImgHTMLAttributes<HTMLImageElement>) => (
<img
className={cn('rounded-md', className)}
alt={alt}
{...props}
/>
<img className={cn('rounded-md', className)} alt={alt} {...props} />
),
hr: ({ ...props }: React.HTMLAttributes<HTMLHRElement>) => (
<hr
className="my-4 md:my-8"
{...props}
/>
<hr className="my-4 md:my-8" {...props} />
),
table: ({ className, ...props }: React.HTMLAttributes<HTMLTableElement>) => (
<div className="my-6 w-full overflow-y-auto rounded-none">
@ -139,10 +115,7 @@ const components = {
</div>
),
tr: ({ className, ...props }: React.HTMLAttributes<HTMLTableRowElement>) => (
<tr
className={cn('m-0 border-t p-0', className)}
{...props}
/>
<tr className={cn('m-0 border-t p-0', className)} {...props} />
),
th: ({ className, ...props }: React.HTMLAttributes<HTMLTableCellElement>) => (
<th
@ -185,50 +158,32 @@ const components = {
className,
...props
}: React.ComponentProps<typeof AccordionContent>) => (
<AccordionContent
className={cn('[&>p]:m-0', className)}
{...props}
/>
<AccordionContent className={cn('[&>p]:m-0', className)} {...props} />
),
AccordionItem,
AccordionTrigger,
Callout,
Image,
Tabs: ({
className,
...props
}: React.ComponentProps<typeof Tabs>) => (
<Tabs
className={cn('relative mt-6 w-full', className)}
{...props}
/>
Tabs: ({ className, ...props }: React.ComponentProps<typeof Tabs>) => (
<Tabs className={cn('relative mt-6 w-full', className)} {...props} />
),
TabsList: ({
className,
...props
}: React.ComponentProps<typeof TabsList>) => (
<TabsList
className={cn('w-full border-b', className)}
{...props}
/>
<TabsList className={cn('w-full border-b', className)} {...props} />
),
TabsTrigger: ({
className,
...props
}: React.ComponentProps<typeof TabsTrigger>) => (
<TabsTrigger
className={cn('', className)}
{...props}
/>
<TabsTrigger className={cn('', className)} {...props} />
),
TabsContent: ({
className,
...props
}: React.ComponentProps<typeof TabsContent>) => (
<TabsContent
className={cn('p-4', className)}
{...props}
/>
<TabsContent className={cn('p-4', className)} {...props} />
),
Link: ({ className, ...props }: React.ComponentProps<typeof Link>) => (
<Link
@ -244,7 +199,7 @@ const components = {
)}
{...props}
/>
)
),
};
type MdxProps = {

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