, HTMLDivElement>;
+
+export function PaginationButton({
+ className,
+ createQueryString,
+ isPending,
+ page,
+ pageCount,
+ pathname,
+ per_page,
+ router,
+ siblingCount = 1,
+ sort,
+ startTransition,
+ ...props
+}: PaginationButtonProps) {
+ // Memoize pagination range to avoid unnecessary re-renders
+ const paginationRange = useMemo(() => {
+ const delta = siblingCount + 2;
+
+ const range: ("..." | number)[] = [];
+
+ for (
+ let index = Math.max(2, Number(page) - delta);
+ index <= Math.min(pageCount - 1, Number(page) + delta);
+ index++
+ ) {
+ range.push(index);
+ }
+
+ if (Number(page) - delta > 2) {
+ range.unshift("...");
+ }
+
+ if (Number(page) + delta < pageCount - 1) {
+ range.push("...");
+ }
+
+ range.unshift(1);
+
+ if (pageCount !== 1) {
+ range.push(pageCount);
+ }
+
+ return range;
+ }, [pageCount, page, siblingCount]);
+
+ return (
+
+ {
+ startTransition(() => {
+ router.push(
+ `${pathname}?${createQueryString({
+ page: 1,
+ per_page: per_page || null,
+ sort,
+ })}`,
+ );
+ });
+ }}
+ size="icon"
+ variant="outline"
+ >
+
+
+ {
+ startTransition(() => {
+ router.push(
+ `${pathname}?${createQueryString({
+ page: Number(page) - 1,
+ per_page: per_page || null,
+ sort,
+ })}`,
+ );
+ });
+ }}
+ size="icon"
+ variant="outline"
+ >
+
+
+ {paginationRange.map((pageNumber, index) =>
+ pageNumber === "..." ? (
+
+ ...
+
+ ) : (
+ {
+ startTransition(() => {
+ router.push(
+ `${pathname}?${createQueryString({
+ page: pageNumber,
+ per_page: per_page || null,
+ sort,
+ })}`,
+ );
+ });
+ }}
+ size="icon"
+ variant={Number(page) === pageNumber ? "default" : "outline"}
+ >
+ {pageNumber}
+
+ ),
+ )}
+ {
+ startTransition(() => {
+ router.push(
+ `${pathname}?${createQueryString({
+ page: Number(page) + 1,
+ per_page: per_page || null,
+ sort,
+ })}`,
+ );
+ });
+ }}
+ size="icon"
+ variant="outline"
+ >
+
+
+ {
+ router.push(
+ `${pathname}?${createQueryString({
+ page: pageCount || 10,
+ per_page: per_page || null,
+ sort,
+ })}`,
+ );
+ }}
+ size="icon"
+ variant="outline"
+ >
+
+
+
+ );
+}
diff --git a/src/components/Navigation/Pagination/ProductPager.tsx b/src/components/Navigation/Pagination/ProductPager.tsx
new file mode 100644
index 00000000..cff9dec7
--- /dev/null
+++ b/src/components/Navigation/Pagination/ProductPager.tsx
@@ -0,0 +1,84 @@
+"use client";
+
+import { useTransition } from "react";
+
+import { useRouter } from "next/navigation";
+
+import { Button } from "@/browser/reliverse/ui/Button";
+import {
+ getNextProductIdAction,
+ getPreviousProductIdAction,
+} from "@/server/reliverse/actions/product";
+import consola from "consola";
+
+import type { Product } from "~/db/schema";
+
+import { Icons } from "~/components/Common/Icons";
+
+type ProductPagerProps = {
+ product: Product;
+};
+
+export function ProductPager({ product }: ProductPagerProps) {
+ const router = useRouter();
+ const [isPending, startTransition] = useTransition();
+
+ return (
+
+ {
+ startTransition(async () => {
+ try {
+ const previousProductId = await getPreviousProductIdAction({
+ id: Number(product.id),
+ storeId: product.storeId,
+ });
+
+ router.push(
+ `/dashboard/stores/${product.storeId}/products/${previousProductId}`,
+ );
+ } catch (error) {
+ error instanceof Error
+ ? consola.error(error.message)
+ : consola.error(
+ "Something went wrong, please try again. Please check Pagination/ProductPager.tsx file.",
+ );
+ }
+ });
+ }}
+ size="icon"
+ variant="ghost"
+ >
+
+ Previous product
+
+ {
+ startTransition(async () => {
+ try {
+ const nextProductId = await getNextProductIdAction({
+ id: Number(product.id),
+ storeId: product.storeId,
+ });
+
+ router.push(
+ `/dashboard/stores/${product.storeId}/products/${nextProductId}`,
+ );
+ } catch (error) {
+ error instanceof Error
+ ? consola.error(error.message)
+ : consola.error("Something went wrong, please try again.");
+ }
+ });
+ }}
+ size="icon"
+ variant="ghost"
+ >
+
+ Next product
+
+
+ );
+}
diff --git a/src/components/Navigation/Pagination/StorePager.tsx b/src/components/Navigation/Pagination/StorePager.tsx
new file mode 100644
index 00000000..e36c670e
--- /dev/null
+++ b/src/components/Navigation/Pagination/StorePager.tsx
@@ -0,0 +1,81 @@
+"use client";
+
+import { useTransition } from "react";
+
+import { useRouter } from "next/navigation";
+
+import { Button } from "@/browser/reliverse/ui/Button";
+import {
+ getNextStoreIdAction,
+ getPreviousStoreIdAction,
+} from "@/server/reliverse/actions/store";
+import consola from "consola";
+
+import { Icons } from "~/components/Common/Icons";
+
+type StorePagerProps = {
+ storeId: string;
+ userId: string;
+};
+
+export function StorePager({ storeId, userId }: StorePagerProps) {
+ const router = useRouter();
+ const [isPending, startTransition] = useTransition();
+
+ const handlePreviousStoreClick = () => {
+ startTransition(async () => {
+ try {
+ const previousStoreId = await getPreviousStoreIdAction({
+ id: Number(storeId),
+ userId,
+ });
+
+ router.push(`/dashboard/stores/${previousStoreId}`);
+ } catch (error) {
+ error instanceof Error
+ ? consola.error(error)
+ : consola.error("Something went wrong, please try again.");
+ }
+ });
+ };
+
+ const handleNextStoreClick = () => {
+ startTransition(async () => {
+ try {
+ const nextStoreId = await getNextStoreIdAction({
+ id: Number(storeId),
+ userId,
+ });
+
+ router.push(`/dashboard/stores/${nextStoreId}`);
+ } catch (error) {
+ error instanceof Error
+ ? consola.error(error)
+ : consola.error("Something went wrong, please try again.");
+ }
+ });
+ };
+
+ return (
+
+
+
+ Previous store
+
+
+
+ Next store
+
+
+ );
+}
diff --git a/src/components/Navigation/Pagination/StoreSwitcher.tsx b/src/components/Navigation/Pagination/StoreSwitcher.tsx
new file mode 100644
index 00000000..256d5413
--- /dev/null
+++ b/src/components/Navigation/Pagination/StoreSwitcher.tsx
@@ -0,0 +1,149 @@
+"use client";
+
+import type { ComponentPropsWithoutRef } from "react";
+import { useState } from "react";
+
+import { useRouter } from "next/navigation";
+
+import { Button } from "@/browser/reliverse/ui/Button";
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+ CommandSeparator,
+} from "@/browser/reliverse/ui/Command";
+import { Dialog, DialogTrigger } from "@/browser/reliverse/ui/Dialog";
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/browser/reliverse/ui/Popover";
+import { getRandomPatternStyle } from "@/server/reliverse/pattern";
+import {
+ CaretSortIcon,
+ CheckIcon,
+ PlusCircledIcon,
+} from "@radix-ui/react-icons";
+
+import type { Store } from "~/db/schema";
+
+import { cn } from "~/utils";
+
+type StoreSwitcherProperties = {
+ currentStore: Pick;
+ dashboardRedirectPath: string;
+ stores: Pick[];
+} & ComponentPropsWithoutRef;
+
+export function StoreSwitcher({
+ className,
+ currentStore,
+ dashboardRedirectPath,
+ stores,
+ ...properties
+}: StoreSwitcherProperties) {
+ const router = useRouter();
+ const [isOpen, setIsOpen] = useState(false);
+ const [isDialogOpen, setIsDialogOpen] = useState(false);
+
+ return (
+
+
+
+
+
+ {currentStore.name}
+
+
+
+
+
+
+
+ No stores found.
+
+ {stores.map((store) => (
+ {
+ router.push(`/dashboard/stores/${store.id}`);
+ setIsOpen(false);
+ }}
+ >
+
+ {store.name}
+
+
+ ))}
+
+
+
+
+
+
+ {
+ router.push(dashboardRedirectPath);
+ setIsOpen(false);
+ setIsDialogOpen(true);
+ }}
+ >
+
+ Create store
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/Navigation/Pagination/StoreTabs.tsx b/src/components/Navigation/Pagination/StoreTabs.tsx
new file mode 100644
index 00000000..5369741a
--- /dev/null
+++ b/src/components/Navigation/Pagination/StoreTabs.tsx
@@ -0,0 +1,66 @@
+"use client";
+
+import type { ComponentPropsWithoutRef } from "react";
+
+import { usePathname, useRouter } from "next/navigation";
+
+import { Tabs, TabsList, TabsTrigger } from "@/browser/reliverse/ui/Tabs";
+
+import { cn } from "~/utils";
+
+type StoreTabsProps = {
+ storeId: string;
+} & ComponentPropsWithoutRef;
+
+export function StoreTabs({ className, storeId, ...props }: StoreTabsProps) {
+ const router = useRouter();
+ const pathname = usePathname();
+
+ const tabs = [
+ {
+ href: `/dashboard/stores/${storeId}`,
+ title: "Store",
+ },
+ {
+ href: `/dashboard/stores/${storeId}/products`,
+ title: "Products",
+ },
+ {
+ href: `/dashboard/stores/${storeId}/orders`,
+ title: "Orders",
+ },
+ {
+ href: `/dashboard/stores/${storeId}/analytics`,
+ title: "Analytics",
+ },
+ ];
+
+ return (
+ {
+ router.push(value);
+ }}
+ >
+
+ {tabs.map((tab) => (
+ {
+ router.push(tab.href);
+ }}
+ value={tab.href}
+ >
+ {tab.title}
+
+ ))}
+
+
+ );
+}
diff --git a/src/components/Navigation/SidebarNav.tsx b/src/components/Navigation/SidebarNav.tsx
new file mode 100644
index 00000000..116713db
--- /dev/null
+++ b/src/components/Navigation/SidebarNav.tsx
@@ -0,0 +1,72 @@
+"use client";
+
+import type { HTMLAttributes } from "react";
+
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+
+import type { SidebarNavItem } from "~/types";
+
+import { Icons } from "~/components/Common/Icons";
+import { cn } from "~/utils";
+
+type SidebarNavProps = {
+ items: SidebarNavItem[];
+} & HTMLAttributes;
+
+export function SidebarNav({ className, items, ...props }: SidebarNavProps) {
+ const pathname = usePathname();
+
+ // @ts-expect-error TODO: fix
+ if (!items && items.length > 0) {
+ return null;
+ }
+
+ return (
+
+ {items.map((item, index) => {
+ const Icon = Icons[item.icon || "chevronLeft"];
+
+ return item.href ? (
+
+
+
+ {item.title}
+
+
+ ) : (
+
+ {item.title}
+
+ );
+ })}
+
+ );
+}
diff --git a/src/components/Navigation/SiteFooter.tsx b/src/components/Navigation/SiteFooter.tsx
new file mode 100644
index 00000000..0fc0c5a4
--- /dev/null
+++ b/src/components/Navigation/SiteFooter.tsx
@@ -0,0 +1,147 @@
+import type { ReactNode } from "react";
+
+import Link from "next/link";
+
+import { buttonVariants } from "@/browser/reliverse/ui/Button";
+import { DiscordLogoIcon, GitHubLogoIcon } from "@radix-ui/react-icons";
+import { config } from "@reliverse/core";
+import { Coffee, Music, Store } from "lucide-react";
+
+import { siteConfig } from "~/app";
+import JoinNewsletterForm from "~/components/Forms/JoinNewsletterForm";
+import { ThemesGeneralSwitcher } from "~/components/Switchers/ThemesGeneralSwitcher";
+import { Shell } from "~/components/Wrappers/ShellVariants";
+import { env } from "~/env";
+import { cn } from "~/utils";
+
+type SocialIconLinkProps = {
+ children: ReactNode;
+ href: string;
+ label: string;
+};
+
+const SocialIconLink = ({ children, href, label }: SocialIconLinkProps) => (
+
+ {children}
+
+);
+
+export function SiteFooter() {
+ return (
+
+ );
+}
diff --git a/src/components/Navigation/SiteHeader.tsx b/src/components/Navigation/SiteHeader.tsx
new file mode 100644
index 00000000..03be79db
--- /dev/null
+++ b/src/components/Navigation/SiteHeader.tsx
@@ -0,0 +1,133 @@
+import Link from "next/link";
+
+import { buttonVariants } from "@/browser/reliverse/ui/Button";
+import { DiscordLogoIcon, GitHubLogoIcon } from "@radix-ui/react-icons";
+import { Coffee } from "lucide-react";
+import { getTranslations } from "next-intl/server";
+import { intlProvider } from "reliverse.config";
+
+import { siteConfig } from "~/app";
+import { UserButton } from "~/components/Account/UserButton";
+import Combobox from "~/components/Combobox/Combobox";
+import { CartDialog } from "~/components/Commerce/Cart";
+import { LocaleSwitcher } from "~/components/Common/LocaleSwitcher";
+import { MainMenu } from "~/components/Navigation/MainMenu";
+import { MobileMenu } from "~/components/Navigation/MobileMenu";
+import { ThemesGeneralSwitcher } from "~/components/Switchers/ThemesGeneralSwitcher";
+import { dashboardConfig } from "~/constants/nav-items";
+import { cn } from "~/utils";
+
+export async function SiteHeader() {
+ const t = await getTranslations();
+
+ return (
+
+ );
+}
+
+function ThemeDropdown() {
+ if (!siteConfig.themeToggleEnabled) {
+ return null;
+ }
+
+ return ;
+}
+
+function IntlDropdown() {
+ if (intlProvider !== "disable") {
+ return ;
+ }
+
+ return null;
+}
+
+function DiscordLink() {
+ return (
+
+
+
+ );
+}
+
+function GithubLink() {
+ return (
+
+
+
+ );
+}
+
+function PatreonLink() {
+ return (
+
+
+
+ );
+}
diff --git a/src/components/Navigation/UserMenu.tsx b/src/components/Navigation/UserMenu.tsx
new file mode 100644
index 00000000..f99f363d
--- /dev/null
+++ b/src/components/Navigation/UserMenu.tsx
@@ -0,0 +1,154 @@
+import Link from "next/link";
+
+import {
+ Avatar,
+ AvatarFallback,
+ AvatarImage,
+} from "@/browser/reliverse/ui/Avatar";
+import { Button, buttonVariants } from "@/browser/reliverse/ui/Button";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuGroup,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/browser/reliverse/ui/Dropdown";
+import { SignedIn, SignInButton } from "@clerk/nextjs";
+import { debugEnabled } from "reliverse.config";
+
+import { authProvider } from "~/auth";
+import { authjs } from "~/auth/authjs";
+import { clerk } from "~/auth/clerk";
+import { Icons } from "~/components/Common/Icons";
+import { SignOutButton } from "~/core/auth/authjs/components/sign-out-button";
+import { env } from "~/env";
+import { cn, getInitials } from "~/utils";
+
+export default async function UserMenu() {
+ const user = authProvider === "clerk" ? await clerk() : await authjs();
+
+ const initials = await getInitials(user.name || "TestUser");
+
+ if (debugEnabled) {
+ console.log(user.id, user.name, user.email, user.image);
+ }
+
+ if (user) {
+ return (
+
+
+
+
+
+ {initials}
+
+
+
+
+
+
+
{user.name}
+
+ {user.email}
+
+
+
+
+
+
+
+
+ Stores
+
+
+
+
+
+ Billing
+
+
+
+
+
+ Account
+
+
+
+
+
+ Settings
+
+
+
+
+
+ Purchases
+
+
+
+
+
+ Dashboard
+
+
+
+
+
+ Admin Page
+
+
+
+
+
+ {authProvider === "authjs" && (
+
+
+
+ )}
+ {authProvider === "clerk" &&
+ env.CLERK_SECRET_KEY &&
+ env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY && (
+ <>
+
+
+
+ Log Out
+
+
+ >
+ )}
+
+
+
+ );
+ }
+
+ return ;
+}
diff --git a/src/components/Playground/Boards/BoardList.tsx b/src/components/Playground/Boards/BoardList.tsx
new file mode 100644
index 00000000..5906b1ab
--- /dev/null
+++ b/src/components/Playground/Boards/BoardList.tsx
@@ -0,0 +1,80 @@
+import Link from "next/link";
+
+import { Skeleton } from "@radix-ui/themes";
+import { PlusIcon } from "lucide-react";
+
+import { getUserBoards } from "~/data/other/boards";
+import { genRandomName } from "~/utils";
+
+// import { CreateBoard } from "~/components/Playground/Boards/CreateBoard";
+export async function BoardList() {
+ const boards = await getUserBoards();
+
+ return (
+
+
+ The boards
+ {/* */}
+
+ {boards.length === 0 && (
+
No boards yet
+ )}
+ {boards.length > 0 && (
+
+ {boards.map((board) => (
+
+
+
+ {board.name}
+
+
+ ))}
+
+ )}
+
+ );
+}
+
+// Loading component with its display name
+const BoardListLoading = () => (
+
+ {/* TODO: Make the Skeleton brighter than default on dark background */}
+
+
+ The boards
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {genRandomName()}
+
+
+
+
+
+
+);
+
+// Set the display name to avoid ESLint warning
+BoardListLoading.displayName = "BoardListLoading";
+
+BoardList.Loading = BoardListLoading;
+
+export default BoardList;
diff --git a/src/components/Playground/Boards/CreateBoard.tsx b/src/components/Playground/Boards/CreateBoard.tsx
new file mode 100644
index 00000000..a8072ca3
--- /dev/null
+++ b/src/components/Playground/Boards/CreateBoard.tsx
@@ -0,0 +1,29 @@
+"use client";
+
+import { useActionState } from "react";
+
+import type { MakeAction } from "@/server/reliverse/actions/auth";
+
+import { createBoard } from "@/server/reliverse/actions/auth";
+import { faker } from "@faker-js/faker";
+import { PlusIcon } from "lucide-react";
+
+import { SubmitButton } from "~/components/Playground/Boards/SubmitButton";
+import { genRandomName } from "~/utils";
+
+export function CreateBoard() {
+ const [, dispatch] = useActionState(
+ createBoard as MakeAction, // eslint-disable-next-line unicorn/no-useless-undefined
+ undefined,
+ );
+
+ return (
+
+ );
+}
diff --git a/src/components/Playground/Boards/EditableText.tsx b/src/components/Playground/Boards/EditableText.tsx
new file mode 100644
index 00000000..8a9de7e8
--- /dev/null
+++ b/src/components/Playground/Boards/EditableText.tsx
@@ -0,0 +1,89 @@
+import type { ReactNode } from "react";
+import { useOptimistic, useRef, useState } from "react";
+import { flushSync } from "react-dom";
+
+type EditableTextProps = {
+ buttonClassName: string;
+ buttonLabel: string;
+ children: ReactNode;
+ fieldName: string;
+ inputClassName: string;
+ inputLabel: string;
+ value: string;
+ onSubmit: () => void;
+};
+
+export function EditableText(props: EditableTextProps) {
+ const [value, updateValue] = useOptimistic(
+ props.value,
+ (_, next: string) => next,
+ );
+
+ const [edit, setEdit] = useState(false);
+ const inputRef = useRef(null);
+ const buttonRef = useRef(null);
+
+ const submit = (form: FormData | HTMLFormElement) => {
+ const fd = form instanceof FormData ? form : new FormData(form);
+ const value = fd.get(props.fieldName) as string;
+
+ if (value && value !== props.value) {
+ props.onSubmit();
+ updateValue(value);
+ }
+
+ flushSync(() => {
+ setEdit(false);
+ });
+ buttonRef.current && buttonRef.current.focus();
+ };
+
+ return edit ? (
+
+ ) : (
+ {
+ flushSync(() => {
+ setEdit(true);
+ });
+ inputRef.current && inputRef.current.select();
+ }}
+ ref={buttonRef}
+ type="button"
+ >
+ {value || Edit }
+
+ );
+}
diff --git a/src/components/Playground/Boards/SignIn.tsx b/src/components/Playground/Boards/SignIn.tsx
new file mode 100644
index 00000000..d1373a34
--- /dev/null
+++ b/src/components/Playground/Boards/SignIn.tsx
@@ -0,0 +1,126 @@
+"use client";
+
+import { useActionState, useEffect } from "react";
+
+import {
+ signInWithCredentials,
+ signInWithGithub,
+} from "@/server/reliverse/actions/auth";
+import consola from "consola";
+import { Columns4, Github } from "lucide-react";
+import { signIn } from "next-auth/webauthn";
+import tryToCatch from "try-to-catch";
+
+import { PasskeySVG } from "~/components/Common/Icons/SVG";
+import { SubmitButton } from "~/components/Playground/Boards/SubmitButton";
+
+export function SignInForm(props: {
+ githubEnabled: boolean;
+}) {
+ // @ts-expect-error TODO: Fix ts
+ const [error, dispatch] = useActionState(signInWithCredentials);
+
+ useEffect(() => {
+ if (error) {
+ consola.error(error.error);
+ }
+ }, [error]);
+
+ // @ts-expect-error TODO: Fix ts
+ const [, dispatchPasskey] = useActionState(async () => {
+ await tryToCatch(signIn, "passkey");
+ consola.error("Failed to sign in with passkey");
+ });
+
+ return (
+
+
+
+ Sign in
+
+
+
+
+ or
+
+
+
+
+ );
+}
+
+export function AddPassKey() {
+ // @ts-expect-error TODO: Fix ts
+ const [, dispatch] = useActionState(async () => {
+ await tryToCatch(signIn, "passkey", {
+ action: "register",
+ });
+ consola.error("Failed to register passkey");
+ });
+
+ return (
+
+ );
+}
diff --git a/src/components/Playground/Boards/SubmitButton.tsx b/src/components/Playground/Boards/SubmitButton.tsx
new file mode 100644
index 00000000..cb807609
--- /dev/null
+++ b/src/components/Playground/Boards/SubmitButton.tsx
@@ -0,0 +1,57 @@
+"use client";
+
+import type { ButtonHTMLAttributes, ReactNode } from "react";
+import { useFormStatus } from "react-dom";
+
+import { Spinner } from "@radix-ui/themes";
+import { twMerge } from "tailwind-merge";
+
+// import { createRoot } from "react-dom/client";
+const isString = (a: unknown): a is string => typeof a === "string";
+
+type SubmitButtonProps = {
+ children: ReactNode;
+ icon?: ReactNode;
+ type?: never;
+} & ButtonHTMLAttributes;
+
+export function SubmitButton(props: SubmitButtonProps) {
+ const { pending } = useFormStatus();
+
+ return (
+
+ {props.icon ? (
+ <>
+
+ {props.icon}
+
+ {isString(props.children) ? (
+ {props.children}
+ ) : (
+ props.children
+ )}
+ >
+ ) : (
+
+ {props.children}
+
+ )}
+
+ );
+}
diff --git a/src/components/Playground/Privileges.tsx b/src/components/Playground/Privileges.tsx
new file mode 100644
index 00000000..995490e6
--- /dev/null
+++ b/src/components/Playground/Privileges.tsx
@@ -0,0 +1,43 @@
+"use client";
+
+import type { FormEvent } from "react";
+import { startTransition } from "react";
+
+import { Button } from "@/browser/reliverse/ui/Button";
+import { catchError } from "@/server/reliverse/errors/helpers/auth";
+import consola from "consola";
+
+import { changeUserPrivileges } from "~/core/adm/actions";
+
+export default function ButtonSetPrivileges(
+ userId: string,
+ newRole: "admin" | "user",
+) {
+ function onSubmit(event_: FormEvent) {
+ event_.preventDefault();
+ startTransition(async () => {
+ async function changePrivileges() {
+ try {
+ const session = await changeUserPrivileges({
+ role: newRole,
+ userId,
+ });
+
+ if (session !== undefined) {
+ consola.info(session.res);
+ }
+ } catch (error) {
+ catchError(error);
+ }
+ }
+
+ await changePrivileges();
+ });
+ }
+
+ return (
+
+ );
+}
diff --git a/src/components/Playground/ReactActionState.tsx b/src/components/Playground/ReactActionState.tsx
new file mode 100644
index 00000000..c2e32857
--- /dev/null
+++ b/src/components/Playground/ReactActionState.tsx
@@ -0,0 +1,182 @@
+"use client";
+
+import { useActionState, useOptimistic } from "react";
+
+import { redirect } from "next/navigation";
+
+import { Button } from "@/browser/reliverse/ui/Button";
+import { RedirectType } from "next/dist/client/components/redirect";
+
+// TODO: This file is currently excluded in Million Lint config
+export function UseActionStateExample() {
+ return (
+
+
Example Coming Soon...
+
+
useActionState
+
+
+
+
useActionState: Submit And Redirect
+
+
+
+
useFormStatus
+
+
+
+
useOptimistic
+
+
+
+ );
+}
+
+// async function increment(previousState: number, formData: FormData) {
+function increment(previousState: number) {
+ return previousState + 1;
+}
+
+function SimpleUseActionState() {
+ const [state, formAction] = useActionState(increment, 0);
+
+ return (
+
+ );
+}
+
+type FormTheNameProps = {
+ name: string;
+};
+
+type ReturnResponse = {
+ message: string;
+ ok: boolean;
+};
+
+async function updateName(props: FormTheNameProps): Promise {
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+
+ if (props.name.length < 3) {
+ return {
+ message: "Name is too short",
+ ok: false,
+ };
+ }
+
+ return {
+ message: "Name updated",
+ ok: true,
+ };
+}
+
+const parseFormData = (formData: FormData) => {
+ const name = formData.get("name") as string;
+
+ return {
+ name,
+ };
+};
+
+function FormTheName() {
+ const [res, submitAction, isPending] = useActionState<
+ null | ReturnResponse,
+ FormData
+ >(async (_, formData) => {
+ const error = await updateName(parseFormData(formData));
+
+ if (!error.ok) {
+ return error;
+ }
+
+ return redirect("/res", RedirectType.push);
+ }, null);
+
+ return (
+
+ );
+}
+
+function FormUseFormStatus() {
+ // const status = useFormStatus();
+ // function Submit() {
+ // return (
+ //
+ //
+ // Submit
+ //
+ // {status.pending &&
Submitting...
}
+ //
+ // );
+ // }
+ // const action = async () => {
+ // await new Promise((resolve) => setTimeout(resolve, 1000));
+ // };
+ return (
+ <>
+ {/* */}
+ Soon...
+ >
+ );
+}
+
+const FormUseOptimistic = () => {
+ const [name, setName] = useOptimistic("name");
+ const [res, submitAction, isPending] = useActionState<
+ null | ReturnResponse,
+ FormData
+ >(
+ async (_, formData) => {
+ const props = parseFormData(formData);
+
+ setName(props.name);
+ const error = await updateName(props);
+
+ if (!error.ok) {
+ return error;
+ }
+
+ // redirect to /res without 3rd argument
+ // redirect("/res", RedirectType.push);
+ return null;
+ },
+ null, // redirect to /res
+ "/res",
+ );
+
+ return (
+
+ );
+};
diff --git a/src/components/Products/Catalogue.tsx b/src/components/Products/Catalogue.tsx
new file mode 100644
index 00000000..d7fb13bd
--- /dev/null
+++ b/src/components/Products/Catalogue.tsx
@@ -0,0 +1,7 @@
+import { useTranslations } from "next-intl";
+
+export default function ProductsCatalogue() {
+ const t = useTranslations();
+
+ return {t("store.products.productsCatalogue")} ;
+}
diff --git a/src/components/Providers/AuthProvider.tsx b/src/components/Providers/AuthProvider.tsx
new file mode 100644
index 00000000..36fd9970
--- /dev/null
+++ b/src/components/Providers/AuthProvider.tsx
@@ -0,0 +1,70 @@
+import type { ReactNode } from "react";
+
+import { getClerkLocale } from "@/server/reliverse/clerk/getClerkLocale";
+import { ClerkProvider } from "@clerk/nextjs";
+import { authProvider } from "reliverse.config";
+
+import { env } from "~/env";
+
+export function AuthProvider({
+ children,
+ locale,
+}: { children: ReactNode; locale: string }) {
+ // https://clerk.com/docs/components/customization/localization
+ const clerkLocale = getClerkLocale(locale);
+
+ // if (
+ // authProvider === "clerk" &&
+ // env.CLERK_SECRET_KEY &&
+ // env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY
+ // ) {
+ // return {children} ;
+ // }
+
+ // if (authProvider === "authjs" && env.AUTH_SECRET) {
+ // return {children} ;
+ // }
+
+ // return <>{children}>;
+
+ if (
+ authProvider !== "clerk" ||
+ !env.CLERK_SECRET_KEY ||
+ !env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY
+ ) {
+ return <>{children}>;
+ }
+
+ return (
+ <>
+ {authProvider !== "clerk" ||
+ !env.CLERK_SECRET_KEY ||
+ !env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY ? (
+ <>{children}>
+ ) : (
+
+ {children}
+
+ )}
+ >
+ );
+}
diff --git a/src/components/Providers/FlowbiteTheme.tsx b/src/components/Providers/FlowbiteTheme.tsx
new file mode 100644
index 00000000..e9f93f1d
--- /dev/null
+++ b/src/components/Providers/FlowbiteTheme.tsx
@@ -0,0 +1,87 @@
+import type { CustomFlowbiteTheme } from "flowbite-react";
+
+export const customTheme: CustomFlowbiteTheme = {
+ accordion: {
+ content: {
+ base: "py-5 px-5 last:rounded-b-lg dark:bg-zinc-900 first:rounded-t-lg",
+ },
+ root: {
+ base: "divide-y divide-zinc-200 border-zinc-200 dark:divide-zinc-700 dark:border-zinc-700",
+ flush: {
+ off: "rounded-lg border",
+ on: "border-b",
+ },
+ },
+ title: {
+ arrow: {
+ base: "h-6 w-6 shrink-0",
+ open: {
+ off: "",
+ on: "rotate-180",
+ },
+ },
+ base: `flex w-full items-center justify-between first:rounded-t-lg
+ last:rounded-b-lg py-5 px-5 text-left font-medium text-zinc-500 dark:text-zinc-400`,
+ flush: {
+ off: "hover:bg-zinc-100 focus:ring-4 focus:ring-zinc-200 dark:hover:bg-zinc-800 dark:focus:ring-zinc-800",
+ on: "bg-transparent dark:bg-transparent",
+ },
+ heading: "flex w-full flex-row justify-between",
+ open: {
+ off: "",
+ on: "text-zinc-900 bg-zinc-100 dark:bg-zinc-800 dark:text-white",
+ },
+ },
+ },
+ navbar: {
+ brand: {
+ base: "flex items-center",
+ },
+ collapse: {
+ base: "w-full md:block md:w-auto",
+ hidden: {
+ off: "",
+ on: "hidden",
+ },
+ list: "mt-4 flex flex-col md:mt-0 md:flex-row md:space-x-8 md:text-sm md:font-medium",
+ },
+ link: {
+ active: {
+ off: `border-b border-zinc-100 text-zinc-400 hover:bg-zinc-50
+ dark:border-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-700
+ dark:hover:text-white md:border-0 md:hover:bg-transparent
+ md:hover:text-cyan-700 md:dark:hover:bg-transparent md:dark:hover:text-white`,
+ on: "bg-cyan-700 text-white dark:text-white md:bg-transparent md:text-cyan-700",
+ },
+ base: "block py-2 pr-4 pl-3 md:p-0",
+ disabled: {
+ off: "",
+ on: "text-zinc-400 hover:cursor-not-allowed dark:text-zinc-600",
+ },
+ },
+ root: {
+ base: "bg-black px-2 py-2.5 dark:border-zinc-700 dark:bg-zinc-800 sm:px-4",
+ bordered: {
+ off: "",
+ on: "border",
+ },
+ inner: {
+ base: "mx-auto flex flex-wrap items-center justify-between",
+ fluid: {
+ off: "container",
+ on: "",
+ },
+ },
+ rounded: {
+ off: "",
+ on: "rounded",
+ },
+ },
+ toggle: {
+ base: `inline-flex items-center rounded-lg p-2 text-sm text-zinc-500
+ hover:bg-zinc-100 focus:outline-none focus:ring-2 focus:ring-zinc-200
+ dark:text-zinc-400 dark:hover:bg-zinc-700 dark:focus:ring-zinc-600 md:hidden`,
+ icon: "h-6 w-6 shrink-0",
+ },
+ },
+};
diff --git a/src/components/Providers/NextintlProvider.tsx b/src/components/Providers/NextintlProvider.tsx
new file mode 100644
index 00000000..8e34d9d1
--- /dev/null
+++ b/src/components/Providers/NextintlProvider.tsx
@@ -0,0 +1,46 @@
+"use client";
+
+import type { ReactNode } from "react";
+
+import { NextIntlClientProvider } from "next-intl";
+import _ from "radash";
+
+const noop = () => {};
+
+// @deprecated
+// next-intl: next.js internationalization library
+// @see https://next-intl-docs.vercel.app
+export function NextIntlProvider({
+ children,
+ locale,
+ messages,
+}: {
+ children: ReactNode;
+ locale: string;
+ messages: unknown;
+}) {
+ return (
+ {
+ const nestedMessages = _.get(messages, namespace || "");
+
+ if (!nestedMessages) {
+ return key;
+ }
+
+ if (error.code === "MISSING_MESSAGE") {
+ // @ts-expect-error - default key
+ return nestedMessages.default;
+ }
+
+ // @ts-expect-error TODO: fix key
+ return nestedMessages[key];
+ }}
+ locale={locale} // @ts-expect-error TODO: fix
+ messages={messages}
+ onError={noop}
+ >
+ {children}
+
+ );
+}
diff --git a/src/components/Providers/SessionProvider.tsx b/src/components/Providers/SessionProvider.tsx
new file mode 100644
index 00000000..cd4da6c2
--- /dev/null
+++ b/src/components/Providers/SessionProvider.tsx
@@ -0,0 +1,4 @@
+"use client";
+
+// @see https://github.com/jherr/app-router-auth-using-next-auth */
+export { SessionProvider } from "next-auth/react";
diff --git a/src/components/Providers/ThemeProvider.tsx b/src/components/Providers/ThemeProvider.tsx
new file mode 100644
index 00000000..13b01c2f
--- /dev/null
+++ b/src/components/Providers/ThemeProvider.tsx
@@ -0,0 +1,32 @@
+"use client";
+
+import type { ThemeProviderProps } from "next-themes/dist/types";
+
+import { TooltipProvider } from "@/browser/reliverse/ui/Tooltip";
+import { Theme as RadixThemes } from "@radix-ui/themes";
+import { ThemeProvider as NextThemesProvider } from "next-themes";
+
+export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
+ return (
+
+
+
+ {/* import { Popover as PopoverProvider } from "@/browser/reliverse/ui/Popover"; */}
+ {/* {children} */}
+ {children}
+
+
+
+ );
+}
diff --git a/src/components/Providers/Tooltip.tsx b/src/components/Providers/Tooltip.tsx
new file mode 100644
index 00000000..d2d61813
--- /dev/null
+++ b/src/components/Providers/Tooltip.tsx
@@ -0,0 +1,45 @@
+"use client";
+
+import type { ComponentPropsWithoutRef, ComponentRef } from "react";
+import { forwardRef } from "react";
+
+import * as TooltipPrimitive from "@radix-ui/react-tooltip";
+
+import { cn } from "~/utils";
+
+const TooltipProvider = TooltipPrimitive.Provider;
+const Tooltip = TooltipPrimitive.Root;
+const TooltipTrigger = TooltipPrimitive.Trigger;
+
+const TooltipContent = forwardRef<
+ ComponentRef,
+ ComponentPropsWithoutRef
+>(({ className, sideOffset = 4, ...props }, ref) => (
+
+));
+
+TooltipContent.displayName = TooltipPrimitive.Content.displayName;
+
+export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };
diff --git a/src/components/Sections/Questions/AccordionSection.tsx b/src/components/Sections/Questions/AccordionSection.tsx
new file mode 100644
index 00000000..6dbc699f
--- /dev/null
+++ b/src/components/Sections/Questions/AccordionSection.tsx
@@ -0,0 +1,68 @@
+import {
+ Accordion,
+ AccordionContent,
+ AccordionItem,
+ AccordionTrigger,
+} from "@/browser/reliverse/ui/Accordion";
+import { useTranslations } from "next-intl";
+
+import { FundingPlatforms } from "~/components/Common/funding";
+
+export function AccordionSection() {
+ const t = useTranslations();
+
+ // Set items according to the "faq"
+ // entries in the en-us.json file
+ const items = [1, 2, 3] as const;
+
+ type FaqNumber = (typeof items)[number];
+
+ type TranslationKeys =
+ | `faq.${FaqNumber}.details`
+ | `faq.${FaqNumber}.summary`;
+
+ return (
+
+ {items.map((item) => {
+ const summaryKey = `faq.${item}.summary` as TranslationKeys;
+ const detailsKey = `faq.${item}.details` as TranslationKeys;
+
+ // const itemId = uuid(); // Generate a unique id for each item
+ const itemId = item;
+
+ // Generate a unique id for each item
+ return (
+
+
+ {item}. {t(summaryKey)}
+
+
+ {itemId !== 3 ? (
+
+ {t(detailsKey)}
+
+ ) : (
+
+ {t(detailsKey)}
+
+
+ )}
+
+
+ );
+ })}
+
+ );
+}
diff --git a/src/components/Sections/Questions/AccordionSectionFlowbite.tsx b/src/components/Sections/Questions/AccordionSectionFlowbite.tsx
new file mode 100644
index 00000000..210c8542
--- /dev/null
+++ b/src/components/Sections/Questions/AccordionSectionFlowbite.tsx
@@ -0,0 +1,50 @@
+import {
+ Accordion,
+ AccordionContent,
+ AccordionPanel,
+ AccordionTitle,
+} from "flowbite-react";
+import { useTranslations } from "next-intl";
+import { v4 as uuid } from "uuid";
+
+export function FaqFlowbite() {
+ const t = useTranslations();
+
+ // Set items according to the "faq"
+ // entries in the en-us.json file
+ const items = [1, 2, 3] as const;
+
+ type FaqNumber = (typeof items)[number];
+
+ type TranslationKeys =
+ | `faq.${FaqNumber}.details`
+ | `faq.${FaqNumber}.summary`;
+
+ return (
+
+ {items.map((item) => {
+ const summaryKey = `faq.${item}.summary` as TranslationKeys;
+ const detailsKey = `faq.${item}.details` as TranslationKeys;
+
+ return (
+
+
+ {item}. {t(summaryKey)}
+
+
+
+ {t(detailsKey)}
+
+
+
+ );
+ })}
+
+ );
+}
diff --git a/src/components/Skeletons/Forms/UpdateNotificationFormSkeleton.tsx b/src/components/Skeletons/Forms/UpdateNotificationFormSkeleton.tsx
new file mode 100644
index 00000000..b79fb6dd
--- /dev/null
+++ b/src/components/Skeletons/Forms/UpdateNotificationFormSkeleton.tsx
@@ -0,0 +1,26 @@
+import { Skeleton } from "@/browser/reliverse/ui/Skeleton";
+
+export default function UpdateNotificationFormSkeleton() {
+ return (
+
+ {Array.from({
+ length: 3,
+ }).map((_, index) => (
+
+ ))}
+
+
+ );
+}
diff --git a/src/components/Switchers/ThemesGeneralSwitcher.tsx b/src/components/Switchers/ThemesGeneralSwitcher.tsx
new file mode 100644
index 00000000..c6becb47
--- /dev/null
+++ b/src/components/Switchers/ThemesGeneralSwitcher.tsx
@@ -0,0 +1,112 @@
+"use client";
+
+import type { ButtonProps } from "@/browser/reliverse/ui/Button";
+
+import { Button } from "@/browser/reliverse/ui/Button";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/browser/reliverse/ui/Dropdown";
+import { useIsClient } from "@uidotdev/usehooks";
+import { Laptop, Moon, Sun } from "lucide-react";
+import { useTheme } from "next-themes";
+
+type ThemesGeneralSwitcherProps = {
+ iconClassName?: string;
+} & ButtonProps;
+
+export function ThemesGeneralSwitcher({
+ iconClassName = "mr-2 h-4 w-4",
+ ...props
+}: ThemesGeneralSwitcherProps) {
+ const { setTheme } = useTheme();
+ const isMounted = useIsClient();
+
+ if (!isMounted) {
+ return (
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ Theme
+
+ {
+ setTheme("dark");
+ }}
+ >
+
+ Dark
+
+ {
+ setTheme("light");
+ }}
+ >
+
+ Light
+
+ {
+ setTheme("system");
+ }}
+ >
+
+ System
+
+
+
+ );
+}
diff --git a/src/components/Wrappers/DialogShell.tsx b/src/components/Wrappers/DialogShell.tsx
new file mode 100644
index 00000000..c40c70a4
--- /dev/null
+++ b/src/components/Wrappers/DialogShell.tsx
@@ -0,0 +1,57 @@
+"use client";
+
+import type { HTMLAttributes, ReactNode } from "react";
+import { useEffect } from "react";
+
+import { useRouter } from "next/navigation";
+
+import { Button } from "@/browser/reliverse/ui/Button";
+
+import { Icons } from "~/components/Common/Icons";
+import { cn } from "~/utils";
+
+type DialogShellProps = {
+ children: ReactNode;
+} & HTMLAttributes;
+
+export function DialogShell({
+ children,
+ className,
+ ...props
+}: DialogShellProps) {
+ const router = useRouter();
+
+ useEffect(() => {
+ const handleEsc = (event: KeyboardEvent) => {
+ if (event.key === "Escape") {
+ router.back();
+ }
+ };
+
+ window.addEventListener("keydown", handleEsc);
+
+ return () => {
+ window.removeEventListener("keydown", handleEsc);
+ };
+ }, [router]);
+
+ return (
+
+ {
+ router.back();
+ }}
+ >
+
+ Close
+
+ {children}
+
+ );
+}
diff --git a/src/components/Wrappers/GeneralShell.tsx b/src/components/Wrappers/GeneralShell.tsx
new file mode 100644
index 00000000..de9a17fa
--- /dev/null
+++ b/src/components/Wrappers/GeneralShell.tsx
@@ -0,0 +1,13 @@
+import type { ReactNode } from "react";
+
+export function GeneralShell({
+ children,
+}: {
+ children: ReactNode;
+}) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/components/Wrappers/OrdersTableShell.tsx b/src/components/Wrappers/OrdersTableShell.tsx
new file mode 100644
index 00000000..f1cfc47c
--- /dev/null
+++ b/src/components/Wrappers/OrdersTableShell.tsx
@@ -0,0 +1,102 @@
+"use client";
+
+import { useMemo } from "react";
+
+import type { ColumnDef } from "@tanstack/react-table";
+
+import { Badge } from "@/browser/reliverse/ui/Badge";
+
+import type { Order } from "~/db/schema";
+import type { CheckoutItem } from "~/types";
+
+import { DataTable } from "~/components/Modules/DataTable/DataTable";
+import { DataTableColumnHeader } from "~/components/Modules/DataTable/DataTableColumnHeader";
+import { cn, formatDate, formatPrice } from "~/utils";
+
+type OrdersTableShellProps = {
+ data: Order[];
+ pageCount: number;
+};
+
+export function OrdersTableShell({ data, pageCount }: OrdersTableShellProps) {
+ // Memoize the columns so they don't re-render on every render
+ const columns = useMemo[]>(
+ () => [
+ {
+ accessorKey: "id",
+ header: ({ column }) => (
+
+ ),
+ },
+ {
+ accessorKey: "email",
+ header: ({ column }) => (
+
+ ),
+ },
+ {
+ accessorKey: "stripePaymentIntentStatus",
+ cell: ({ cell }) => (
+
+ {String(cell.getValue())}
+
+ ),
+ header: ({ column }) => (
+
+ ),
+ },
+ {
+ accessorKey: "total",
+ cell: ({ cell }) => formatPrice(cell.getValue() as number),
+ header: ({ column }) => (
+
+ ),
+ },
+ {
+ accessorKey: "items",
+ cell: ({ cell }) => {
+ const checkoutItems = cell.getValue() as CheckoutItem[];
+ let totalItems = 0;
+
+ for (const item of checkoutItems) {
+ totalItems += item.quantity;
+ }
+
+ return {totalItems} ;
+ },
+ header: ({ column }) => (
+
+ ),
+ },
+ {
+ accessorKey: "createdAt",
+ cell: ({ cell }) => formatDate(cell.getValue() as Date),
+ enableColumnFilter: false,
+ header: ({ column }) => (
+
+ ),
+ },
+ ],
+ [],
+ );
+
+ return (
+
+ );
+}
diff --git a/src/components/Wrappers/PageLayout.tsx b/src/components/Wrappers/PageLayout.tsx
new file mode 100644
index 00000000..40d2e9a5
--- /dev/null
+++ b/src/components/Wrappers/PageLayout.tsx
@@ -0,0 +1,42 @@
+import type { ReactNode } from "react";
+import Balancer from "react-wrap-balancer";
+
+import { Shell } from "~/components/Wrappers/ShellVariants";
+import { cn, typography } from "~/utils";
+
+type Props = {
+ children?: ReactNode;
+ title?: string;
+ variant?: "centered" | "default" | "markdown" | "sidebar";
+};
+
+export default function PageLayout({ children, title, variant }: Props) {
+ return (
+
+
+ {title && (
+
+ {title}
+
+ )}
+ {children}
+
+
+ );
+}
diff --git a/src/components/Wrappers/ProductsTableShell.tsx b/src/components/Wrappers/ProductsTableShell.tsx
new file mode 100644
index 00000000..9c75e3bc
--- /dev/null
+++ b/src/components/Wrappers/ProductsTableShell.tsx
@@ -0,0 +1,259 @@
+"use client";
+
+import { useMemo, useState, useTransition } from "react";
+
+import Link from "next/link";
+
+import type { ColumnDef } from "@tanstack/react-table";
+
+import { Badge } from "@/browser/reliverse/ui/Badge";
+import { Button } from "@/browser/reliverse/ui/Button";
+import { Checkbox } from "@/browser/reliverse/ui/Checkbox";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuTrigger,
+} from "@/browser/reliverse/ui/Dropdown";
+import { DotsHorizontalIcon } from "@radix-ui/react-icons";
+
+import type { Product } from "~/db/schema";
+
+import { DataTable } from "~/components/Modules/DataTable/DataTable";
+import { DataTableColumnHeader } from "~/components/Modules/DataTable/DataTableColumnHeader";
+import { products } from "~/db/schema";
+import { formatDate, formatPrice } from "~/utils";
+
+type ProductsTableShellProps = {
+ data: Product[];
+ pageCount: number;
+ storeId: string;
+};
+
+export function ProductsTableShell({
+ data,
+ pageCount,
+ storeId,
+}: ProductsTableShellProps) {
+ const [isPending, startTransition] = useTransition();
+
+ const [, setSelectedRowIds] = useState([]);
+
+ // Memoize the columns so they don't re-render on every render
+ const columns = useMemo[]>(
+ () => [
+ {
+ id: "select",
+ cell: ({ row }) => (
+ {
+ row.toggleSelected(!!value);
+ // @ts-expect-error TODO: fix id type
+ setSelectedRowIds((previous) =>
+ value
+ ? [...previous, row.original.id]
+ : previous.filter(
+ (
+ id, // @ts-expect-error TODO: fix id type
+ ) => id !== row.original.id,
+ ),
+ );
+ }}
+ />
+ ),
+ enableHiding: false,
+ enableSorting: false,
+ header: ({ table }) => (
+ {
+ table.toggleAllPageRowsSelected(!!value);
+ // @ts-expect-error TODO: fix id type
+ setSelectedRowIds((previous) =>
+ previous.length === data.length
+ ? []
+ : data.map((row) => row.id),
+ );
+ }}
+ />
+ ),
+ },
+ {
+ accessorKey: "name",
+ header: ({ column }) => (
+
+ ),
+ },
+ {
+ accessorKey: "category",
+ cell: ({ cell }) => {
+ const categories = Object.values(products.category.enumValues);
+ const category = cell.getValue() as Product["category"];
+
+ // @ts-expect-error TODO: fix id type
+ if (!categories.includes(category)) {
+ return null;
+ }
+
+ return (
+
+ {category}
+
+ );
+ },
+ header: ({ column }) => (
+
+ ),
+ },
+ {
+ accessorKey: "price",
+ cell: ({ cell }) => formatPrice(cell.getValue() as number),
+ header: ({ column }) => (
+
+ ),
+ },
+ {
+ accessorKey: "inventory",
+ header: ({ column }) => (
+
+ ),
+ },
+ {
+ accessorKey: "rating",
+ header: ({ column }) => (
+
+ ),
+ },
+ {
+ accessorKey: "createdAt",
+ cell: ({ cell }) => formatDate(cell.getValue() as Date),
+ enableColumnFilter: false,
+ header: ({ column }) => (
+
+ ),
+ },
+ {
+ id: "actions",
+ cell: ({ row }) => (
+
+
+
+
+
+
+
+
+
+ Edit
+
+
+
+ View
+
+
+ {
+ startTransition(() => {
+ row.toggleSelected(false);
+
+ // toast.promise(
+
+ // deleteProductAction({
+
+ // id: row.original.id,
+
+ // storeId,
+
+ // }),
+
+ // {
+
+ // loading: "Deleting...",
+
+ // success: () => "Product deleted successfully.",
+
+ // error: (err: unknown) => catchError(err),
+
+ // },
+
+ // );
+ });
+ }}
+ >
+ Delete
+ ⌘⌫
+
+
+
+ ),
+ },
+ ],
+ [data, isPending, storeId],
+ );
+
+ function deleteSelectedRows() {
+ // toast.promise(
+ // Promise.all(
+ // selectedRowIds.map((id) =>
+ // deleteProductAction({
+ // id,
+ // storeId,
+ // }),
+ // ),
+ // ),
+ // {
+ // loading: "Deleting...",
+ // success: () => {
+ // setSelectedRowIds([]);
+ // return "Products deleted successfully.";
+ // },
+ // error: (err: unknown) => {
+ // setSelectedRowIds([]);
+ // return catchError(err);
+ // },
+ // },
+ // ); */
+ }
+
+ return (
+ {
+ deleteSelectedRows();
+ }}
+ filterableColumns={[
+ {
+ id: "category",
+ options: products.category.enumValues.map((category) => ({
+ label: `${category.charAt(0).toUpperCase()}${category.slice(1)}`,
+ value: category,
+ })),
+ title: "Category",
+ },
+ ]}
+ newRowLink={`/dashboard/stores/${storeId}/products/new`}
+ pageCount={pageCount}
+ searchableColumns={[
+ {
+ id: "name",
+ title: "names",
+ },
+ ]}
+ />
+ );
+}
diff --git a/src/components/Wrappers/ShellVariants.tsx b/src/components/Wrappers/ShellVariants.tsx
new file mode 100644
index 00000000..2f2d91db
--- /dev/null
+++ b/src/components/Wrappers/ShellVariants.tsx
@@ -0,0 +1,60 @@
+import type { ElementType, HTMLAttributes } from "react";
+
+import type { VariantProps } from "class-variance-authority";
+
+import { cva } from "class-variance-authority";
+
+import { cn } from "~/utils";
+
+const shellVariants = cva(
+ `
+ grid items-center gap-8 pb-8 pt-6
+
+ md:py-8
+ `,
+ {
+ defaultVariants: {
+ variant: "default",
+ },
+ variants: {
+ variant: {
+ centered: "container flex h-dvh max-w-2xl flex-col justify-center",
+ default: "container",
+ markdown: `
+ container max-w-3xl gap-0 py-8
+
+ lg:py-10
+
+ md:py-10
+ `,
+ sidebar: "",
+ },
+ },
+ },
+);
+
+type ShellProps = {
+ as?: ElementType;
+} & HTMLAttributes &
+ VariantProps;
+
+function Shell({
+ as: Comp = "main",
+ className,
+ variant,
+ ...props
+}: ShellProps) {
+ return (
+
+ );
+}
+
+export { Shell };
diff --git a/src/constants/metadata.ts b/src/constants/metadata.ts
new file mode 100644
index 00000000..105821ce
--- /dev/null
+++ b/src/constants/metadata.ts
@@ -0,0 +1,15 @@
+// Did you know that you can edit all settings of this file headlessly?
+// Just run pnpm reli:setup and configure the advanced settings. Perfect!
+export default {
+ name: "Relivator",
+ appNameDesc: "Relivator 1.2.6: Next.js 15, React 19, TailwindCSS Template",
+ appPublisher: "Reliverse",
+ appVersion: "1.2.6",
+ author: {
+ email: "blefnk@gmail.com",
+ fullName: "Nazar Kornienko",
+ handle: "blefnk",
+ handleAt: "@blefnk",
+ url: "https://github.com/blefnk",
+ },
+};
diff --git a/src/constants/nav-items.ts b/src/constants/nav-items.ts
new file mode 100644
index 00000000..f64df2a3
--- /dev/null
+++ b/src/constants/nav-items.ts
@@ -0,0 +1,54 @@
+import type { SidebarNavItem } from "~/types";
+
+type DashboardConfig = {
+ sidebarNav: SidebarNavItem[];
+};
+
+// You can keep it in sync with similar:
+// src/components/navigation/user-menu.tsx
+export const dashboardConfig: DashboardConfig = {
+ sidebarNav: [
+ {
+ href: "/dashboard/stores",
+ icon: "store",
+ items: [],
+ title: "Stores",
+ },
+ {
+ href: "/dashboard/billing",
+ icon: "billing",
+ items: [],
+ title: "Billing",
+ },
+ {
+ href: "/dashboard/account",
+ icon: "user",
+ items: [],
+ title: "Account",
+ },
+ {
+ href: "/dashboard/settings",
+ icon: "settings",
+ items: [],
+ title: "Settings",
+ },
+ {
+ href: "/dashboard/purchases",
+ icon: "dollarSign",
+ items: [],
+ title: "Purchases",
+ },
+ {
+ href: "/dashboard",
+ icon: "laptop",
+ items: [],
+ title: "Dashboard",
+ },
+ {
+ href: "/dashboard/admin",
+ icon: "terminal",
+ items: [],
+ title: "Admin Page",
+ },
+ ],
+};
diff --git a/src/constants/navigation.ts b/src/constants/navigation.ts
new file mode 100644
index 00000000..764c1542
--- /dev/null
+++ b/src/constants/navigation.ts
@@ -0,0 +1,70 @@
+export const locales = [
+ "de",
+ "en",
+ "es",
+ "fa",
+ "fr",
+ "hi",
+ "it",
+ "pl",
+ "tr",
+ "uk",
+ "zh",
+] as const;
+
+// Labels for each supported locale, used
+// for displaying human-readable names
+const labels = {
+ de: "German",
+ en: "English",
+ es: "Spanish",
+ fa: "Persian",
+ fr: "French",
+ hi: "Hindi",
+ it: "Italian",
+ pl: "Polish",
+ tr: "Turkish",
+ uk: "Ukrainian",
+ zh: "Chinese",
+} as const;
+
+export const getLocaleLabels = (translateLanguages: boolean) => {
+ if (translateLanguages) {
+ return {
+ de: "Deutsch",
+ en: "English",
+ es: "Español",
+ fa: "فارسی",
+ fr: "Français",
+ hi: "हिन्दी",
+ it: "Italiano",
+ pl: "Polski",
+ tr: "Türkçe",
+ uk: "Українська",
+ zh: "中文",
+ } as const;
+ }
+
+ return labels;
+};
+
+// Type representing valid locale strings
+export type Locale = (typeof locales)[number];
+
+export const defaultLocale: Locale = "en" as const;
+
+export const localeFlags: {
+ [key in Locale]: string;
+} = {
+ de: "🇩🇪",
+ en: "🇬🇧",
+ es: "🇪🇸",
+ fa: "🇮🇷",
+ fr: "🇫🇷",
+ hi: "🇮🇳",
+ it: "🇮🇹",
+ pl: "🇵🇱",
+ tr: "🇹🇷",
+ uk: "🇺🇦",
+ zh: "🇨🇳",
+};
diff --git a/src/constants/products.ts b/src/constants/products.ts
new file mode 100644
index 00000000..4df82f1e
--- /dev/null
+++ b/src/constants/products.ts
@@ -0,0 +1,220 @@
+import type { Product } from "~/db/schema";
+import type { Option } from "~/types";
+
+export const sortOptions = [
+ {
+ label: "Date: Old to new",
+ value: "createdAt.asc",
+ },
+ {
+ label: "Date: New to old",
+ value: "createdAt.desc",
+ },
+ {
+ label: "Price: Low to high",
+ value: "price.asc",
+ },
+ {
+ label: "Price: High to low",
+ value: "price.desc",
+ },
+ {
+ label: "Alphabetical: A to Z",
+ value: "name.asc",
+ },
+ {
+ label: "Alphabetical: Z to A",
+ value: "name.desc",
+ },
+];
+
+export const productCategories = [
+ {
+ image: "/images/skateboard-one.webp",
+ subcategories: [
+ {
+ description: "The board itself.",
+ image: "/images/deck-one.webp",
+ slug: "decks",
+ title: "Decks",
+ },
+ {
+ description: "The wheels that go on the board.",
+ image: "/images/wheel-one.webp",
+ slug: "wheels",
+ title: "Wheels",
+ },
+ {
+ description: "The trucks that go on the board.",
+ image: "/images/truck-one.webp",
+ slug: "trucks",
+ title: "Trucks",
+ },
+ {
+ description: "The bearings that go in the wheels.",
+ image: "/images/bearing-one.webp",
+ slug: "bearings",
+ title: "Bearings",
+ },
+ {
+ description: "The griptape that goes on the board.",
+ image: "/images/griptape-one.webp",
+ slug: "griptape",
+ title: "Griptape",
+ },
+ {
+ description: "The hardware that goes on the board.",
+ image: "/images/hardware-one.webp",
+ slug: "hardware",
+ title: "Hardware",
+ },
+ {
+ description: "The tools that go with the board.",
+ image: "/images/tool-one.webp",
+ slug: "tools",
+ title: "Tools",
+ },
+ ],
+ title: "furniture",
+ },
+ {
+ image: "/images/clothing-one.webp",
+ subcategories: [
+ {
+ description: "Cool and comfy tees for effortless style.",
+ slug: "t-shirts",
+ title: "T-shirts",
+ },
+ {
+ description: "Cozy up in trendy hoodies.",
+ slug: "hoodies",
+ title: "Hoodies",
+ },
+ {
+ description: "Relaxed and stylish pants for everyday wear.",
+ slug: "pants",
+ title: "Pants",
+ },
+ {
+ description: "Stay cool with casual and comfortable shorts.",
+ slug: "shorts",
+ title: "Shorts",
+ },
+ {
+ description: "Top off the look with stylish and laid-back hats.",
+ slug: "hats",
+ title: "Hats",
+ },
+ ],
+ title: "clothing",
+ },
+ {
+ image: "/images/shoe-one.webp",
+ subcategories: [
+ {
+ description: "Rad low tops shoes for a stylish low-profile look.",
+ slug: "low-tops",
+ title: "Low Tops",
+ },
+ {
+ description: "Elevate the style with rad high top shoes.",
+ slug: "high-tops",
+ title: "High Tops",
+ },
+ {
+ description: "Effortless style with rad slip-on shoes.",
+ slug: "slip-ons",
+ title: "Slip-ons",
+ },
+ {
+ description: "Performance-driven rad shoes for the pros.",
+ slug: "pros",
+ title: "Pros",
+ },
+ {
+ description: "Timeless style with rad classic shoes.",
+ slug: "classics",
+ title: "Classics",
+ },
+ ],
+ title: "tech",
+ },
+ {
+ image: "/images/backpack-one.webp",
+ subcategories: [
+ {
+ description: "Essential tools for maintaining the skateboard, all rad.",
+ slug: "skate-tools",
+ title: "Skate Tools",
+ },
+ {
+ description: "Upgrade the ride with our rad selection of bushings.",
+ slug: "bushings",
+ title: "Bushings",
+ },
+ {
+ description:
+ "Enhance the skateboard's performance with rad shock and riser pads.",
+ slug: "shock-riser-pads",
+ title: "Shock & Riser Pads",
+ },
+ {
+ description:
+ "Add creativity and style to the tricks with our rad skate rails.",
+ slug: "skate-rails",
+ title: "Skate Rails",
+ },
+ {
+ description: "Keep the board gliding smoothly with our rad skate wax.",
+ slug: "wax",
+ title: "Wax",
+ },
+ {
+ description: "Keep the feet comfy and stylish with our rad socks.",
+ slug: "socks",
+ title: "Socks",
+ },
+ {
+ description: "Carry the gear in style with our rad backpacks.",
+ slug: "backpacks",
+ title: "Backpacks",
+ },
+ ],
+ title: "accessories",
+ },
+] satisfies {
+ subcategories: {
+ description?: string;
+ image?: string;
+ slug: string;
+ title: string;
+ }[];
+ image: string;
+ title: Product["category"];
+}[];
+
+export const productTags = [
+ "new",
+ "sale",
+ "bestseller",
+ "featured",
+ "popular",
+ "trending",
+ "limited",
+ "exclusive",
+];
+
+export function getSubcategories(category?: string): Option[] {
+ if (!category) {
+ return [];
+ }
+
+ const categoryObject = productCategories.find((c) => c.title === category);
+
+ return (
+ categoryObject?.subcategories.map((s) => ({
+ label: s.title,
+ value: s.slug,
+ })) || []
+ );
+}
diff --git a/src/constants/stores.ts b/src/constants/stores.ts
new file mode 100644
index 00000000..fe648eb1
--- /dev/null
+++ b/src/constants/stores.ts
@@ -0,0 +1,37 @@
+export const storeSortOptions = [
+ {
+ label: "Item count: Low to high",
+ value: "productCount.asc",
+ },
+ {
+ label: "Item count: High to low",
+ value: "productCount.desc",
+ },
+ {
+ label: "Date: Old to new",
+ value: "createdAt.asc",
+ },
+ {
+ label: "Date: New to old",
+ value: "createdAt.desc",
+ },
+ {
+ label: "Alphabetical: A to Z",
+ value: "name.asc",
+ },
+ {
+ label: "Alphabetical: Z to A",
+ value: "name.desc",
+ },
+];
+
+export const storeStatusOptions = [
+ {
+ label: "Active",
+ value: "active",
+ },
+ {
+ label: "Inactive",
+ value: "inactive",
+ },
+];
diff --git a/src/core/adm/actions.ts b/src/core/adm/actions.ts
index b38262e7..92dd4807 100644
--- a/src/core/adm/actions.ts
+++ b/src/core/adm/actions.ts
@@ -1,56 +1,76 @@
-"use server";
+import { redirect } from "next/navigation";
import { eq } from "drizzle-orm";
-import { db } from "~/data/db";
-import type { User } from "~/data/db/schema";
-import { users } from "~/data/db/schema";
-import { redirect } from "~/navigation";
-import { getServerAuthSession } from "~/utils/auth/users";
+import { authjs } from "~/auth/authjs";
+import { db } from "~/db";
+import { users } from "~/db/schema";
-interface changeUserPrivilegiesProps {
- role: "user" | "admin";
+type ChangeUserPrivilegesProps = {
+ role: "admin" | "user";
userId: string;
-}
+};
-export async function changeUserPrivilegies({
+export async function changeUserPrivileges({
role,
userId,
-}: changeUserPrivilegiesProps) {
- const session = await getServerAuthSession();
- if (!session) return redirect("/auth");
+}: ChangeUserPrivilegesProps) {
+ // const session = await authjs();
+ const session = await authjs();
+
+ if (!session) {
+ return redirect("/auth");
+ }
try {
- const admin: User = await db.query.users.findFirst({
- columns: { role: true },
+ const admin = await db.query.users.findFirst({
+ columns: {
+ role: true,
+ },
where: eq(users.id, session.id),
});
+
if (!admin || admin.role !== "admin") {
return {
- result:
- "You need to have 'admin' role in 'users' table to change user privileges",
+ res: "You need to have 'admin' role in 'users' table to change privileges",
};
}
const user = await db.query.users.findFirst({
where: eq(users.id, userId),
});
- if (!user) return { result: "User not found" };
- const success: boolean = await db
+ if (!user) {
+ return {
+ res: "User not found",
+ };
+ }
+
+ const success = await db
.update(users)
- .set({ role } as User)
+ .set({
+ role,
+ })
.where(eq(users.id, userId));
- if (success) return { result: `User role was changed to '${role}'` };
- else return { result: "Something wrong when switching user role..." };
+ if (success) {
+ return {
+ res: `User role was changed to '${role}'`,
+ };
+ }
+
+ return {
+ res: "Something wrong when switching user role...",
+ };
} catch (error) {
if (error instanceof Error) {
- console.error("❌ Error in changeUserPrivilegies:", error.message);
- return { result: `Error when switching user role: ${error.message}` };
- } else {
- console.error("❌ Unknown error in changeUserPrivilegies");
- return { result: "An unknown error occurred" };
+ return {
+ res: `Error when switching user role: ${error.message}`,
+ };
}
+
+ return {
+ res: "An unknown error occurred",
+ };
}
}
diff --git a/src/core/auth/authjs/islands/sign-out-button.tsx b/src/core/auth/authjs/components/sign-out-button.tsx
similarity index 83%
rename from src/core/auth/authjs/islands/sign-out-button.tsx
rename to src/core/auth/authjs/components/sign-out-button.tsx
index 07c929f4..c8abb044 100644
--- a/src/core/auth/authjs/islands/sign-out-button.tsx
+++ b/src/core/auth/authjs/components/sign-out-button.tsx
@@ -1,18 +1,17 @@
"use client";
+import { Button } from "@/browser/reliverse/ui/Button";
import { signOut } from "next-auth/react";
-import { Button } from "~/islands/primitives/button";
-
export function SignOutButton() {
return (
{
event.preventDefault();
void signOut();
}}
size="sm"
- className="px-4"
>
Sign Out
diff --git a/src/core/auth/authjs/index-old.ts b/src/core/auth/authjs/index-old.ts
new file mode 100644
index 00000000..26831c7f
--- /dev/null
+++ b/src/core/auth/authjs/index-old.ts
@@ -0,0 +1,107 @@
+import type { NextAuthConfig } from "next-auth";
+import type { Provider } from "next-auth/providers/index";
+
+import { DrizzleAdapter } from "@auth/drizzle-adapter";
+import DiscordProvider from "next-auth/providers/discord";
+import GithubProvider from "next-auth/providers/github";
+import GoogleProvider from "next-auth/providers/google";
+
+import { db } from "~/db";
+import { env } from "~/env";
+
+// 🔴 DEPRECATED AND POSSIBLY WILL BE REMOVED IN RELIVATOR 1.3.0 🔴 ||
+// Starting Relivator 1.3.0, it can be added by using pnpm reliverse ||
+// ================================================================= ||
+// @see https://github.com/jherr/app-router-auth-using-next-auth
+// @see https://github.com/rexfordessilfie/next-auth-account-linking
+// @see https://github.com/steven-tey/dub/blob/main/apps/web/lib/auth/index.ts
+// @see https://github.com/t3-oss/create-t3-app/blob/next/cli/template/extras/src/server/auth-app/with-drizzle.ts
+export type {
+ Account,
+ DefaultSession,
+ Profile,
+ Session,
+ User,
+} from "@auth/core/types";
+
+// Choose the appropriate table based on the provider
+// let table;
+// if (env.DATABASE_URL.startsWith("postgres://")) {
+// table = pgTable;
+// } else if (env.DATABASE_URL.startsWith("mysql://")) {
+// table = mysqlTable;
+// } else if (env.DATABASE_URL.startsWith("db.sqlite")) {
+// table = sqliteTable;
+// } else {
+// consola.error("Invalid DATABASE_URL");
+// }
+// NextAuth.js Providers Configuration
+// ===================================
+// @see https://next-auth.js.org/providers/discord
+// @see https://next-auth.js.org/providers/github
+// @see https://next-auth.js.org/providers/google
+// Note: Normally, when you sign in with an OAuth provider and another account with the same
+// email address already exists, the accounts are not linked automatically. Automatic account
+// linking on sign in is not secure between arbitrary providers and is disabled by default.
+// @see https://github.com/nextauthjs/next-auth/blob/main/packages/core/src/lib/routes/callback/handle-login.ts#L174
+// @see https://github.com/nextauthjs/next-auth/blob/main/docs/docs/guides/providers/custom-provider.md?plain=1#L83
+// @see https://github.com/boxyhq/next-auth/blob/main/docs/docs/ref/adapters/index.md?plain=1#L175
+// @see https://next-auth.js.org/configuration/providers/oauth#allowdangerousemailaccountlinking-option
+// @see https://github.com/nextauthjs/next-auth/blob/main/packages/core/src/providers/oauth.ts#L210
+// @see https://github.com/dustij/dbudget/blob/main/src/app/api/auth/%5B...nextauth%5D/options.ts
+// todo: try to implement our own safe account linking logic if possible
+// @see https://authjs.dev/ref/core/adapters#linkaccount
+//
+const providers = [
+ env.AUTH_DISCORD_ID &&
+ env.AUTH_DISCORD_SECRET &&
+ DiscordProvider({
+ allowDangerousEmailAccountLinking: true,
+ clientId: env.AUTH_DISCORD_ID,
+ clientSecret: env.AUTH_DISCORD_SECRET,
+ }),
+ env.AUTH_GITHUB_ID &&
+ env.AUTH_GITHUB_SECRET &&
+ GithubProvider({
+ allowDangerousEmailAccountLinking: true,
+ clientId: env.AUTH_GITHUB_ID,
+ clientSecret: env.AUTH_GITHUB_SECRET,
+ }),
+ env.AUTH_GOOGLE_ID &&
+ env.AUTH_GOOGLE_SECRET &&
+ GoogleProvider({
+ allowDangerousEmailAccountLinking: true,
+ clientId: env.AUTH_GOOGLE_ID,
+ clientSecret: env.AUTH_GOOGLE_SECRET,
+ // eslint-disable-next-line @stylistic/max-len
+ }), // // ...add more authjs providers here // Most other providers require a bit more work than the Discord provider. For example, the
+ // GitHub provider requires you to add the `refresh_token_expires_in` field to the Account
+ // model. Refer to the NextAuth.js docs for the provider you want to use. Example:
+ // @see https://next-auth.js.org/providers/github
+ //
+].filter(Boolean) as Provider[];
+
+// Options for NextAuth.js used to configure adapters, providers, callbacks, etc.
+// @see https://next-auth.js.org/providers
+// @see https://authjs.dev/ref/adapter/drizzle
+// @see https://next-auth.js.org/configuration/options
+// @see https://next-auth.js.org/configuration/callbacks
+export const authOptions: NextAuthConfig = {
+ adapter: DrizzleAdapter(db),
+ callbacks: {
+ session: ({ session, user }) => ({
+ ...session,
+ user: {
+ ...session,
+ id: user.id,
+ },
+ }),
+ },
+ pages: {
+ newUser: "/auth",
+ signIn: "/auth/sign-in",
+ signOut: "/auth/sign-out",
+ },
+ providers,
+ secret: env.AUTH_SECRET,
+};
diff --git a/src/core/auth/authjs/index.ts b/src/core/auth/authjs/index.ts
deleted file mode 100644
index 2118d23a..00000000
--- a/src/core/auth/authjs/index.ts
+++ /dev/null
@@ -1,148 +0,0 @@
-/**
- * @see https://github.com/jherr/app-router-auth-using-next-auth
- * @see https://github.com/rexfordessilfie/next-auth-account-linking
- * @see https://github.com/t3-oss/create-t3-app/blob/next/cli/template/extras/src/server/auth-app/with-drizzle.ts
- * @see https://github.com/steven-tey/dub/blob/main/apps/web/lib/auth/index.ts
- */
-
-import { DrizzleAdapter } from "@auth/drizzle-adapter";
-import {
- getServerSession,
- type DefaultSession,
- type NextAuthOptions,
-} from "next-auth";
-import DiscordProvider from "next-auth/providers/discord";
-import GithubProvider from "next-auth/providers/github";
-import GoogleProvider from "next-auth/providers/google";
-import type { Provider } from "next-auth/providers/index";
-import { getLocale } from "next-intl/server";
-
-import { db } from "~/data/db";
-import { mysqlTable } from "~/data/db/schema/mysql";
-import { pgTable } from "~/data/db/schema/pgsql";
-import { env } from "~/env.mjs";
-
-// Choose the appropriate table based on the provider
-let table;
-if (env.DATABASE_URL.startsWith("postgres://")) table = pgTable;
-else if (env.DATABASE_URL.startsWith("mysql://")) table = mysqlTable;
-else throw new Error("Invalid DATABASE_URL");
-
-/**
- * Module augmentation for `next-auth` types.
- * Allows us to add custom properties to the
- * `session` object and keep type safety.
- *
- * Returned by useSession, getSession and received
- * as a prop on the SessionProvider React Context
- *
- * @see https://next-auth.js.org/getting-started/typescript#module-augmentation
- */
-declare module "next-auth" {
- interface Session extends DefaultSession {
- user: {
- id: string;
- name: string;
- email: string;
- image?: string;
- // ...other properties
- // role: UserRole;
- } & DefaultSession["user"];
- }
- // interface User {
- // ...other properties
- // role: UserRole;
- // }
-}
-
-/**
- * NextAuth.js Providers Configuration
- * ===================================
- * @see https://next-auth.js.org/providers/discord
- * @see https://next-auth.js.org/providers/github
- * @see https://next-auth.js.org/providers/google
- *
- * Note: Normally, when you sign in with an OAuth provider and another account with the same
- * email address already exists, the accounts are not linked automatically. Automatic account
- * linking on sign in is not secure between arbitrary providers and is disabled by default.
- * @see https://github.com/nextauthjs/next-auth/blob/main/packages/core/src/lib/routes/callback/handle-login.ts#L174
- * @see https://github.com/nextauthjs/next-auth/blob/main/docs/docs/guides/providers/custom-provider.md?plain=1#L83
- * @see https://github.com/boxyhq/next-auth/blob/main/docs/docs/reference/adapters/index.md?plain=1#L175
- * @see https://next-auth.js.org/configuration/providers/oauth#allowdangerousemailaccountlinking-option
- * @see https://github.com/nextauthjs/next-auth/blob/main/packages/core/src/providers/oauth.ts#L210
- * @see https://github.com/dustij/dbudget/blob/main/src/app/api/auth/%5B...nextauth%5D/options.ts
- *
- * todo: try to implement our own safe account linking logic if possible
- * @see https://authjs.dev/reference/core/adapters#linkaccount
- */
-const providers = [
- env.DISCORD_CLIENT_ID &&
- env.DISCORD_CLIENT_SECRET &&
- DiscordProvider({
- clientId: env.DISCORD_CLIENT_ID,
- clientSecret: env.DISCORD_CLIENT_SECRET,
- allowDangerousEmailAccountLinking: true,
- }),
- env.GITHUB_CLIENT_ID &&
- env.GITHUB_CLIENT_SECRET &&
- GithubProvider({
- clientId: env.GITHUB_CLIENT_ID,
- clientSecret: env.GITHUB_CLIENT_SECRET,
- allowDangerousEmailAccountLinking: true,
- }),
- env.GOOGLE_CLIENT_ID &&
- env.GOOGLE_CLIENT_SECRET &&
- GoogleProvider({
- clientId: env.GOOGLE_CLIENT_ID,
- clientSecret: env.GOOGLE_CLIENT_SECRET,
- allowDangerousEmailAccountLinking: true,
- }),
- /**
- * ...add more authjs providers here
- *
- * Most other providers require a bit more work than the Discord provider. For example, the
- * GitHub provider requires you to add the `refresh_token_expires_in` field to the Account
- * model. Refer to the NextAuth.js docs for the provider you want to use. Example:
- *
- * @see https://next-auth.js.org/providers/github
- */
-].filter(Boolean) as Provider[];
-
-/**
- * Options for NextAuth.js used to configure adapters, providers, callbacks, etc.
- *
- * @see https://next-auth.js.org/providers
- * @see https://authjs.dev/reference/adapter/drizzle
- * @see https://next-auth.js.org/configuration/options
- * @see https://next-auth.js.org/configuration/callbacks
- */
-export const authOptions: NextAuthOptions = {
- // @ts-expect-error strange error drizzle
- adapter: DrizzleAdapter(db, table),
- secret: env.NEXTAUTH_SECRET,
- providers,
- callbacks: {
- session: ({ session, user }) => ({
- ...session,
- user: { ...session.user, id: user.id },
- }),
- },
- pages: {
- newUser: "/auth",
- signIn: "/sign-in",
- signOut: "/sign-out",
- },
-};
-
-/**
- * Wrapper for `getServerSession` so that you don't need to import the `authOptions` in every file.
- *
- * @see https://next-auth.js.org/configuration/nextjs
- */
-export const getNextAuthServerSession = () => getServerSession(authOptions);
-
-/**
- * DOCUMENTATION
- * =============
- * Coming Soon...
- */
diff --git a/src/core/auth/authjs/islands/check-user-button.tsx b/src/core/auth/authjs/islands/check-user-button.tsx
deleted file mode 100644
index 2329569b..00000000
--- a/src/core/auth/authjs/islands/check-user-button.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-"use client";
-
-import { useState } from "react";
-
-export default function WhoAmIButton({
- whoAmIAction,
-}: {
- whoAmIAction: () => Promise;
-}) {
- const [name, setName] = useState();
- return (
-
-
{
- setName(await whoAmIAction());
- }}
- >
- Who Am I?
-
- {name &&
You are {name}
}
-
- );
-}
diff --git a/src/core/auth/authjs/other/checks.tsx b/src/core/auth/authjs/other/checks.tsx
deleted file mode 100644
index b2330950..00000000
--- a/src/core/auth/authjs/other/checks.tsx
+++ /dev/null
@@ -1 +0,0 @@
-// todo: move the corresponding code from auth/page.tsx to the this separate file
diff --git a/src/core/auth/clerkjs/components/user-profile-clerk.tsx b/src/core/auth/clerkjs/components/user-profile-clerk.tsx
new file mode 100644
index 00000000..8c8c41c3
--- /dev/null
+++ b/src/core/auth/clerkjs/components/user-profile-clerk.tsx
@@ -0,0 +1,54 @@
+"use client";
+
+import type { Theme } from "@clerk/types";
+
+import { UserProfile as ClerkUserProfile } from "@clerk/nextjs";
+import { dark, neobrutalism } from "@clerk/themes";
+import { useTheme } from "next-themes";
+
+const appearance: Theme = {
+ baseTheme: undefined,
+ elements: {
+ card: "shadow-none",
+ headerSubtitle: "hidden",
+ headerTitle: "hidden",
+ navbar: "hidden",
+ navbarMobileMenuButton: "hidden",
+ },
+ variables: {
+ borderRadius: "0.25rem",
+ },
+};
+
+export function UserProfileClerk() {
+ const { resolvedTheme, theme } = useTheme();
+
+ // Determine the base theme based on the current theme or system pref
+ const baseTheme =
+ theme === "light"
+ ? neobrutalism
+ : theme === "dark"
+ ? dark
+ : theme === "system"
+ ? resolvedTheme === "dark"
+ ? dark
+ : neobrutalism
+ : appearance.baseTheme;
+
+ return (
+
+ );
+}
diff --git a/src/core/auth/clerkjs/islands/user-profile-clerk.tsx b/src/core/auth/clerkjs/islands/user-profile-clerk.tsx
deleted file mode 100644
index e669f68b..00000000
--- a/src/core/auth/clerkjs/islands/user-profile-clerk.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-"use client";
-
-import { UserProfile as ClerkUserProfile } from "@clerk/nextjs";
-import { dark, neobrutalism, shadesOfPurple } from "@clerk/themes";
-import type { Theme } from "@clerk/types";
-import { useTheme } from "next-themes";
-
-const appearance: Theme = {
- baseTheme: undefined,
- variables: {
- borderRadius: "0.25rem",
- },
- elements: {
- card: "shadow-none",
- navbar: "hidden",
- navbarMobileMenuButton: "hidden",
- headerTitle: "hidden",
- headerSubtitle: "hidden",
- },
-};
-
-export function UserProfileClerk() {
- const { theme, resolvedTheme } = useTheme();
-
- // Determine the base theme based on the current theme or system preference
- const baseTheme =
- theme === "light" ? neobrutalism
- : theme === "dark" ? dark
- : theme === "system" ?
- resolvedTheme === "dark" ?
- dark
- : neobrutalism
- : appearance.baseTheme;
-
- return (
-
- );
-}
diff --git a/src/core/auth/clerkjs/other/checks.tsx b/src/core/auth/clerkjs/other/checks.tsx
index 09dc900d..e69de29b 100644
--- a/src/core/auth/clerkjs/other/checks.tsx
+++ b/src/core/auth/clerkjs/other/checks.tsx
@@ -1 +0,0 @@
-// todo: move the corresponding code from auth/page.tsx to this separate file
diff --git a/src/core/auth/shared/index.ts b/src/core/auth/shared/index.ts
deleted file mode 100644
index af568ef1..00000000
--- a/src/core/auth/shared/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from "./islands/auth-pages-content";
diff --git a/src/core/auth/shared/islands/auth-pages-content.tsx b/src/core/auth/shared/islands/auth-pages-content.tsx
deleted file mode 100644
index e3e30ea8..00000000
--- a/src/core/auth/shared/islands/auth-pages-content.tsx
+++ /dev/null
@@ -1,249 +0,0 @@
-"use client";
-
-import React, {
- useEffect,
- useMemo,
- useState,
- type HTMLAttributes,
-} from "react";
-import { cls } from "~/utils";
-import { signIn, type ClientSafeProvider } from "next-auth/react";
-import { useLocale } from "next-intl";
-import { useQueryState } from "next-usequerystate";
-import toast from "react-hot-toast";
-import { Balancer } from "react-wrap-balancer";
-import { cnBase } from "tailwind-variants";
-
-import { appts, siteConfig } from "~/app";
-import { env } from "~/env.mjs";
-import { OAuthSignInClerk } from "~/islands/content/clerk-page-oauth";
-import ProviderButton from "~/islands/content/provider-button";
-import {
- Card,
- CardContent,
- CardDescription,
- CardFooter,
- CardHeader,
-} from "~/islands/primitives/card";
-import { Shell } from "~/islands/wrappers/shell-variants";
-import { Link } from "~/navigation";
-import { typography } from "~/server/text";
-
-type AuthIntlProps = {
- tSignin: string;
- tOAuthSignin: string;
- tOAuthCallback: string;
- tOAuthCreateAccount: string;
- tEmailCreateAccount: string;
- tCallback: string;
- tOAuthAccountNotLinked: string;
- tDefault: string;
- tUnknownError: string;
- tPrivacy: string;
- tTerms: string;
- tAnd: string;
- tSignUpLink: string;
- tSignInLink: string;
- tAuthLegal: string;
- tSignUpHere: string;
- tNoAccount: string;
- tSignInHere: string;
- tHaveAccount: string;
- tPleaseWait: string;
-};
-
-type AuthPagesContentProps = AuthIntlProps &
- HTMLAttributes & {
- providers: Record | null;
- isRegPage: boolean;
- user: any;
- };
-
-export function AuthPagesContent({
- className,
- user,
- isRegPage,
- providers,
- tSignin,
- tOAuthSignin,
- tOAuthCallback,
- tOAuthCreateAccount,
- tEmailCreateAccount,
- tCallback,
- tOAuthAccountNotLinked,
- tDefault,
- tUnknownError,
- tPrivacy,
- tTerms,
- tAnd,
- tSignUpLink,
- tSignInLink,
- tAuthLegal,
- tSignUpHere,
- tNoAccount,
- tSignInHere,
- tHaveAccount,
- tPleaseWait,
- ...props
-}: AuthPagesContentProps) {
- const [error] = useQueryState("error");
-
- const [isProviderLoading, setProviderLoading] = useState(false);
-
- const oauthProvidersAuthjs = useMemo(
- () =>
- Object.values(providers ?? {}).filter(
- (provider) => provider.type === "oauth",
- ),
- [providers],
- );
-
- const authProvider = env.NEXT_PUBLIC_AUTH_PROVIDER || "authjs";
-
- const errors: Record = {
- Signin: tSignin,
- OAuthSignin: tOAuthSignin,
- OAuthCallback: tOAuthCallback,
- OAuthCreateAccount: tOAuthCreateAccount,
- EmailCreateAccount: tEmailCreateAccount,
- Callback: tCallback,
- OAuthAccountNotLinked: tOAuthAccountNotLinked,
- default: tDefault,
- };
-
- useEffect(() => {
- // todo: when error occurs, user looses chosen locale and has detected
- // If there's an error query parameter in the url, display the message
- if (error) toast.error(errors[error] ?? tUnknownError);
- }, [toast, error]);
-
- const locale = useLocale();
- const callbackUrl = `/${locale}/auth`;
-
- const handleSignIn = (provider: string) => {
- signIn(provider, { callbackUrl });
- };
-
- if (appts.debug) {
- console.log("[appts.debug]: Does user visits a sign up page?", isRegPage);
- }
-
- return (
-