chore: remove unused code (#5697)

This commit is contained in:
Johannes
2025-05-07 07:16:27 -07:00
committed by GitHub
parent b338c6d28d
commit f0be6de0b3
28 changed files with 0 additions and 1379 deletions
-34
View File
@@ -1,34 +0,0 @@
import "server-only";
import { cache } from "@/lib/cache";
import { ZId } from "@formbricks/types/common";
import { hasUserEnvironmentAccess } from "../environment/auth";
import { validateInputs } from "../utils/validate";
import { actionClassCache } from "./cache";
import { getActionClass } from "./service";
export const canUserUpdateActionClass = (userId: string, actionClassId: string): Promise<boolean> =>
cache(
async () => {
validateInputs([userId, ZId], [actionClassId, ZId]);
try {
if (!userId) return false;
const actionClass = await getActionClass(actionClassId);
if (!actionClass) return false;
const hasAccessToEnvironment = await hasUserEnvironmentAccess(userId, actionClass.environmentId);
if (!hasAccessToEnvironment) return false;
return true;
} catch (error) {
throw error;
}
},
[`canUserUpdateActionClass-${userId}-${actionClassId}`],
{
tags: [actionClassCache.tag.byId(actionClassId)],
}
)();
-39
View File
@@ -1,39 +0,0 @@
export const fetchRessource = async (url: string) => {
const res = await fetch(url);
// If the status code is not in the range 200-299,
// we still try to parse and throw it.
if (!res.ok) {
const error: any = new Error("An error occurred while fetching the data.");
// Attach extra info to the error object.
error.info = await res.json();
error.status = res.status;
throw error;
}
return res.json();
};
export const fetcher = async (url: string) => {
const res = await fetch(url);
// If the status code is not in the range 200-299,
// we still try to parse and throw it.
if (!res.ok) {
const error: any = new Error("An error occurred while fetching the data.");
// Attach extra info to the error object.
error.info = await res.json();
error.status = res.status;
throw error;
}
return res.json();
};
export const updateRessource = async (url: string, { arg }: { arg: any }) => {
return fetch(url, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(arg),
});
};
-28
View File
@@ -1,28 +0,0 @@
import "server-only";
import { structuredClone } from "@/lib/pollyfills/structuredClone";
import { TI18nString } from "@formbricks/types/surveys/types";
import { isI18nObject } from "./utils";
// Helper function to extract a regular string from an i18nString.
const extractStringFromI18n = (i18nString: TI18nString, languageCode: string): string => {
if (typeof i18nString === "object" && i18nString !== null) {
return i18nString[languageCode] || "";
}
return i18nString;
};
// Assuming I18nString and extraction logic are defined
const reverseTranslateObject = <T extends Record<string, any>>(obj: T, languageCode: string): T => {
const clonedObj = structuredClone(obj);
for (let key in clonedObj) {
const value = clonedObj[key];
if (isI18nObject(value)) {
// Now TypeScript knows `value` is I18nString, treat it accordingly
clonedObj[key] = extractStringFromI18n(value, languageCode) as T[Extract<keyof T, string>];
} else if (typeof value === "object" && value !== null) {
// Recursively handle nested objects
clonedObj[key] = reverseTranslateObject(value, languageCode);
}
}
return clonedObj;
};
-31
View File
@@ -1,31 +0,0 @@
import "server-only";
import { ZId } from "@formbricks/types/common";
import { cache } from "../cache";
import { hasUserEnvironmentAccess } from "../environment/auth";
import { validateInputs } from "../utils/validate";
import { getIntegration } from "./service";
export const canUserAccessIntegration = async (userId: string, integrationId: string): Promise<boolean> =>
cache(
async () => {
validateInputs([userId, ZId], [integrationId, ZId]);
if (!userId) return false;
try {
const integration = await getIntegration(integrationId);
if (!integration) return false;
const hasAccessToEnvironment = await hasUserEnvironmentAccess(userId, integration.environmentId);
if (!hasAccessToEnvironment) return false;
return true;
} catch (error) {
throw error;
}
},
[`canUserAccessIntegration-${userId}-${integrationId}`],
{
tags: [`integrations-${integrationId}`],
}
)();
View File
-36
View File
@@ -1,36 +0,0 @@
import "server-only";
import { cache } from "@/lib/cache";
import { ZId } from "@formbricks/types/common";
import { hasUserEnvironmentAccess } from "../environment/auth";
import { getSurvey } from "../survey/service";
import { validateInputs } from "../utils/validate";
import { responseCache } from "./cache";
import { getResponse } from "./service";
export const canUserAccessResponse = (userId: string, responseId: string): Promise<boolean> =>
cache(
async () => {
validateInputs([userId, ZId], [responseId, ZId]);
if (!userId) return false;
try {
const response = await getResponse(responseId);
if (!response) return false;
const survey = await getSurvey(response.surveyId);
if (!survey) return false;
const hasAccessToEnvironment = await hasUserEnvironmentAccess(userId, survey.environmentId);
if (!hasAccessToEnvironment) return false;
return true;
} catch (error) {
throw error;
}
},
[`canUserAccessResponse-${userId}-${responseId}`],
{
tags: [responseCache.tag.byId(responseId)],
}
)();
-66
View File
@@ -1,66 +0,0 @@
import { cache } from "@/lib/cache";
import { ZId } from "@formbricks/types/common";
import { canUserAccessResponse } from "../response/auth";
import { getResponse } from "../response/service";
import { validateInputs } from "../utils/validate";
import { responseNoteCache } from "./cache";
import { getResponseNote } from "./service";
export const canUserModifyResponseNote = async (userId: string, responseNoteId: string): Promise<boolean> =>
cache(
async () => {
validateInputs([userId, ZId], [responseNoteId, ZId]);
if (!userId || !responseNoteId) return false;
try {
const responseNote = await getResponseNote(responseNoteId);
if (!responseNote) return false;
return responseNote.user.id === userId;
} catch (error) {
throw error;
}
},
[`canUserModifyResponseNote-${userId}-${responseNoteId}`],
{
tags: [responseNoteCache.tag.byId(responseNoteId)],
}
)();
export const canUserResolveResponseNote = async (
userId: string,
responseId: string,
responseNoteId: string
): Promise<boolean> =>
cache(
async () => {
validateInputs([userId, ZId], [responseNoteId, ZId]);
if (!userId || !responseId || !responseNoteId) return false;
try {
const response = await getResponse(responseId);
let noteExistsOnResponse = false;
response?.notes.forEach((note) => {
if (note.id === responseNoteId) {
noteExistsOnResponse = true;
}
});
if (!noteExistsOnResponse) return false;
const canAccessResponse = await canUserAccessResponse(userId, responseId);
return canAccessResponse;
} catch (error) {
throw error;
}
},
[`canUserResolveResponseNote-${userId}-${responseNoteId}`],
{
tags: [responseNoteCache.tag.byId(responseNoteId)],
}
)();
-113
View File
@@ -1,113 +0,0 @@
import { cache } from "@/lib/cache";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { ZId } from "@formbricks/types/common";
import { TSurvey } from "@formbricks/types/surveys/types";
import { hasUserEnvironmentAccess } from "../environment/auth";
import { validateInputs } from "../utils/validate";
import { canUserAccessSurvey } from "./auth";
import { surveyCache } from "./cache";
import { getSurvey } from "./service";
// Mock dependencies
vi.mock("@/lib/cache", () => ({
cache: vi.fn((fn) => fn),
}));
vi.mock("@/lib/utils/validate", () => ({
validateInputs: vi.fn(),
}));
vi.mock("./service", () => ({
getSurvey: vi.fn(),
}));
vi.mock("../environment/auth", () => ({
hasUserEnvironmentAccess: vi.fn(),
}));
vi.mock("./cache", () => ({
surveyCache: {
tag: {
byId: vi.fn().mockReturnValue("survey-tag-id"),
},
},
}));
describe("canUserAccessSurvey", () => {
const userId = "user-123";
const surveyId = "survey-456";
const environmentId = "env-789";
const mockSurvey = {
id: surveyId,
environmentId: environmentId,
} as TSurvey;
beforeEach(() => {
vi.resetAllMocks();
vi.mocked(cache).mockImplementation((fn) => () => fn());
vi.mocked(getSurvey).mockResolvedValue(mockSurvey);
vi.mocked(hasUserEnvironmentAccess).mockResolvedValue(true);
vi.mocked(surveyCache.tag.byId).mockReturnValue(`survey-${surveyId}`);
});
test("validates input parameters", async () => {
await canUserAccessSurvey(userId, surveyId);
expect(validateInputs).toHaveBeenCalledWith([surveyId, ZId], [userId, ZId]);
});
test("returns false if userId is falsy", async () => {
const result = await canUserAccessSurvey("", surveyId);
expect(result).toBe(false);
expect(getSurvey).not.toHaveBeenCalled();
});
test("returns false if survey is not found", async () => {
vi.mocked(getSurvey).mockResolvedValueOnce(null);
await expect(canUserAccessSurvey(userId, surveyId)).rejects.toThrowError("Survey not found");
expect(getSurvey).toHaveBeenCalledWith(surveyId);
expect(hasUserEnvironmentAccess).not.toHaveBeenCalled();
});
test("calls hasUserEnvironmentAccess with userId and survey's environmentId", async () => {
await canUserAccessSurvey(userId, surveyId);
expect(getSurvey).toHaveBeenCalledWith(surveyId);
expect(hasUserEnvironmentAccess).toHaveBeenCalledWith(userId, environmentId);
});
test("returns false if user doesn't have access to the environment", async () => {
vi.mocked(hasUserEnvironmentAccess).mockResolvedValueOnce(false);
const result = await canUserAccessSurvey(userId, surveyId);
expect(result).toBe(false);
expect(getSurvey).toHaveBeenCalledWith(surveyId);
expect(hasUserEnvironmentAccess).toHaveBeenCalledWith(userId, environmentId);
});
test("returns true if user has access to the environment", async () => {
const result = await canUserAccessSurvey(userId, surveyId);
expect(result).toBe(true);
expect(getSurvey).toHaveBeenCalledWith(surveyId);
expect(hasUserEnvironmentAccess).toHaveBeenCalledWith(userId, environmentId);
});
test("rethrows errors that occur during execution", async () => {
const error = new Error("Test error");
vi.mocked(getSurvey).mockRejectedValueOnce(error);
await expect(canUserAccessSurvey(userId, surveyId)).rejects.toThrow(error);
});
test("uses cache with correct cache key and tags", async () => {
await canUserAccessSurvey(userId, surveyId);
expect(cache).toHaveBeenCalledWith(expect.any(Function), [`canUserAccessSurvey-${userId}-${surveyId}`], {
tags: [`survey-${surveyId}`],
});
expect(surveyCache.tag.byId).toHaveBeenCalledWith(surveyId);
});
});
-31
View File
@@ -1,31 +0,0 @@
import { cache } from "@/lib/cache";
import { ZId } from "@formbricks/types/common";
import { hasUserEnvironmentAccess } from "../environment/auth";
import { validateInputs } from "../utils/validate";
import { surveyCache } from "./cache";
import { getSurvey } from "./service";
export const canUserAccessSurvey = (userId: string, surveyId: string): Promise<boolean> =>
cache(
async () => {
validateInputs([surveyId, ZId], [userId, ZId]);
if (!userId) return false;
try {
const survey = await getSurvey(surveyId);
if (!survey) throw new Error("Survey not found");
const hasAccessToEnvironment = await hasUserEnvironmentAccess(userId, survey.environmentId);
if (!hasAccessToEnvironment) return false;
return true;
} catch (error) {
throw error;
}
},
[`canUserAccessSurvey-${userId}-${surveyId}`],
{
tags: [surveyCache.tag.byId(surveyId)],
}
)();
-21
View File
@@ -1,21 +0,0 @@
import "server-only";
import { ZId } from "@formbricks/types/common";
import { hasUserEnvironmentAccess } from "../environment/auth";
import { validateInputs } from "../utils/validate";
import { getTag } from "./service";
export const canUserAccessTag = async (userId: string, tagId: string): Promise<boolean> => {
validateInputs([userId, ZId], [tagId, ZId]);
try {
const tag = await getTag(tagId);
if (!tag) return false;
const hasAccessToEnvironment = await hasUserEnvironmentAccess(userId, tag.environmentId);
if (!hasAccessToEnvironment) return false;
return true;
} catch (error) {
throw error;
}
};
-31
View File
@@ -1,31 +0,0 @@
import "server-only";
import { cache } from "@/lib/cache";
import { ZId } from "@formbricks/types/common";
import { canUserAccessResponse } from "../response/auth";
import { canUserAccessTag } from "../tag/auth";
import { validateInputs } from "../utils/validate";
import { tagOnResponseCache } from "./cache";
export const canUserAccessTagOnResponse = (
userId: string,
tagId: string,
responseId: string
): Promise<boolean> =>
cache(
async () => {
validateInputs([userId, ZId], [tagId, ZId], [responseId, ZId]);
try {
const isAuthorizedForTag = await canUserAccessTag(userId, tagId);
const isAuthorizedForResponse = await canUserAccessResponse(userId, responseId);
return isAuthorizedForTag && isAuthorizedForResponse;
} catch (error) {
throw error;
}
},
[`canUserAccessTagOnResponse-${userId}-${tagId}-${responseId}`],
{
tags: [tagOnResponseCache.tag.byResponseIdAndTagId(responseId, tagId)],
}
)();
@@ -1,43 +0,0 @@
import { cache } from "@/lib/cache";
import { environmentCache } from "@/lib/environment/cache";
import { validateInputs } from "@/lib/utils/validate";
import { Environment, Prisma } from "@prisma/client";
import { cache as reactCache } from "react";
import { z } from "zod";
import { prisma } from "@formbricks/database";
import { logger } from "@formbricks/logger";
import { DatabaseError } from "@formbricks/types/errors";
export const getEnvironment = reactCache(
async (environmentId: string): Promise<Pick<Environment, "id" | "appSetupCompleted"> | null> =>
cache(
async () => {
validateInputs([environmentId, z.string().cuid2()]);
try {
const environment = await prisma.environment.findUnique({
where: {
id: environmentId,
},
select: {
id: true,
appSetupCompleted: true,
},
});
return environment;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
logger.error(error, "Error fetching environment");
throw new DatabaseError(error.message);
}
throw error;
}
},
[`survey-lib-getEnvironment-${environmentId}`],
{
tags: [environmentCache.tag.byId(environmentId)],
}
)()
);
-46
View File
@@ -1,46 +0,0 @@
import { cache } from "@/lib/cache";
import { membershipCache } from "@/lib/membership/cache";
import { validateInputs } from "@/lib/utils/validate";
import { OrganizationRole, Prisma } from "@prisma/client";
import { cache as reactCache } from "react";
import { z } from "zod";
import { prisma } from "@formbricks/database";
import { logger } from "@formbricks/logger";
import { AuthorizationError, DatabaseError, UnknownError } from "@formbricks/types/errors";
export const getMembershipRoleByUserIdOrganizationId = reactCache(
async (userId: string, organizationId: string): Promise<OrganizationRole> =>
cache(
async () => {
validateInputs([userId, z.string()], [organizationId, z.string().cuid2()]);
try {
const membership = await prisma.membership.findUnique({
where: {
userId_organizationId: {
userId,
organizationId,
},
},
});
if (!membership) {
throw new AuthorizationError("You are not a member of this organization");
}
return membership.role;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
logger.error(error, "Error fetching membership role by user id and organization id");
throw new DatabaseError(error.message);
}
throw new UnknownError("Error while fetching membership");
}
},
[`survey-getMembershipRoleByUserIdOrganizationId-${userId}-${organizationId}`],
{
tags: [membershipCache.tag.byUserId(userId), membershipCache.tag.byOrganizationId(organizationId)],
}
)()
);
@@ -1,40 +0,0 @@
import { cache } from "@/lib/cache";
import { environmentCache } from "@/lib/environment/cache";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { ResourceNotFoundError } from "@formbricks/types/errors";
export const getOrganizationIdByEnvironmentId = reactCache(
async (environmentId: string): Promise<string | null> =>
cache(
async () => {
const organization = await prisma.organization.findFirst({
where: {
projects: {
some: {
environments: {
some: {
id: environmentId,
},
},
},
},
},
select: {
id: true,
},
});
if (!organization) {
throw new ResourceNotFoundError("Organization", null);
}
return organization.id;
},
[`survey-list-getOrganizationIdByEnvironmentId-${environmentId}`],
{
tags: [environmentCache.tag.byId(environmentId)],
}
)()
);
@@ -1,122 +0,0 @@
import { cn } from "@/lib/cn";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/modules/ui/components/dropdown-menu";
import { ChevronDownIcon } from "lucide-react";
import React from "react";
import { z } from "zod";
const ZBadgeSelectOptionSchema = z.object({
text: z.string(),
type: z.enum(["warning", "success", "error", "gray"]),
});
const ZBadgeSelectPropsSchema = z.object({
text: z.string().optional(),
type: z.enum(["warning", "success", "error", "gray"]).optional(),
options: z.array(ZBadgeSelectOptionSchema).optional(),
selectedIndex: z.number().optional(),
onChange: z.function().args(z.number()).returns(z.void()).optional(),
size: z.enum(["tiny", "normal", "large"]),
className: z.string().optional(),
isLoading: z.boolean().optional(),
});
export type TBadgeSelectOption = z.infer<typeof ZBadgeSelectOptionSchema>;
export type TBadgeSelectProps = z.infer<typeof ZBadgeSelectPropsSchema>;
export const BadgeSelect: React.FC<TBadgeSelectProps & { isLoading?: boolean }> = ({
text,
type,
options,
selectedIndex = 0,
onChange,
size,
className,
isLoading = false,
}) => {
const bgColor = {
warning: "bg-amber-100",
success: "bg-emerald-100",
error: "bg-red-100",
gray: "bg-slate-100",
};
const borderColor = {
warning: "border-amber-200",
success: "border-emerald-200",
error: "border-red-200",
gray: "border-slate-200",
};
const textColor = {
warning: "text-amber-800",
success: "text-emerald-800",
error: "text-red-800",
gray: "text-slate-600",
};
const padding = {
tiny: "px-1.5 py-0.5",
normal: "px-2.5 py-0.5",
large: "px-3.5 py-1",
};
const textSize = size === "large" ? "text-sm" : "text-xs";
const currentOption = options ? options[selectedIndex] : { text, type: type || "gray" };
const renderContent = () => {
if (isLoading) {
return (
<span className="animate-pulse" aria-busy="true">
<span className={cn("inline-block h-2 w-8 rounded-full bg-black/10")}></span>
</span>
);
}
return (
<>
{currentOption.text}
{options && <ChevronDownIcon className="ml-1 h-3 w-3" aria-hidden="true" />}
</>
);
};
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<span
className={cn(
"border-opacity-50 inline-flex items-center rounded-full border font-medium",
options && !isLoading ? "hover:border-opacity-100 cursor-pointer" : "pointer-events-none",
bgColor[currentOption.type],
borderColor[currentOption.type],
textColor[currentOption.type],
padding[size],
textSize,
className
)}>
{renderContent()}
</span>
</DropdownMenuTrigger>
{options && (
<DropdownMenuContent className="mt-1 bg-white shadow-lg">
{options.map((option, index) => (
<DropdownMenuItem
key={index}
className={cn("cursor-pointer px-4 py-2 hover:bg-slate-100", textSize)}
onClick={(event) => {
event.stopPropagation();
onChange?.(index);
}}>
{option.text}
</DropdownMenuItem>
))}
</DropdownMenuContent>
)}
</DropdownMenu>
);
};
@@ -1,119 +0,0 @@
import type { Meta, StoryObj } from "@storybook/react";
import { BadgeSelect } from "./index";
const meta = {
title: "ui/BadgeSelect",
component: BadgeSelect,
tags: ["autodocs"],
parameters: {
layout: "centered",
},
argTypes: {
type: {
control: "select",
options: ["warning", "success", "error", "gray"],
},
size: { control: "select", options: ["small", "normal", "large"] },
className: { control: "text" },
},
} satisfies Meta<typeof BadgeSelect>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Warning: Story = {
args: {
text: "Warning",
type: "warning",
size: "normal",
},
};
export const Success: Story = {
args: {
text: "Success",
type: "success",
size: "normal",
},
};
export const Error: Story = {
args: {
text: "Error",
type: "error",
size: "normal",
},
};
export const Gray: Story = {
args: {
text: "Gray",
type: "gray",
size: "normal",
},
};
export const LargeWarning: Story = {
args: {
text: "Warning",
type: "warning",
size: "large",
},
};
export const LargeSuccess: Story = {
args: {
text: "Success",
type: "success",
size: "large",
},
};
export const LargeError: Story = {
args: {
text: "Error",
type: "error",
size: "large",
},
};
export const LargeGray: Story = {
args: {
text: "Gray",
type: "gray",
size: "large",
},
};
export const TinyWarning: Story = {
args: {
text: "Warning",
type: "warning",
size: "tiny",
},
};
export const TinySuccess: Story = {
args: {
text: "Success",
type: "success",
size: "tiny",
},
};
export const TinyError: Story = {
args: {
text: "Error",
type: "error",
size: "tiny",
},
};
export const TinyGray: Story = {
args: {
text: "Gray",
type: "gray",
size: "tiny",
},
};
@@ -1,98 +0,0 @@
import { cn } from "@/modules/ui/lib/utils";
import { Slot } from "@radix-ui/react-slot";
import { ChevronRight, MoreHorizontal } from "lucide-react";
import * as React from "react";
const Breadcrumb = React.forwardRef<
HTMLElement,
React.ComponentPropsWithoutRef<"nav"> & {
separator?: React.ReactNode;
}
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />);
Breadcrumb.displayName = "Breadcrumb";
const BreadcrumbList = React.forwardRef<HTMLOListElement, React.ComponentPropsWithoutRef<"ol">>(
({ className, ...props }, ref) => (
<ol
ref={ref}
className={cn(
"flex flex-wrap items-center gap-1.5 break-words text-sm text-slate-500 sm:gap-2.5 dark:text-slate-400",
className
)}
{...props}
/>
)
);
BreadcrumbList.displayName = "BreadcrumbList";
const BreadcrumbItem = React.forwardRef<HTMLLIElement, React.ComponentPropsWithoutRef<"li">>(
({ className, ...props }, ref) => (
<li ref={ref} className={cn("inline-flex items-center gap-1.5", className)} {...props} />
)
);
BreadcrumbItem.displayName = "BreadcrumbItem";
const BreadcrumbLink = React.forwardRef<
HTMLAnchorElement,
React.ComponentPropsWithoutRef<"a"> & {
asChild?: boolean;
}
>(({ asChild, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a";
return (
<Comp
ref={ref}
className={cn("transition-colors hover:text-slate-950 dark:hover:text-slate-50", className)}
{...props}
/>
);
});
BreadcrumbLink.displayName = "BreadcrumbLink";
const BreadcrumbPage = React.forwardRef<HTMLSpanElement, React.ComponentPropsWithoutRef<"span">>(
({ className, ...props }, ref) => (
<span
ref={ref}
role="link"
aria-disabled="true"
aria-current="page"
className={cn("font-normal text-slate-950 dark:text-slate-50", className)}
{...props}
/>
)
);
BreadcrumbPage.displayName = "BreadcrumbPage";
const BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentProps<"li">) => (
<li
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:h-3.5 [&>svg]:w-3.5", className)}
{...props}>
{children ?? <ChevronRight />}
</li>
);
BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
const BreadcrumbEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => (
<span
role="presentation"
aria-hidden="true"
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More</span>
</span>
);
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
};
@@ -1,70 +0,0 @@
export const Pagination = ({
baseUrl,
currentPage,
totalItems,
itemsPerPage,
}: {
baseUrl: string;
currentPage: number;
totalItems: number;
itemsPerPage: number;
}) => {
const totalPages = Math.ceil(totalItems / itemsPerPage);
const previousPageLink = currentPage === 1 ? "#" : `${baseUrl}?page=${currentPage - 1}`;
const nextPageLink = currentPage === totalPages ? "#" : `${baseUrl}?page=${currentPage + 1}`;
const getDisplayedPages = () => {
if (totalPages <= 20) {
return Array.from({ length: totalPages }, (_, idx) => idx + 1);
} else {
let range = [currentPage - 2, currentPage - 1, currentPage, currentPage + 1, currentPage + 2];
return [1, ...range.filter((n) => n > 1 && n < totalPages), totalPages];
}
};
return (
<nav aria-label="Page navigation" className="flex justify-center">
<ul className="mt-4 inline-flex -space-x-px text-sm">
<li>
<a
href={previousPageLink}
className={`ml-0 flex h-8 items-center justify-center rounded-l-lg border border-slate-300 bg-white px-3 text-slate-500 ${
currentPage === 1
? "cursor-not-allowed opacity-50"
: "hover:bg-slate-100 hover:text-slate-700 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-400 dark:hover:bg-slate-700 dark:hover:text-white"
}`}>
Previous
</a>
</li>
{getDisplayedPages().map((pageNum) => {
const pageLink = `${baseUrl}?page=${pageNum}`;
return (
<li key={pageNum} className="hidden sm:block">
<a
href={pageNum === currentPage ? "#" : pageLink}
className={`flex h-8 items-center justify-center px-3 ${
pageNum === currentPage ? "bg-blue-50 text-green-500" : "bg-white text-slate-500"
} border border-slate-300 hover:bg-slate-100 hover:text-slate-700 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-400 dark:hover:bg-slate-700 dark:hover:text-white`}>
{pageNum}
</a>
</li>
);
})}
<li>
<a
href={nextPageLink}
className={`ml-0 flex h-8 items-center justify-center rounded-r-lg border border-slate-300 bg-white px-3 text-slate-500 ${
currentPage === totalPages
? "cursor-not-allowed opacity-50"
: "hover:bg-slate-100 hover:text-slate-700 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-400 dark:hover:bg-slate-700 dark:hover:text-white"
}`}>
Next
</a>
</li>
</ul>
</nav>
);
};
@@ -1,18 +0,0 @@
interface ResponsiveVideoProps {
src: string;
title?: string;
}
export const ResponsiveVideo: React.FC<ResponsiveVideoProps> = ({ src, title }: ResponsiveVideoProps) => {
return (
<div className="relative" style={{ paddingTop: "56.25%" }}>
<iframe
className="absolute left-0 top-0 h-full w-full rounded"
src={src}
title={title}
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen></iframe>
</div>
);
};
@@ -1,25 +0,0 @@
"use client";
import { cn } from "@/modules/ui/lib/utils";
import * as SeparatorPrimitive from "@radix-ui/react-separator";
import * as React from "react";
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(({ className, orientation = "horizontal", decorative = true, ...props }, ref) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-slate-200 dark:bg-slate-800",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
{...props}
/>
));
Separator.displayName = SeparatorPrimitive.Root.displayName;
export { Separator };
@@ -1,119 +0,0 @@
"use client";
import { cn } from "@/lib/cn";
import * as SheetPrimitive from "@radix-ui/react-dialog";
import { type VariantProps, cva } from "class-variance-authority";
import { X } from "lucide-react";
import * as React from "react";
const Sheet = SheetPrimitive.Root;
const SheetTrigger = SheetPrimitive.Trigger;
const SheetClose = SheetPrimitive.Close;
const SheetPortal = SheetPrimitive.Portal;
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80",
className
)}
{...props}
ref={ref}
/>
));
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
{
variants: {
side: {
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
bottom:
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-md",
right:
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-md",
},
},
defaultVariants: {
side: "right",
},
}
);
interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
const SheetContent = React.forwardRef<React.ElementRef<typeof SheetPrimitive.Content>, SheetContentProps>(
({ side = "right", className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}>
{children}
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-none disabled:pointer-events-none">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
)
);
SheetContent.displayName = SheetPrimitive.Content.displayName;
const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
);
SheetHeader.displayName = "SheetHeader";
const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
{...props}
/>
);
SheetFooter.displayName = "SheetFooter";
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title
ref={ref}
className={cn("text-foreground flex items-center text-lg font-semibold", className)}
{...props}
/>
));
SheetTitle.displayName = SheetPrimitive.Title.displayName;
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description
ref={ref}
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
));
SheetDescription.displayName = SheetPrimitive.Description.displayName;
export {
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetOverlay,
SheetPortal,
SheetTitle,
SheetTrigger,
};
@@ -1,11 +0,0 @@
interface SimpleLayoutProps {
children: React.ReactNode;
}
export const SimpleLayout = ({ children }: SimpleLayoutProps) => {
return (
<div className="max-w-8xl flex">
<div className="w-full">{children}</div>
</div>
);
};
@@ -1,54 +0,0 @@
"use client";
import { cn } from "@/lib/cn";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import * as React from "react";
const Tabs = TabsPrimitive.Root;
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-lg bg-slate-100 p-1 text-slate-500 dark:bg-slate-800 dark:text-slate-400",
className
)}
{...props}
/>
));
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center rounded-lg px-3 py-1.5 text-sm font-medium whitespace-nowrap ring-offset-white transition-all focus-visible:ring-2 focus-visible:ring-slate-950 focus-visible:ring-offset-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-slate-950 data-[state=active]:shadow-sm dark:ring-offset-slate-950 dark:focus-visible:ring-slate-300 dark:data-[state=active]:bg-slate-950 dark:data-[state=active]:text-slate-50",
className
)}
{...props}
/>
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-white focus-visible:ring-2 focus-visible:ring-slate-950 focus-visible:ring-offset-2 focus-visible:outline-none dark:ring-offset-slate-950 dark:focus-visible:ring-slate-300",
className
)}
{...props}
/>
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsContent, TabsList, TabsTrigger };
@@ -1,20 +0,0 @@
import * as React from "react";
import { cn } from "../../lib/utils";
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm ring-offset-white placeholder:text-slate-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-950 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-800 dark:bg-slate-950 dark:ring-offset-slate-950 dark:placeholder:text-slate-400 dark:focus-visible:ring-slate-300",
className
)}
ref={ref}
{...props}
/>
);
});
Textarea.displayName = "Textarea";
export { Textarea };
@@ -1,52 +0,0 @@
"use client";
import { cn } from "@/lib/cn";
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
import { type VariantProps } from "class-variance-authority";
import * as React from "react";
import { toggleVariants } from "./toggle";
const ToggleGroupContext = React.createContext<VariantProps<typeof toggleVariants>>({
size: "default",
variant: "default",
});
const ToggleGroup = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> & VariantProps<typeof toggleVariants>
>(({ className, variant, size, children, ...props }, ref) => (
<ToggleGroupPrimitive.Root
ref={ref}
className={cn("flex items-center justify-center gap-1", className)}
{...props}>
<ToggleGroupContext.Provider value={{ variant, size }}>{children}</ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
));
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName;
const ToggleGroupItem = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> & VariantProps<typeof toggleVariants>
>(({ className, children, variant, size, ...props }, ref) => {
const context = React.useContext(ToggleGroupContext);
return (
<ToggleGroupPrimitive.Item
ref={ref}
className={cn(
toggleVariants({
variant: context.variant || variant,
size: context.size || size,
}),
className
)}
{...props}>
{children}
</ToggleGroupPrimitive.Item>
);
});
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName;
export { ToggleGroup, ToggleGroupItem };
@@ -1,39 +0,0 @@
"use client";
import { cn } from "@/lib/cn";
import * as TogglePrimitive from "@radix-ui/react-toggle";
import { type VariantProps, cva } from "class-variance-authority";
import * as React from "react";
const toggleVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-white transition-colors hover:bg-slate-200 hover:text-slate-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-slate-200 data-[state=on]:text-slate-900 dark:ring-offset-slate-950 dark:hover:bg-slate-800 dark:hover:text-slate-400 dark:focus-visible:ring-slate-300 dark:data-[state=on]:bg-slate-800 dark:data-[state=on]:text-slate-50",
{
variants: {
variant: {
default: "bg-transparent",
outline:
"border border-slate-300 bg-transparent hover:bg-slate-200 hover:text-slate-900 dark:border-slate-800 dark:hover:bg-slate-800 dark:hover:text-slate-50",
},
size: {
default: "h-10 px-3",
sm: "h-9 px-2.5",
lg: "h-11 px-5",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
const Toggle = React.forwardRef<
React.ElementRef<typeof TogglePrimitive.Root>,
React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> & VariantProps<typeof toggleVariants>
>(({ className, variant, size, ...props }, ref) => (
<TogglePrimitive.Root ref={ref} className={cn(toggleVariants({ variant, size, className }))} {...props} />
));
Toggle.displayName = TogglePrimitive.Root.displayName;
export { Toggle, toggleVariants };
@@ -1,27 +0,0 @@
import { Alert, AlertDescription } from "@/modules/ui/components/alert";
import { KeyIcon } from "lucide-react";
import Link from "next/link";
interface UpgradePlanNoticeProps {
message: string;
url: string;
textForUrl: string;
}
export const UpgradePlanNotice = ({ message, url, textForUrl }: UpgradePlanNoticeProps) => {
return (
<Alert className="flex gap-2 bg-slate-50 p-2 [&:has(svg)]:pl-3">
<div className="flex h-5 w-5 items-center justify-center rounded-sm border border-slate-200 bg-white">
<KeyIcon className="h-3 w-3 text-slate-900" />
</div>
<AlertDescription>
<span className="mr-1 text-slate-600">{message}</span>
<span className="underline">
<Link href={url} target="_blank">
{textForUrl}
</Link>
</span>
</AlertDescription>
</Alert>
);
};
@@ -1,46 +0,0 @@
import { useEffect, useState } from "preact/hooks";
const REDIRECT_TIMEOUT = 5;
interface RedirectCountDownProps {
redirectUrl: string | null;
isRedirectDisabled: boolean;
}
export function RedirectCountDown({ redirectUrl, isRedirectDisabled }: RedirectCountDownProps) {
const [timeRemaining, setTimeRemaining] = useState(REDIRECT_TIMEOUT);
useEffect(() => {
let interval: NodeJS.Timeout | undefined;
if (redirectUrl) {
interval = setInterval(() => {
setTimeRemaining((prevTime) => {
if (prevTime <= 0) {
clearInterval(interval);
if (!isRedirectDisabled) {
window.top?.location.replace(redirectUrl);
}
return 0;
}
return prevTime - 1;
});
}, 1000);
}
// Clean up the interval when the component is unmounted
return () => {
clearInterval(interval);
};
}, [redirectUrl, isRedirectDisabled]);
if (!redirectUrl) return null;
return (
<div>
<div className="fb-bg-accent-bg fb-text-subheading fb-mt-10 fb-rounded-md fb-p-2 fb-text-sm">
<span>You&apos;re redirected in </span>
<span>{timeRemaining}</span>
</div>
</div>
);
}