LogoGalsenext

API Référence

---

This section provides comprehensive documentation of the functions, methods, components, and other key elements of Galsenext.

Theme

ThemeProvider: /src/theme/ThemeProvider.tsx

"use client";
 
import * as React from "react";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import { type ThemeProviderProps } from "next-themes/dist/types";
 
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
  return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}

ThemeToggle: /src/components/ThemeToggle.tsx

"use client";
 
import { Button } from "@/components/ui/button";
import { Moon, Sun } from "lucide-react";
import { useTheme } from "next-themes";
 
export function ThemeToggle() {
  const { setTheme, theme } = useTheme();
 
  return (
    <Button
      variant="ghost"
      size="icon"
      onClick={() => setTheme(theme === "light" ? "dark" : "light")}
    >
      <Sun className="h-[1.5rem] w-[1.3rem] dark:hidden" />
      <Moon className="hidden h-5 w-5 dark:block" />
      <span className="sr-only">Toggle theme</span>
    </Button>
  );
}

Layout

LayoutComponents: /src/components/Layout/Layout.tsx

import type { ComponentPropsWithoutRef } from "react";
import { cn } from "@/lib/utils";
import { Typography } from "@/src/components/ui/Typography";
 
export const Layout = (props: ComponentPropsWithoutRef<"div">) => {
  return (
    <div
      {...props}
      className={cn(
        "max-w-3xl flex-wrap w-full flex gap-4 m-auto px-4 mt-4",
        props.className
      )}
    />
  );
};
 
export const LayoutHeader = (props: ComponentPropsWithoutRef<"div">) => {
  return (
    <div
      {...props}
      className={cn(
        "flex items-start gap-1 flex-col w-full md:flex-1 min-w-[200px]",
        props.className
      )}
    />
  );
};
 
export const LayoutTitle = (props: ComponentPropsWithoutRef<"h1">) => {
  return <Typography {...props} variant="h2" className={cn(props.className)} />;
};
 
export const LayoutDescription = (props: ComponentPropsWithoutRef<"p">) => {
  return <Typography {...props} className={cn(props.className)} />;
};
 
export const LayoutActions = (props: ComponentPropsWithoutRef<"div">) => {
  return (
    <div {...props} className={cn("flex items-center", props.className)} />
  );
};
 
export const LayoutContent = (props: ComponentPropsWithoutRef<"div">) => {
  return <div {...props} className={cn("w-full", props.className)} />;
};

UI Components

Typography: /src/components/ui/Typography.tsx

import { VariantProps, cva } from "class-variance-authority";
import type {
  ComponentPropsWithoutRef,
  ElementType,
  PropsWithChildren,
} from "react";
import { cn } from "@/lib/utils";
 
type PolymorphicAsProp<E extends ElementType> = {
  as?:
    | E
    | React.ComponentType<Omit<ComponentPropsWithoutRef<E>, "as">>
    | React.FunctionComponent<Omit<ComponentPropsWithoutRef<E>, "as">>;
};
 
type PolymorphicProps<E extends ElementType> = PropsWithChildren<
  Omit<ComponentPropsWithoutRef<E>, "as"> & PolymorphicAsProp<E>
>;
 
const typographyVariants = cva("", {
  variants: {
    variant: {
      h1: "scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl font-caption",
      h2: "scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight transition-colors first:mt-0 font-caption",
      h3: "scroll-m-20 text-xl font-semibold tracking-tight font-caption",
      p: "leading-7 [&:not(:first-child)]:mt-6",
      base: "",
      quote: "mt-6 border-l-2 pl-6 italic",
      code: "relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold",
      lead: "text-xl text-muted-foreground",
      large: "text-lg font-semibold",
      small: "text-sm font-medium leading-none",
      muted: "text-sm text-muted-foreground",
      link: "text-indigo-500 font-medium hover:underline",
    },
  },
  defaultVariants: {
    variant: "base",
  },
});
 
type TypographyCvaProps = VariantProps<typeof typographyVariants>;
 
const defaultElement = "p";
 
const defaultElementMapping: Record<
  NonNullable<TypographyCvaProps["variant"]>,
  ElementType
> = {
  h1: "h1",
  h2: "h2",
  h3: "h3",
  p: "p",
  quote: "blockquote" as "p",
  code: "code",
  lead: "p",
  large: "p",
  small: "p",
  muted: "p",
  link: "a",
  base: "p",
} as const;
 
export function Typography<E extends ElementType = typeof defaultElement>({
  as,
  children,
  className,
  variant,
  ...restProps
}: PolymorphicProps<E> & TypographyCvaProps) {
  const Component: ElementType =
    as ?? defaultElementMapping[variant ?? "base"] ?? defaultElement;
 
  return (
    <Component
      {...(restProps as ComponentPropsWithoutRef<E>)}
      className={cn(typographyVariants({ variant }), className)}
    >
      {children}
    </Component>
  );
}

Loader: /src/components/ui/Loader.tsx

import { cn } from "@/lib/utils";
import { Loader2, LucideProps } from "lucide-react";
 
export const Loader = ({ className, ...props }: LucideProps) => {
  return <Loader2 className={cn("animate-spin", className)} {...props} />;
};

Authentication components

LoggedInButton: src/features/auth/LoggedInButton.tsx

"use client";
 
import {
  AlertDialog,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useMutation } from "@tanstack/react-query";
import { Loader, LogOut, User2 } from "lucide-react";
import { Session } from "next-auth";
import { signOut } from "next-auth/react";
 
export type LoggedInButtonProps = {
  user: Session["user"];
};
 
export const LoggedInButton = (props: LoggedInButtonProps) => {
  const mutation = useMutation({
    mutationFn: async () => {
      signOut();
    },
  });
 
  return (
    <DropdownMenu>
      <AlertDialog>
        <DropdownMenuTrigger>
          <Button variant="outline" size="sm">
            <Avatar className="mr-2 h-8 w-8">
              <AvatarFallback>{props.user?.name?.[0]}</AvatarFallback>
              {props.user.image && (
                <AvatarImage
                  src={props.user.image}
                  alt={props.user.name ?? "user picture"}
                />
              )}
            </Avatar>
            {props.user.name}
          </Button>
        </DropdownMenuTrigger>
        <DropdownMenuContent>
          <DropdownMenuItem>
            <User2 className="mr-2" size={12} /> Account
          </DropdownMenuItem>
          <DropdownMenuSeparator />
          <AlertDialogTrigger asChild>
            <DropdownMenuItem>
              <LogOut className="mr-2" size={12} /> Logout
            </DropdownMenuItem>
          </AlertDialogTrigger>
        </DropdownMenuContent>
        <AlertDialogContent>
          <AlertDialogHeader>
            <AlertDialogTitle>
              Are you sure you want to logout?
            </AlertDialogTitle>
          </AlertDialogHeader>
          <AlertDialogFooter>
            <AlertDialogCancel asChild>
              <Button variant="secondary">Cancel</Button>
            </AlertDialogCancel>
            <Button
              variant="destructive"
              disabled={mutation.isPending}
              onClick={() => {
                mutation.mutate();
              }}
            >
              {mutation.isPending ? (
                <Loader className="mr-2" size={12} />
              ) : (
                <LogOut className="mr-2" size={12} />
              )}
              Logout
            </Button>
          </AlertDialogFooter>
        </AlertDialogContent>
      </AlertDialog>
    </DropdownMenu>
  );
};

LoginButton: src/features/auth/LoginButton.tsx

"use client";
 
import { Button } from "@/components/ui/button";
import { useMutation } from "@tanstack/react-query";
import { LogIn } from "lucide-react";
import { signIn } from "next-auth/react";
import { Loader } from "@/components/ui/loader";
 
export const LoginButton = () => {
  const mutation = useMutation({
    mutationFn: async () => signIn(),
  });
 
  return (
    <Button
      className="gap-2"
      variant="outline"
      size="sm"
      disabled={mutation.isPending}
      onClick={() => {
        mutation.mutate();
      }}
    >
      {mutation.isPending ? <Loader size={12} /> : <LogIn size={12} />}
      Login
    </Button>
  );
};

LogoutButton: src/features/auth/LogoutButton.tsx

"use client";
 
import { Button } from "@/components/ui/button";
import { useMutation } from "@tanstack/react-query";
import { LogOut } from "lucide-react";
import { signOut } from "next-auth/react";
import { Loader } from "@/components/ui/loader";
 
export const LogoutButton = () => {
  const mutation = useMutation({
    mutationFn: async () => await signOut({ callbackUrl: "/" }),
  });
 
  return (
    <Button
      className="gap-2 hover:cursor-pointer"
      variant="outline"
      size="sm"
      disabled={mutation.isPending}
      onClick={() => {
        mutation.mutate();
      }}
    >
      {mutation.isPending ? <Loader size={12} /> : <LogOut size={12} />}
      Logout
    </Button>
  );
};

User: src/features/auth/User.tsx

"use client";
 
import Link from "next/link";
import { Loader, LogOut, User, BookOpenText, Home } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
  Tooltip,
  TooltipContent,
  TooltipTrigger,
  TooltipProvider,
} from "@/components/ui/tooltip";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useMutation } from "@tanstack/react-query";
import { Session } from "next-auth";
import { signOut } from "next-auth/react";
import {
  AlertDialog,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
 
export type UserNavProps = {
  user: Session["user"];
};
 
export const UserNav = (props: UserNavProps) => {
  const mutation = useMutation({
    mutationFn: async () => {
      await signOut({ callbackUrl: "/" });
    },
  });
 
  return (
    <DropdownMenu>
      <AlertDialog>
        <TooltipProvider disableHoverableContent>
          <Tooltip delayDuration={100}>
            <TooltipTrigger asChild>
              <DropdownMenuTrigger asChild>
                <Button
                  variant="outline"
                  className="relative h-8 w-8 rounded-full"
                >
                  <Avatar className="h-8 w-8">
                    <AvatarFallback>{props.user?.name?.[0]}</AvatarFallback>
                    {props.user?.image && (
                      <AvatarImage
                        src={props.user.image}
                        alt={props.user.name ?? "user picture"}
                      />
                    )}
                  </Avatar>
                </Button>
              </DropdownMenuTrigger>
            </TooltipTrigger>
            <TooltipContent side="bottom">Profile</TooltipContent>
          </Tooltip>
        </TooltipProvider>
 
        <DropdownMenuContent className="w-56" align="end" forceMount>
          <DropdownMenuLabel className="font-normal">
            <div className="flex flex-col space-y-1">
              <p className="text-sm font-medium leading-none">
                {props.user?.name}
              </p>
              <p className="text-xs leading-none text-muted-foreground">
                {props.user?.email}
              </p>
            </div>
          </DropdownMenuLabel>
          <DropdownMenuSeparator />
          <DropdownMenuGroup>
            <DropdownMenuItem className="hover:cursor-pointer" asChild>
              <Link href="/" className="flex items-center">
                <Home className="w-4 h-4 mr-3 text-muted-foreground" />
                Home
              </Link>
            </DropdownMenuItem>
            <DropdownMenuItem className="hover:cursor-pointer" asChild>
              <Link href="/write" className="flex items-center">
                <BookOpenText className="w-4 h-4 mr-3 text-muted-foreground" />
                Write
              </Link>
            </DropdownMenuItem>
            <DropdownMenuItem className="hover:cursor-pointer" asChild>
              <Link href="/profile" className="flex items-center">
                <User className="w-4 h-4 mr-3 text-muted-foreground" />
                Profile
              </Link>
            </DropdownMenuItem>
          </DropdownMenuGroup>
          <DropdownMenuSeparator />
          <AlertDialogTrigger asChild>
            <DropdownMenuItem className="hover:cursor-pointer">
              <LogOut className="w-4 h-4 mr-3 text-muted-foreground" />
              Sign out
            </DropdownMenuItem>
          </AlertDialogTrigger>
        </DropdownMenuContent>
        <AlertDialogContent>
          <AlertDialogHeader>
            <AlertDialogTitle>
              Are you sure you want to logout ?
            </AlertDialogTitle>
          </AlertDialogHeader>
          <AlertDialogFooter>
            <AlertDialogCancel asChild>
              <Button variant="secondary">Cancel</Button>
            </AlertDialogCancel>
            <Button
              variant="destructive"
              disabled={mutation.isPending}
              onClick={() => {
                mutation.mutate();
              }}
            >
              {mutation.isPending ? (
                <Loader className="mr-2" size={12} />
              ) : (
                <LogOut className="mr-2" size={12} />
              )}
              Logout
            </Button>
          </AlertDialogFooter>
        </AlertDialogContent>
      </AlertDialog>
    </DropdownMenu>
  );
};

Home Page: src/app/home/page.tsx

import { AuthenticatedContent } from "@/features/auth/AuthenticatedContent";
import { PublicContent } from "@/features/auth/PublicContent";
import { Onboarding } from "@/features/auth/Onboarding";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { redirect } from "next/navigation";
 
export default async function Home() {
  const session = await getServerSession(authOptions);
 
  if (session?.user?.isNew) {
    return <Onboarding />;
  }
 
  if (session) {
    return <AuthenticatedContent />;
  }
 
  return <PublicContent />;
}

Providers: src/components/Providers.tsx

"use client";
 
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { SessionProvider } from "next-auth/react";
import { ThemeProvider } from "@/components/ui/theme-provider";
import { Toaster } from "@/components/ui/toaster";
import { I18nProviderClient } from "@/components/ui/i18n-provider-client";
 
const queryClient = new QueryClient();
 
export function Providers({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <SessionProvider>
      <ThemeProvider attribute="class" defaultTheme="system">
        <I18nProviderClient>
          <QueryClientProvider client={queryClient}>
            {children}
            <Toaster />
          </QueryClientProvider>
        </I18nProviderClient>
      </ThemeProvider>
    </SessionProvider>
  );
}

Utilities

BackButton: /src/components/ui/utils/BackButton.tsx

"use client";
 
import { useRouter } from "next/navigation";
import { Button, ButtonProps } from "@/components/ui/button";
 
export const BackButton = (props: ButtonProps) => {
  const router = useRouter();
  return (
    <Button
      {...props}
      onClick={(e) => {
        router.back();
        props?.onClick?.(e);
      }}
    />
  );
};

Breadcrumb: /src/components/ui/utils/Breadcrumb.tsx

"use client";
 
import { useIsClient } from "@/hooks/useIsClient";
import { ChevronRight } from "lucide-react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { Fragment } from "react";
 
export const Breadcrumb = () => {
  const _pathname = usePathname();
  const pathname = _pathname?.split("/").filter(Boolean) ?? [];
 
  const isClient = useIsClient();
 
  if (!isClient) return;
 
  return (
    <nav aria-label="Breadcrumb" className="mx-4">
      <ol
        role="list"
        className="text-skin-secondary flex items-center gap-1 text-sm"
      >
        {pathname.map((item, index) => (
          <BreadcrumbItem
            item={item}
            index={index}
            pathname={pathname}
            isPrismaId={isPrismaId}
          />
        ))}
      </ol>
    </nav>
  );
};
 
const isPrismaId = (id: string): boolean => {
  const regex = /^[\w-]{25}$/;
  return regex.test(id);
};
 
const formatId = (id: string): string => {
  if (id.length <= 4) {
    return id;
  }
  return `${id.slice(0, 2)}...${id.slice(-2)}`;
};
 
interface BreadcrumbItemProps {
  item: string;
  index: number;
  pathname: string[];
  isPrismaId: (id: string) => boolean;
}
 
const BreadcrumbItem: React.FC<BreadcrumbItemProps> = ({
  item,
  index,
  pathname,
  isPrismaId,
}) => {
  return (
    <Fragment key={item}>
      <li>
        <Link
          href={`/${pathname.slice(0, index + 1).join("/")}`}
          className="block text-xs text-muted-foreground transition hover:text-foreground"
        >
          {isPrismaId(item) ? formatId(item) : item}
        </Link>
      </li>
      {index !== pathname.length - 1 && (
        <ChevronRight className="text-muted-foreground" size={16} />
      )}
    </Fragment>
  );
};

Pagination: src/features/pagination/PaginationButton.tsx

"use client";
 
import { Button } from "@/components/ui/button";
import { useRouter } from "next/navigation";
 
export type PaginationButtonProps = {
  currentPage: number;
  totalPages: number;
};
 
export const PaginationButton = ({
  currentPage,
  totalPages,
}: PaginationButtonProps) => {
  const router = useRouter();
 
  const handlePageChange = (page: number) => {
    if (page > 0 && page <= totalPages) {
      router.push(`/page/${page}`);
    }
  };
 
  return (
    <div className="flex gap-2">
      <Button
        variant="outline"
        size="sm"
        disabled={currentPage === 1}
        onClick={() => handlePageChange(currentPage - 1)}
      >
        Prev
      </Button>
      <Button
        variant="outline"
        size="sm"
        disabled={currentPage === totalPages}
        onClick={() => handlePageChange(currentPage + 1)}
      >
        Next
      </Button>
    </div>
  );
};

On this page