refactor: run biome format to src folder
This commit is contained in:
parent
92ec1b14c5
commit
34006556c8
@ -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',
|
||||
});
|
||||
|
@ -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>
|
||||
|
@ -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 />
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -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. */}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -1,4 +1,4 @@
|
||||
import Link from "next/link";
|
||||
import Link from 'next/link';
|
||||
|
||||
/**
|
||||
* https://nsui.irung.me/content-6
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -5,7 +5,7 @@ import {
|
||||
Settings2,
|
||||
Sparkles,
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
} from 'lucide-react';
|
||||
|
||||
/**
|
||||
* https://nsui.irung.me/features-4
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 = () => {
|
||||
|
@ -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
|
||||
|
@ -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() {
|
||||
|
@ -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() {
|
||||
|
@ -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() {
|
||||
|
@ -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() {
|
||||
|
@ -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" />
|
||||
|
@ -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" />
|
||||
|
@ -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" />
|
||||
|
@ -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" />
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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!',
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -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!',
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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
|
||||
/>
|
||||
|
@ -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[];
|
||||
|
@ -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}`}
|
||||
>
|
||||
|
@ -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}
|
||||
|
@ -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[];
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -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" />;
|
||||
}
|
||||
|
@ -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/
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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/
|
||||
|
@ -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/
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import type { SVGProps } from "react";
|
||||
import type { SVGProps } from 'react';
|
||||
|
||||
/**
|
||||
* https://icon-sets.iconify.design/logos/producthunt/
|
||||
|
@ -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/
|
||||
|
@ -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/
|
||||
|
@ -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/
|
||||
|
@ -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/
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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/
|
||||
|
@ -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/
|
||||
|
@ -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/
|
||||
|
@ -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">
|
||||
© {new Date().getFullYear()} {siteConfig.name} All Rights Reserved.
|
||||
© {new Date().getFullYear()} {siteConfig.name} All Rights
|
||||
Reserved.
|
||||
</span>
|
||||
|
||||
<ThemeSwitcherHorizontal />
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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 */}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -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)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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" />
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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 (
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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];
|
||||
|
@ -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;
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { TriangleAlertIcon } from "lucide-react";
|
||||
import { TriangleAlertIcon } from 'lucide-react';
|
||||
|
||||
interface FormErrorProps {
|
||||
message?: string;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { CircleCheckIcon } from "lucide-react";
|
||||
import { CircleCheckIcon } from 'lucide-react';
|
||||
|
||||
interface FormSuccessProps {
|
||||
message?: string;
|
||||
|
@ -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">
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user