mirror of
				https://github.com/kyantech/Palmr.git
				synced 2025-11-03 21:43:20 +00:00 
			
		
		
		
	fix: update translations and clean up imports in various components
- Translated SMTP connection test messages in French and Polish for better localization. - Removed unused icon imports in the two-factor verification and profile components to streamline the code. - Simplified user data extraction in the login hook for clarity and consistency.
This commit is contained in:
		@@ -15,7 +15,6 @@ export default async function Page(props: { params: Promise<{ slug?: string[] }>
 | 
			
		||||
 | 
			
		||||
  const MDXContent = page.data.body;
 | 
			
		||||
 | 
			
		||||
  // Check if this is an older version page that needs a warning
 | 
			
		||||
  const shouldShowWarning = page.url.startsWith("/docs/2.0.0-beta");
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
 
 | 
			
		||||
@@ -18,7 +18,6 @@ import {
 | 
			
		||||
  TokenResponse,
 | 
			
		||||
} from "./types";
 | 
			
		||||
 | 
			
		||||
// Constants
 | 
			
		||||
const DEFAULT_BASE_URL = "http://localhost:3000";
 | 
			
		||||
const STATE_EXPIRY_TIME = 600000; // 10 minutes
 | 
			
		||||
const CLEANUP_INTERVAL = 5 * 60 * 1000; // 5 minutes
 | 
			
		||||
@@ -43,7 +42,6 @@ export class AuthProvidersService {
 | 
			
		||||
    setInterval(() => this.cleanupExpiredStates(), CLEANUP_INTERVAL);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Utility methods
 | 
			
		||||
  private buildBaseUrl(requestContext?: RequestContextService): string {
 | 
			
		||||
    return requestContext ? `${requestContext.protocol}://${requestContext.host}` : DEFAULT_BASE_URL;
 | 
			
		||||
  }
 | 
			
		||||
@@ -87,7 +85,6 @@ export class AuthProvidersService {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Provider configuration methods
 | 
			
		||||
  private isOfficial(providerName: string): boolean {
 | 
			
		||||
    return providerName in providersConfig.officialProviders;
 | 
			
		||||
  }
 | 
			
		||||
@@ -114,7 +111,6 @@ export class AuthProvidersService {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async resolveEndpoints(provider: any, config: ProviderConfig): Promise<ProviderEndpoints> {
 | 
			
		||||
    // Use custom endpoints if all are provided
 | 
			
		||||
    if (provider.authorizationEndpoint && provider.tokenEndpoint && provider.userInfoEndpoint) {
 | 
			
		||||
      return {
 | 
			
		||||
        authorizationEndpoint: this.resolveEndpointUrl(provider.authorizationEndpoint, provider.issuerUrl),
 | 
			
		||||
@@ -123,7 +119,6 @@ export class AuthProvidersService {
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Try discovery if supported
 | 
			
		||||
    if (config.supportsDiscovery && provider.issuerUrl) {
 | 
			
		||||
      const discoveredEndpoints = await this.attemptDiscovery(provider.issuerUrl);
 | 
			
		||||
      if (discoveredEndpoints) {
 | 
			
		||||
@@ -131,7 +126,6 @@ export class AuthProvidersService {
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Fallback to intelligent endpoints
 | 
			
		||||
    const baseUrl = provider.issuerUrl?.replace(/\/$/, "") || "";
 | 
			
		||||
    const detectedType = detectProviderType(provider.issuerUrl || "");
 | 
			
		||||
    const fallbackPattern = getFallbackEndpoints(detectedType);
 | 
			
		||||
@@ -224,7 +218,6 @@ export class AuthProvidersService {
 | 
			
		||||
    return config.specialHandling?.emailEndpoint || null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // PKCE and OAuth setup methods
 | 
			
		||||
  private setupPkceIfNeeded(provider: any): { codeVerifier?: string; codeChallenge?: string } {
 | 
			
		||||
    const needsPkce = provider.type === DEFAULT_PROVIDER_TYPE;
 | 
			
		||||
 | 
			
		||||
@@ -263,7 +256,6 @@ export class AuthProvidersService {
 | 
			
		||||
    return authUrl.toString();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Callback handling methods
 | 
			
		||||
  private validateAndGetPendingState(state: string): PendingState {
 | 
			
		||||
    const pendingState = this.pendingStates.get(state);
 | 
			
		||||
 | 
			
		||||
@@ -299,7 +291,6 @@ export class AuthProvidersService {
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Public methods
 | 
			
		||||
  async getEnabledProviders(requestContext?: RequestContextService) {
 | 
			
		||||
    const providers = await prisma.authProvider.findMany({
 | 
			
		||||
      where: { enabled: true },
 | 
			
		||||
@@ -605,16 +596,13 @@ export class AuthProvidersService {
 | 
			
		||||
      throw new Error(ERROR_MESSAGES.MISSING_USER_INFO);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // First, check if there's already an auth provider entry for this external ID
 | 
			
		||||
    const existingAuthProvider = await this.findExistingAuthProvider(provider.id, String(externalId));
 | 
			
		||||
    if (existingAuthProvider) {
 | 
			
		||||
      return await this.updateExistingUserFromProvider(existingAuthProvider.user, userInfo);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Check if there's a user with this email
 | 
			
		||||
    const existingUser = await this.findExistingUserByEmail(userInfo.email);
 | 
			
		||||
    if (existingUser) {
 | 
			
		||||
      // Check if this user already has this provider linked
 | 
			
		||||
      const existingUserProvider = await prisma.userAuthProvider.findFirst({
 | 
			
		||||
        where: {
 | 
			
		||||
          userId: existingUser.id,
 | 
			
		||||
 
 | 
			
		||||
@@ -72,10 +72,8 @@ export class EmailService {
 | 
			
		||||
    let smtpConfig: SmtpConfig;
 | 
			
		||||
 | 
			
		||||
    if (config) {
 | 
			
		||||
      // Use provided configuration
 | 
			
		||||
      smtpConfig = config;
 | 
			
		||||
    } else {
 | 
			
		||||
      // Fallback to saved configuration
 | 
			
		||||
      smtpConfig = {
 | 
			
		||||
        smtpEnabled: await this.configService.getValue("smtpEnabled"),
 | 
			
		||||
        smtpHost: await this.configService.getValue("smtpHost"),
 | 
			
		||||
 
 | 
			
		||||
@@ -533,7 +533,6 @@ export class ReverseShareService {
 | 
			
		||||
      const { FilesystemStorageProvider } = await import("../../providers/filesystem-storage.provider.js");
 | 
			
		||||
      const provider = FilesystemStorageProvider.getInstance();
 | 
			
		||||
 | 
			
		||||
      // Use streaming copy for filesystem mode
 | 
			
		||||
      const sourcePath = provider.getFilePath(file.objectName);
 | 
			
		||||
      const fs = await import("fs");
 | 
			
		||||
      const { pipeline } = await import("stream/promises");
 | 
			
		||||
@@ -541,14 +540,11 @@ export class ReverseShareService {
 | 
			
		||||
      const sourceStream = fs.createReadStream(sourcePath);
 | 
			
		||||
      const decryptStream = provider.createDecryptStream();
 | 
			
		||||
 | 
			
		||||
      // Create a passthrough stream to get the decrypted content
 | 
			
		||||
      const { PassThrough } = await import("stream");
 | 
			
		||||
      const passThrough = new PassThrough();
 | 
			
		||||
 | 
			
		||||
      // First, decrypt the source file into the passthrough stream
 | 
			
		||||
      await pipeline(sourceStream, decryptStream, passThrough);
 | 
			
		||||
 | 
			
		||||
      // Then upload the decrypted content
 | 
			
		||||
      await provider.uploadFileFromStream(newObjectName, passThrough);
 | 
			
		||||
    } else {
 | 
			
		||||
      const downloadUrl = await this.fileService.getPresignedGetUrl(file.objectName, 300);
 | 
			
		||||
 
 | 
			
		||||
@@ -31,14 +31,12 @@ export class TwoFactorService {
 | 
			
		||||
      throw new Error("Two-factor authentication is already enabled");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Generate secret
 | 
			
		||||
    const secret = speakeasy.generateSecret({
 | 
			
		||||
      name: `${appName || "Palmr"}:${userEmail}`,
 | 
			
		||||
      issuer: appName || "Palmr",
 | 
			
		||||
      length: 32,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Generate QR code
 | 
			
		||||
    const qrCodeUrl = await QRCode.toDataURL(secret.otpauth_url || "");
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
@@ -70,7 +68,7 @@ export class TwoFactorService {
 | 
			
		||||
      secret: secret,
 | 
			
		||||
      encoding: "base32",
 | 
			
		||||
      token: token,
 | 
			
		||||
      window: 1, // Allow 1 step behind/ahead for clock drift
 | 
			
		||||
      window: 1,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (!verified) {
 | 
			
		||||
 
 | 
			
		||||
@@ -179,7 +179,6 @@ export class FilesystemStorageProvider implements StorageProvider {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async uploadFile(objectName: string, buffer: Buffer): Promise<void> {
 | 
			
		||||
    // For backward compatibility, convert buffer to stream and use streaming upload
 | 
			
		||||
    const filePath = this.getFilePath(objectName);
 | 
			
		||||
    const dir = path.dirname(filePath);
 | 
			
		||||
 | 
			
		||||
@@ -197,7 +196,6 @@ export class FilesystemStorageProvider implements StorageProvider {
 | 
			
		||||
 | 
			
		||||
    await fs.mkdir(dir, { recursive: true });
 | 
			
		||||
 | 
			
		||||
    // Use the new temp file system for better organization
 | 
			
		||||
    const tempPath = getTempFilePath(objectName);
 | 
			
		||||
    const tempDir = path.dirname(tempPath);
 | 
			
		||||
 | 
			
		||||
@@ -308,10 +306,8 @@ export class FilesystemStorageProvider implements StorageProvider {
 | 
			
		||||
   */
 | 
			
		||||
  private async cleanupTempFile(tempPath: string): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      // Remove the temp file
 | 
			
		||||
      await fs.unlink(tempPath);
 | 
			
		||||
 | 
			
		||||
      // Try to remove the parent directory if it's empty
 | 
			
		||||
      const tempDir = path.dirname(tempPath);
 | 
			
		||||
      try {
 | 
			
		||||
        const files = await fs.readdir(tempDir);
 | 
			
		||||
@@ -319,7 +315,6 @@ export class FilesystemStorageProvider implements StorageProvider {
 | 
			
		||||
          await fs.rmdir(tempDir);
 | 
			
		||||
        }
 | 
			
		||||
      } catch (dirError: any) {
 | 
			
		||||
        // Ignore errors when trying to remove directory (might not be empty or might not exist)
 | 
			
		||||
        if (dirError.code !== "ENOTEMPTY" && dirError.code !== "ENOENT") {
 | 
			
		||||
          console.warn("Warning: Could not remove temp directory:", dirError.message);
 | 
			
		||||
        }
 | 
			
		||||
@@ -338,11 +333,10 @@ export class FilesystemStorageProvider implements StorageProvider {
 | 
			
		||||
    try {
 | 
			
		||||
      const tempUploadsDir = directoriesConfig.tempUploads;
 | 
			
		||||
 | 
			
		||||
      // Check if temp-uploads directory exists
 | 
			
		||||
      try {
 | 
			
		||||
        await fs.access(tempUploadsDir);
 | 
			
		||||
      } catch {
 | 
			
		||||
        return; // Directory doesn't exist, nothing to clean
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const items = await fs.readdir(tempUploadsDir);
 | 
			
		||||
@@ -354,14 +348,12 @@ export class FilesystemStorageProvider implements StorageProvider {
 | 
			
		||||
          const stat = await fs.stat(itemPath);
 | 
			
		||||
 | 
			
		||||
          if (stat.isDirectory()) {
 | 
			
		||||
            // Check if directory is empty
 | 
			
		||||
            const dirContents = await fs.readdir(itemPath);
 | 
			
		||||
            if (dirContents.length === 0) {
 | 
			
		||||
              await fs.rmdir(itemPath);
 | 
			
		||||
              console.log(`🧹 Cleaned up empty temp directory: ${itemPath}`);
 | 
			
		||||
            }
 | 
			
		||||
          } else if (stat.isFile()) {
 | 
			
		||||
            // Check if file is older than 1 hour (stale temp files)
 | 
			
		||||
            const oneHourAgo = Date.now() - 60 * 60 * 1000;
 | 
			
		||||
            if (stat.mtime.getTime() < oneHourAgo) {
 | 
			
		||||
              await fs.unlink(itemPath);
 | 
			
		||||
@@ -369,7 +361,6 @@ export class FilesystemStorageProvider implements StorageProvider {
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        } catch (error: any) {
 | 
			
		||||
          // Ignore errors for individual items
 | 
			
		||||
          if (error.code !== "ENOENT") {
 | 
			
		||||
            console.warn(`Warning: Could not process temp item ${itemPath}:`, error.message);
 | 
			
		||||
          }
 | 
			
		||||
 
 | 
			
		||||
@@ -64,7 +64,6 @@ export default [
 | 
			
		||||
      "@typescript-eslint/no-var-requires": "off",
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  // Ignore ESLint errors in @/ui directory
 | 
			
		||||
  {
 | 
			
		||||
    ignores: ["src/components/ui/**/*"],
 | 
			
		||||
  },
 | 
			
		||||
 
 | 
			
		||||
@@ -1075,8 +1075,8 @@
 | 
			
		||||
        "description": "URL de base du serveur Palmr (ex: https://palmr.exemple.com)"
 | 
			
		||||
      },
 | 
			
		||||
      "testSmtp": {
 | 
			
		||||
        "title": "[TO_TRANSLATE] Test SMTP Connection",
 | 
			
		||||
        "description": "[TO_TRANSLATE] Test if the SMTP configuration is valid"
 | 
			
		||||
        "title": "Test de la Connexion SMTP",
 | 
			
		||||
        "description": "Tester si la configuration SMTP est valide"
 | 
			
		||||
      },
 | 
			
		||||
      "smtpNoAuth": {
 | 
			
		||||
        "title": "Pas d'Authentification",
 | 
			
		||||
 
 | 
			
		||||
@@ -1152,7 +1152,7 @@
 | 
			
		||||
      "description": "To udostępnienie mogło zostać usunięte lub wygasło."
 | 
			
		||||
    },
 | 
			
		||||
    "pageTitle": "Udostępnij",
 | 
			
		||||
    "downloadAll": "[TO_TRANSLATE] Download All"
 | 
			
		||||
    "downloadAll": "Pobierz wszystkie"
 | 
			
		||||
  },
 | 
			
		||||
  "shareActions": {
 | 
			
		||||
    "deleteTitle": "Usuń udostępnienie",
 | 
			
		||||
 
 | 
			
		||||
@@ -213,7 +213,6 @@ export function FileUploadSection({ reverseShare, password, alias, onUploadSucce
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Check if either name or email is required
 | 
			
		||||
    const nameRequired = reverseShare.nameFieldRequired === "REQUIRED";
 | 
			
		||||
    const emailRequired = reverseShare.emailFieldRequired === "REQUIRED";
 | 
			
		||||
 | 
			
		||||
@@ -227,9 +226,6 @@ export function FileUploadSection({ reverseShare, password, alias, onUploadSucce
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Remove the validation that requires at least one field when both are optional
 | 
			
		||||
    // When both fields are OPTIONAL, they should be truly optional (can be empty)
 | 
			
		||||
 | 
			
		||||
    return true;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
@@ -275,8 +271,6 @@ export function FileUploadSection({ reverseShare, password, alias, onUploadSucce
 | 
			
		||||
 | 
			
		||||
    if (emailRequired && !uploaderEmail.trim()) return false;
 | 
			
		||||
 | 
			
		||||
    // When both fields are OPTIONAL, they should be truly optional (can be empty)
 | 
			
		||||
    // Remove the check that requires at least one field to be filled
 | 
			
		||||
    return true;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -460,7 +460,6 @@ export function ReceivedFilesModal({
 | 
			
		||||
 | 
			
		||||
  const { editingFile, editValue, setEditValue, inputRef, startEdit, cancelEdit } = useFileEdit();
 | 
			
		||||
 | 
			
		||||
  // Clear selections when files change
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setSelectedFiles(new Set());
 | 
			
		||||
  }, [reverseShare?.files]);
 | 
			
		||||
@@ -651,7 +650,6 @@ export function ReceivedFilesModal({
 | 
			
		||||
          document.body.removeChild(a);
 | 
			
		||||
          URL.revokeObjectURL(url);
 | 
			
		||||
 | 
			
		||||
          // Clear selections after successful download
 | 
			
		||||
          setSelectedFiles(new Set());
 | 
			
		||||
        })(),
 | 
			
		||||
        {
 | 
			
		||||
@@ -684,7 +682,6 @@ export function ReceivedFilesModal({
 | 
			
		||||
 | 
			
		||||
          await Promise.all(copyPromises);
 | 
			
		||||
 | 
			
		||||
          // Clear selections after successful copy
 | 
			
		||||
          setSelectedFiles(new Set());
 | 
			
		||||
        } finally {
 | 
			
		||||
          setBulkCopying(false);
 | 
			
		||||
@@ -732,7 +729,6 @@ export function ReceivedFilesModal({
 | 
			
		||||
 | 
			
		||||
          await Promise.all(deletePromises);
 | 
			
		||||
 | 
			
		||||
          // Clear selections and refresh data
 | 
			
		||||
          setSelectedFiles(new Set());
 | 
			
		||||
          setFilesToDeleteBulk([]);
 | 
			
		||||
          if (onRefresh) {
 | 
			
		||||
 
 | 
			
		||||
@@ -44,7 +44,6 @@ export default function AuthCallbackPage() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (token) {
 | 
			
		||||
      // Set the token in a cookie and redirect to dashboard (using Palmr's standard cookie name)
 | 
			
		||||
      document.cookie = `token=${token}; path=/; max-age=${7 * 24 * 60 * 60}; samesite=lax`;
 | 
			
		||||
 | 
			
		||||
      toast.success("Successfully authenticated!");
 | 
			
		||||
@@ -52,7 +51,6 @@ export default function AuthCallbackPage() {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // If no token or error, redirect to login
 | 
			
		||||
    router.push("/login");
 | 
			
		||||
  }, [router, searchParams]);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
"use client";
 | 
			
		||||
 | 
			
		||||
import { useState } from "react";
 | 
			
		||||
import { IconEye, IconEyeClosed, IconShield } from "@tabler/icons-react";
 | 
			
		||||
import { IconShield } from "@tabler/icons-react";
 | 
			
		||||
import { useTranslations } from "next-intl";
 | 
			
		||||
 | 
			
		||||
import { Button } from "@/components/ui/button";
 | 
			
		||||
@@ -25,7 +25,7 @@ export function TwoFactorVerification({
 | 
			
		||||
  error,
 | 
			
		||||
  isSubmitting,
 | 
			
		||||
}: TwoFactorVerificationProps) {
 | 
			
		||||
  const t = useTranslations("twoFactor");
 | 
			
		||||
  const t = useTranslations();
 | 
			
		||||
  const [showBackupCode, setShowBackupCode] = useState(false);
 | 
			
		||||
 | 
			
		||||
  const handleSubmit = (e: React.FormEvent) => {
 | 
			
		||||
@@ -50,22 +50,22 @@ export function TwoFactorVerification({
 | 
			
		||||
            <IconShield className="h-8 w-8 text-primary" />
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <CardTitle>{t("verification.title")}</CardTitle>
 | 
			
		||||
        <CardTitle>{t("twoFactor.verification.title")}</CardTitle>
 | 
			
		||||
        <CardDescription>
 | 
			
		||||
          {showBackupCode ? t("verification.backupDescription") : t("verification.description")}
 | 
			
		||||
          {showBackupCode ? t("twoFactor.verification.backupDescription") : t("twoFactor.verification.description")}
 | 
			
		||||
        </CardDescription>
 | 
			
		||||
      </CardHeader>
 | 
			
		||||
      <CardContent>
 | 
			
		||||
        <form onSubmit={handleSubmit} className="space-y-4">
 | 
			
		||||
          <div>
 | 
			
		||||
            <Label htmlFor="twoFactorCode" className="mb-2">
 | 
			
		||||
              {showBackupCode ? t("verification.backupCode") : t("verification.verificationCode")}
 | 
			
		||||
              {showBackupCode ? t("twoFactor.verification.backupCode") : t("twoFactor.verification.verificationCode")}
 | 
			
		||||
            </Label>
 | 
			
		||||
            {showBackupCode ? (
 | 
			
		||||
              <Input
 | 
			
		||||
                id="twoFactorCode"
 | 
			
		||||
                type="text"
 | 
			
		||||
                placeholder={t("verification.backupCodePlaceholder")}
 | 
			
		||||
                placeholder={t("twoFactor.verification.backupCodePlaceholder")}
 | 
			
		||||
                value={twoFactorCode}
 | 
			
		||||
                onChange={handleBackupCodeChange}
 | 
			
		||||
                className="text-center tracking-widest font-mono"
 | 
			
		||||
@@ -95,7 +95,7 @@ export function TwoFactorVerification({
 | 
			
		||||
            className="w-full"
 | 
			
		||||
            disabled={isSubmitting || twoFactorCode.length < (showBackupCode ? 8 : 6)}
 | 
			
		||||
          >
 | 
			
		||||
            {isSubmitting ? t("verification.verifying") : t("verification.verify")}
 | 
			
		||||
            {isSubmitting ? t("twoFactor.verification.verifying") : t("twoFactor.verification.verify")}
 | 
			
		||||
          </Button>
 | 
			
		||||
 | 
			
		||||
          {error && (
 | 
			
		||||
@@ -112,7 +112,9 @@ export function TwoFactorVerification({
 | 
			
		||||
              }}
 | 
			
		||||
              className="text-sm text-muted-foreground hover:text-primary transition-colors"
 | 
			
		||||
            >
 | 
			
		||||
              {showBackupCode ? t("verification.useAuthenticatorCode") : t("verification.useBackupCode")}
 | 
			
		||||
              {showBackupCode
 | 
			
		||||
                ? t("twoFactor.verification.useAuthenticatorCode")
 | 
			
		||||
                : t("twoFactor.verification.useBackupCode")}
 | 
			
		||||
            </Button>
 | 
			
		||||
          </div>
 | 
			
		||||
        </form>
 | 
			
		||||
 
 | 
			
		||||
@@ -150,8 +150,7 @@ export function useLogin() {
 | 
			
		||||
        token: twoFactorCode,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      const userResponse = await getCurrentUser();
 | 
			
		||||
      const { isAdmin, ...userData } = userResponse.data.user;
 | 
			
		||||
      const { isAdmin, ...userData } = response.data.user;
 | 
			
		||||
 | 
			
		||||
      setUser(userData);
 | 
			
		||||
      setIsAdmin(isAdmin);
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,6 @@ import {
 | 
			
		||||
  IconKey,
 | 
			
		||||
  IconShield,
 | 
			
		||||
  IconShieldCheck,
 | 
			
		||||
  IconX,
 | 
			
		||||
} from "@tabler/icons-react";
 | 
			
		||||
import { useTranslations } from "next-intl";
 | 
			
		||||
 | 
			
		||||
@@ -29,7 +28,7 @@ import { Label } from "@/components/ui/label";
 | 
			
		||||
import { useTwoFactor } from "../hooks/use-two-factor";
 | 
			
		||||
 | 
			
		||||
export function TwoFactorForm() {
 | 
			
		||||
  const t = useTranslations("twoFactor");
 | 
			
		||||
  const t = useTranslations();
 | 
			
		||||
  const {
 | 
			
		||||
    isLoading,
 | 
			
		||||
    status,
 | 
			
		||||
@@ -61,7 +60,7 @@ export function TwoFactorForm() {
 | 
			
		||||
        <CardHeader>
 | 
			
		||||
          <CardTitle className="flex items-center gap-2">
 | 
			
		||||
            <IconShield className="h-5 w-5" />
 | 
			
		||||
            {t("title")}
 | 
			
		||||
            {t("twoFactor.title")}
 | 
			
		||||
          </CardTitle>
 | 
			
		||||
          <CardDescription>Loading...</CardDescription>
 | 
			
		||||
        </CardHeader>
 | 
			
		||||
@@ -79,9 +78,9 @@ export function TwoFactorForm() {
 | 
			
		||||
            ) : (
 | 
			
		||||
              <IconShield className="h-5 w-5" />
 | 
			
		||||
            )}
 | 
			
		||||
            {t("title")}
 | 
			
		||||
            {t("twoFactor.title")}
 | 
			
		||||
          </CardTitle>
 | 
			
		||||
          <CardDescription>{status.enabled ? t("enabled") : t("description")}</CardDescription>
 | 
			
		||||
          <CardDescription>{status.enabled ? t("twoFactor.enabled") : t("twoFactor.description")}</CardDescription>
 | 
			
		||||
        </CardHeader>
 | 
			
		||||
        <CardContent className="space-y-4">
 | 
			
		||||
          <div className="flex items-center justify-between">
 | 
			
		||||
@@ -89,7 +88,7 @@ export function TwoFactorForm() {
 | 
			
		||||
              <p className="font-medium">Status: {status.enabled ? "Enabled" : "Disabled"}</p>
 | 
			
		||||
              {status.enabled && (
 | 
			
		||||
                <p className="text-sm text-muted-foreground">
 | 
			
		||||
                  {t("backupCodes.available", { count: status.availableBackupCodes })}
 | 
			
		||||
                  {t("twoFactor.backupCodes.available", { count: status.availableBackupCodes })}
 | 
			
		||||
                </p>
 | 
			
		||||
              )}
 | 
			
		||||
            </div>
 | 
			
		||||
@@ -98,7 +97,7 @@ export function TwoFactorForm() {
 | 
			
		||||
                <>
 | 
			
		||||
                  <Button variant="outline" onClick={generateNewBackupCodes} disabled={isLoading}>
 | 
			
		||||
                    <IconKey className="h-4 w-4" />
 | 
			
		||||
                    {t("backupCodes.generateNew")}
 | 
			
		||||
                    {t("twoFactor.backupCodes.generateNew")}
 | 
			
		||||
                  </Button>
 | 
			
		||||
                  <Button variant="destructive" onClick={() => setIsDisableModalOpen(true)} disabled={isLoading}>
 | 
			
		||||
                    Disable 2FA
 | 
			
		||||
@@ -119,8 +118,8 @@ export function TwoFactorForm() {
 | 
			
		||||
      <Dialog open={isSetupModalOpen} onOpenChange={setIsSetupModalOpen}>
 | 
			
		||||
        <DialogContent className="max-w-md">
 | 
			
		||||
          <DialogHeader>
 | 
			
		||||
            <DialogTitle>{t("setup.title")}</DialogTitle>
 | 
			
		||||
            <DialogDescription>{t("setup.description")}</DialogDescription>
 | 
			
		||||
            <DialogTitle>{t("twoFactor.setup.title")}</DialogTitle>
 | 
			
		||||
            <DialogDescription>{t("twoFactor.setup.description")}</DialogDescription>
 | 
			
		||||
          </DialogHeader>
 | 
			
		||||
 | 
			
		||||
          {setupData && (
 | 
			
		||||
@@ -132,7 +131,7 @@ export function TwoFactorForm() {
 | 
			
		||||
 | 
			
		||||
              {/* Manual Entry */}
 | 
			
		||||
              <div>
 | 
			
		||||
                <Label className="text-sm font-medium">{t("setup.manualEntryKey")}</Label>
 | 
			
		||||
                <Label className="text-sm font-medium">{t("twoFactor.setup.manualEntryKey")}</Label>
 | 
			
		||||
                <div className="flex items-center gap-2 mt-1">
 | 
			
		||||
                  <Input value={setupData.manualEntryKey} readOnly className="font-mono text-xs" />
 | 
			
		||||
                  <Button
 | 
			
		||||
@@ -148,7 +147,7 @@ export function TwoFactorForm() {
 | 
			
		||||
              {/* Verification Code */}
 | 
			
		||||
              <div>
 | 
			
		||||
                <Label htmlFor="verification-code" className="mb-2">
 | 
			
		||||
                  {t("setup.verificationCode")}
 | 
			
		||||
                  {t("twoFactor.setup.verificationCode")}
 | 
			
		||||
                </Label>
 | 
			
		||||
                <div className="flex justify-start">
 | 
			
		||||
                  <InputOTP maxLength={6} value={verificationCode} onChange={setVerificationCode}>
 | 
			
		||||
@@ -162,17 +161,19 @@ export function TwoFactorForm() {
 | 
			
		||||
                    </InputOTPGroup>
 | 
			
		||||
                  </InputOTP>
 | 
			
		||||
                </div>
 | 
			
		||||
                <span className="text-sm text-muted-foreground mt-1">{t("setup.verificationCodeDescription")}</span>
 | 
			
		||||
                <span className="text-sm text-muted-foreground mt-1">
 | 
			
		||||
                  {t("twoFactor.setup.verificationCodeDescription")}
 | 
			
		||||
                </span>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          )}
 | 
			
		||||
 | 
			
		||||
          <DialogFooter>
 | 
			
		||||
            <Button variant="outline" onClick={() => setIsSetupModalOpen(false)} disabled={isLoading}>
 | 
			
		||||
              {t("setup.cancel")}
 | 
			
		||||
              {t("twoFactor.setup.cancel")}
 | 
			
		||||
            </Button>
 | 
			
		||||
            <Button onClick={verifySetup} disabled={isLoading || !verificationCode || verificationCode.length !== 6}>
 | 
			
		||||
              {t("setup.verifyAndEnable")}
 | 
			
		||||
              {t("twoFactor.setup.verifyAndEnable")}
 | 
			
		||||
            </Button>
 | 
			
		||||
          </DialogFooter>
 | 
			
		||||
        </DialogContent>
 | 
			
		||||
@@ -182,20 +183,20 @@ export function TwoFactorForm() {
 | 
			
		||||
      <Dialog open={isDisableModalOpen} onOpenChange={setIsDisableModalOpen}>
 | 
			
		||||
        <DialogContent className="max-w-md">
 | 
			
		||||
          <DialogHeader>
 | 
			
		||||
            <DialogTitle>{t("disable.title")}</DialogTitle>
 | 
			
		||||
            <DialogDescription>{t("disable.description")}</DialogDescription>
 | 
			
		||||
            <DialogTitle>{t("twoFactor.disable.title")}</DialogTitle>
 | 
			
		||||
            <DialogDescription>{t("twoFactor.disable.description")}</DialogDescription>
 | 
			
		||||
          </DialogHeader>
 | 
			
		||||
 | 
			
		||||
          <div className="space-y-4">
 | 
			
		||||
            <div>
 | 
			
		||||
              <Label htmlFor="disable-password" className="mb-2">
 | 
			
		||||
                {t("disable.password")}
 | 
			
		||||
                {t("twoFactor.disable.password")}
 | 
			
		||||
              </Label>
 | 
			
		||||
              <div className="relative">
 | 
			
		||||
                <Input
 | 
			
		||||
                  id="disable-password"
 | 
			
		||||
                  type={showPassword ? "text" : "password"}
 | 
			
		||||
                  placeholder={t("disable.passwordPlaceholder")}
 | 
			
		||||
                  placeholder={t("twoFactor.disable.passwordPlaceholder")}
 | 
			
		||||
                  value={disablePassword}
 | 
			
		||||
                  onChange={(e) => setDisablePassword(e.target.value)}
 | 
			
		||||
                />
 | 
			
		||||
@@ -218,10 +219,10 @@ export function TwoFactorForm() {
 | 
			
		||||
 | 
			
		||||
          <DialogFooter>
 | 
			
		||||
            <Button variant="outline" onClick={() => setIsDisableModalOpen(false)} disabled={isLoading}>
 | 
			
		||||
              {t("disable.cancel")}
 | 
			
		||||
              {t("twoFactor.disable.cancel")}
 | 
			
		||||
            </Button>
 | 
			
		||||
            <Button variant="destructive" onClick={disable2FA} disabled={isLoading || !disablePassword}>
 | 
			
		||||
              {t("disable.confirm")}
 | 
			
		||||
              {t("twoFactor.disable.confirm")}
 | 
			
		||||
            </Button>
 | 
			
		||||
          </DialogFooter>
 | 
			
		||||
        </DialogContent>
 | 
			
		||||
@@ -231,8 +232,8 @@ export function TwoFactorForm() {
 | 
			
		||||
      <Dialog open={isBackupCodesModalOpen} onOpenChange={setIsBackupCodesModalOpen}>
 | 
			
		||||
        <DialogContent className="max-w-md">
 | 
			
		||||
          <DialogHeader>
 | 
			
		||||
            <DialogTitle>{t("backupCodes.title")}</DialogTitle>
 | 
			
		||||
            <DialogDescription>{t("backupCodes.description")}</DialogDescription>
 | 
			
		||||
            <DialogTitle>{t("twoFactor.backupCodes.title")}</DialogTitle>
 | 
			
		||||
            <DialogDescription>{t("twoFactor.backupCodes.description")}</DialogDescription>
 | 
			
		||||
          </DialogHeader>
 | 
			
		||||
 | 
			
		||||
          <div className="space-y-4">
 | 
			
		||||
@@ -249,11 +250,11 @@ export function TwoFactorForm() {
 | 
			
		||||
            <div className="flex gap-2">
 | 
			
		||||
              <Button variant="outline" onClick={downloadBackupCodes} className="flex-1">
 | 
			
		||||
                <IconDownload className="h-4 w-4" />
 | 
			
		||||
                {t("backupCodes.download")}
 | 
			
		||||
                {t("twoFactor.backupCodes.download")}
 | 
			
		||||
              </Button>
 | 
			
		||||
              <Button variant="outline" onClick={copyBackupCodes} className="flex-1">
 | 
			
		||||
                <IconCopy className="h-4 w-4" />
 | 
			
		||||
                {t("backupCodes.copyToClipboard")}
 | 
			
		||||
                {t("twoFactor.backupCodes.copyToClipboard")}
 | 
			
		||||
              </Button>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
@@ -265,7 +266,7 @@ export function TwoFactorForm() {
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <DialogFooter>
 | 
			
		||||
            <Button onClick={() => setIsBackupCodesModalOpen(false)}>{t("backupCodes.savedMessage")}</Button>
 | 
			
		||||
            <Button onClick={() => setIsBackupCodesModalOpen(false)}>{t("twoFactor.backupCodes.savedMessage")}</Button>
 | 
			
		||||
          </DialogFooter>
 | 
			
		||||
        </DialogContent>
 | 
			
		||||
      </Dialog>
 | 
			
		||||
 
 | 
			
		||||
@@ -12,14 +12,10 @@ import {
 | 
			
		||||
  getTwoFactorStatus,
 | 
			
		||||
  verifyTwoFactorSetup,
 | 
			
		||||
} from "@/http/endpoints/auth/two-factor";
 | 
			
		||||
import type {
 | 
			
		||||
  TwoFactorSetupResponse,
 | 
			
		||||
  TwoFactorStatus,
 | 
			
		||||
  VerifySetupResponse,
 | 
			
		||||
} from "@/http/endpoints/auth/two-factor/types";
 | 
			
		||||
import type { TwoFactorSetupResponse, TwoFactorStatus } from "@/http/endpoints/auth/two-factor/types";
 | 
			
		||||
 | 
			
		||||
export function useTwoFactor() {
 | 
			
		||||
  const t = useTranslations("twoFactor");
 | 
			
		||||
  const t = useTranslations();
 | 
			
		||||
  const { appName } = useAppInfo();
 | 
			
		||||
  const [isLoading, setIsLoading] = useState(true);
 | 
			
		||||
  const [status, setStatus] = useState<TwoFactorStatus>({
 | 
			
		||||
@@ -35,7 +31,6 @@ export function useTwoFactor() {
 | 
			
		||||
  const [verificationCode, setVerificationCode] = useState("");
 | 
			
		||||
  const [disablePassword, setDisablePassword] = useState("");
 | 
			
		||||
 | 
			
		||||
  // Load 2FA status
 | 
			
		||||
  const loadStatus = useCallback(async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      setIsLoading(true);
 | 
			
		||||
@@ -43,13 +38,12 @@ export function useTwoFactor() {
 | 
			
		||||
      setStatus(response.data);
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error("Failed to load 2FA status:", error);
 | 
			
		||||
      toast.error(t("messages.statusLoadFailed"));
 | 
			
		||||
      toast.error(t("twoFactor.messages.statusLoadFailed"));
 | 
			
		||||
    } finally {
 | 
			
		||||
      setIsLoading(false);
 | 
			
		||||
    }
 | 
			
		||||
  }, []);
 | 
			
		||||
  }, [t]);
 | 
			
		||||
 | 
			
		||||
  // Generate 2FA setup
 | 
			
		||||
  const startSetup = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      setIsLoading(true);
 | 
			
		||||
@@ -61,17 +55,16 @@ export function useTwoFactor() {
 | 
			
		||||
      if (error.response?.data?.error) {
 | 
			
		||||
        toast.error(error.response.data.error);
 | 
			
		||||
      } else {
 | 
			
		||||
        toast.error(t("messages.setupFailed"));
 | 
			
		||||
        toast.error(t("twoFactor.messages.setupFailed"));
 | 
			
		||||
      }
 | 
			
		||||
    } finally {
 | 
			
		||||
      setIsLoading(false);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // Verify setup and enable 2FA
 | 
			
		||||
  const verifySetup = async () => {
 | 
			
		||||
    if (!setupData || !verificationCode) {
 | 
			
		||||
      toast.error(t("messages.enterVerificationCode"));
 | 
			
		||||
      toast.error(t("twoFactor.messages.enterVerificationCode"));
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -87,7 +80,7 @@ export function useTwoFactor() {
 | 
			
		||||
        setIsSetupModalOpen(false);
 | 
			
		||||
        setIsBackupCodesModalOpen(true);
 | 
			
		||||
        setVerificationCode("");
 | 
			
		||||
        toast.success(t("messages.enabledSuccess"));
 | 
			
		||||
        toast.success(t("twoFactor.messages.enabledSuccess"));
 | 
			
		||||
        await loadStatus();
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error: any) {
 | 
			
		||||
@@ -95,17 +88,16 @@ export function useTwoFactor() {
 | 
			
		||||
      if (error.response?.data?.error) {
 | 
			
		||||
        toast.error(error.response.data.error);
 | 
			
		||||
      } else {
 | 
			
		||||
        toast.error(t("messages.verificationFailed"));
 | 
			
		||||
        toast.error(t("twoFactor.messages.verificationFailed"));
 | 
			
		||||
      }
 | 
			
		||||
    } finally {
 | 
			
		||||
      setIsLoading(false);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // Disable 2FA
 | 
			
		||||
  const disable2FA = async () => {
 | 
			
		||||
    if (!disablePassword) {
 | 
			
		||||
      toast.error(t("messages.enterPassword"));
 | 
			
		||||
      toast.error(t("twoFactor.messages.enterPassword"));
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -118,7 +110,7 @@ export function useTwoFactor() {
 | 
			
		||||
      if (response.data.success) {
 | 
			
		||||
        setIsDisableModalOpen(false);
 | 
			
		||||
        setDisablePassword("");
 | 
			
		||||
        toast.success(t("messages.disabledSuccess"));
 | 
			
		||||
        toast.success(t("twoFactor.messages.disabledSuccess"));
 | 
			
		||||
        await loadStatus();
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error: any) {
 | 
			
		||||
@@ -126,7 +118,7 @@ export function useTwoFactor() {
 | 
			
		||||
      if (error.response?.data?.error) {
 | 
			
		||||
        toast.error(error.response.data.error);
 | 
			
		||||
      } else {
 | 
			
		||||
        toast.error(t("messages.disableFailed"));
 | 
			
		||||
        toast.error(t("twoFactor.messages.disableFailed"));
 | 
			
		||||
      }
 | 
			
		||||
    } finally {
 | 
			
		||||
      setIsLoading(false);
 | 
			
		||||
@@ -139,14 +131,14 @@ export function useTwoFactor() {
 | 
			
		||||
      const response = await generateBackupCodes();
 | 
			
		||||
      setBackupCodes(response.data.backupCodes);
 | 
			
		||||
      setIsBackupCodesModalOpen(true);
 | 
			
		||||
      toast.success(t("messages.backupCodesGenerated"));
 | 
			
		||||
      toast.success(t("twoFactor.messages.backupCodesGenerated"));
 | 
			
		||||
      await loadStatus();
 | 
			
		||||
    } catch (error: any) {
 | 
			
		||||
      console.error("Failed to generate backup codes:", error);
 | 
			
		||||
      if (error.response?.data?.error) {
 | 
			
		||||
        toast.error(error.response.data.error);
 | 
			
		||||
      } else {
 | 
			
		||||
        toast.error(t("messages.backupCodesFailed"));
 | 
			
		||||
        toast.error(t("twoFactor.messages.backupCodesFailed"));
 | 
			
		||||
      }
 | 
			
		||||
    } finally {
 | 
			
		||||
      setIsLoading(false);
 | 
			
		||||
@@ -169,9 +161,9 @@ export function useTwoFactor() {
 | 
			
		||||
  const copyBackupCodes = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      await navigator.clipboard.writeText(backupCodes.join("\n"));
 | 
			
		||||
      toast.success(t("messages.backupCodesCopied"));
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      toast.error(t("messages.backupCodesCopyFailed"));
 | 
			
		||||
      toast.success(t("twoFactor.messages.backupCodesCopied"));
 | 
			
		||||
    } catch {
 | 
			
		||||
      toast.error(t("twoFactor.messages.backupCodesCopyFailed"));
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
@@ -180,7 +172,6 @@ export function useTwoFactor() {
 | 
			
		||||
  }, [loadStatus]);
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    // State
 | 
			
		||||
    isLoading,
 | 
			
		||||
    status,
 | 
			
		||||
    setupData,
 | 
			
		||||
@@ -188,19 +179,16 @@ export function useTwoFactor() {
 | 
			
		||||
    verificationCode,
 | 
			
		||||
    disablePassword,
 | 
			
		||||
 | 
			
		||||
    // Modal states
 | 
			
		||||
    isSetupModalOpen,
 | 
			
		||||
    isDisableModalOpen,
 | 
			
		||||
    isBackupCodesModalOpen,
 | 
			
		||||
 | 
			
		||||
    // Setters
 | 
			
		||||
    setVerificationCode,
 | 
			
		||||
    setDisablePassword,
 | 
			
		||||
    setIsSetupModalOpen,
 | 
			
		||||
    setIsDisableModalOpen,
 | 
			
		||||
    setIsBackupCodesModalOpen,
 | 
			
		||||
 | 
			
		||||
    // Actions
 | 
			
		||||
    startSetup,
 | 
			
		||||
    verifySetup,
 | 
			
		||||
    disable2FA,
 | 
			
		||||
 
 | 
			
		||||
@@ -70,7 +70,6 @@ export function EditProviderForm({
 | 
			
		||||
  const [showClientSecret, setShowClientSecret] = useState(false);
 | 
			
		||||
  const isOfficial = provider.isOfficial;
 | 
			
		||||
 | 
			
		||||
  // Função para identificar providers oficiais que não devem ter o campo de provider URL editável
 | 
			
		||||
  const isProviderUrlEditable = (providerName: string): boolean => {
 | 
			
		||||
    const nonEditableProviders = ["google", "discord", "github"];
 | 
			
		||||
    return !nonEditableProviders.includes(providerName.toLowerCase());
 | 
			
		||||
 
 | 
			
		||||
@@ -77,7 +77,6 @@ export function SettingsInput({
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Use FileSizeInput for storage size fields
 | 
			
		||||
    if (config.key === "maxFileSize" || config.key === "maxTotalStoragePerUser") {
 | 
			
		||||
      const currentValue = watch(`configs.${config.key}`) || "0";
 | 
			
		||||
      return (
 | 
			
		||||
 
 | 
			
		||||
@@ -41,7 +41,6 @@ import { Dialog, DialogContent, DialogTitle, DialogTrigger } from "@/components/
 | 
			
		||||
import { Input } from "@/components/ui/input";
 | 
			
		||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
 | 
			
		||||
 | 
			
		||||
// Custom CSS for additional grid columns
 | 
			
		||||
const customStyles = `
 | 
			
		||||
  .grid-cols-16 {
 | 
			
		||||
    grid-template-columns: repeat(16, minmax(0, 1fr));
 | 
			
		||||
@@ -51,9 +50,8 @@ const customStyles = `
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
// Inject custom styles
 | 
			
		||||
if (typeof document !== "undefined") {
 | 
			
		||||
  const styleElement = document.createElement("style");
 | 
			
		||||
  const styleElement = document.createElement("iconPicker.style");
 | 
			
		||||
  styleElement.textContent = customStyles;
 | 
			
		||||
  if (!document.head.querySelector("style[data-icon-picker]")) {
 | 
			
		||||
    styleElement.setAttribute("data-icon-picker", "true");
 | 
			
		||||
@@ -73,11 +71,9 @@ interface IconPickerProps {
 | 
			
		||||
  placeholder?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Lazy loading configuration
 | 
			
		||||
const ICONS_PER_BATCH = 100;
 | 
			
		||||
const SCROLL_THRESHOLD = 200; // pixels from bottom to trigger load
 | 
			
		||||
const SCROLL_THRESHOLD = 200;
 | 
			
		||||
 | 
			
		||||
// Virtualized Icon Grid Component with Lazy Loading
 | 
			
		||||
interface VirtualizedIconGridProps {
 | 
			
		||||
  icons: IconData[];
 | 
			
		||||
  onIconSelect: (iconName: string) => void;
 | 
			
		||||
@@ -86,13 +82,12 @@ interface VirtualizedIconGridProps {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function VirtualizedIconGrid({ icons, onIconSelect, renderIcon, showCategories = false }: VirtualizedIconGridProps) {
 | 
			
		||||
  const t = useTranslations("iconPicker");
 | 
			
		||||
  const t = useTranslations();
 | 
			
		||||
  const [visibleCount, setVisibleCount] = useState(ICONS_PER_BATCH);
 | 
			
		||||
  const [isLoading, setIsLoading] = useState(false);
 | 
			
		||||
  const scrollRef = useRef<HTMLDivElement>(null);
 | 
			
		||||
  const sentinelRef = useRef<HTMLDivElement>(null);
 | 
			
		||||
 | 
			
		||||
  // Intersection Observer for infinite scroll
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const sentinel = sentinelRef.current;
 | 
			
		||||
    if (!sentinel) return;
 | 
			
		||||
@@ -102,7 +97,6 @@ function VirtualizedIconGrid({ icons, onIconSelect, renderIcon, showCategories =
 | 
			
		||||
        const entry = entries[0];
 | 
			
		||||
        if (entry.isIntersecting && visibleCount < icons.length && !isLoading) {
 | 
			
		||||
          setIsLoading(true);
 | 
			
		||||
          // Simulate async loading with setTimeout for better UX
 | 
			
		||||
          setTimeout(() => {
 | 
			
		||||
            setVisibleCount((prev) => Math.min(prev + ICONS_PER_BATCH, icons.length));
 | 
			
		||||
            setIsLoading(false);
 | 
			
		||||
@@ -123,19 +117,16 @@ function VirtualizedIconGrid({ icons, onIconSelect, renderIcon, showCategories =
 | 
			
		||||
    };
 | 
			
		||||
  }, [visibleCount, icons.length, isLoading]);
 | 
			
		||||
 | 
			
		||||
  // Reset visible count when icons change
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setVisibleCount(ICONS_PER_BATCH);
 | 
			
		||||
  }, [icons]);
 | 
			
		||||
 | 
			
		||||
  // Reset visible count when switching between search and category view
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setVisibleCount(ICONS_PER_BATCH);
 | 
			
		||||
  }, [showCategories]);
 | 
			
		||||
 | 
			
		||||
  const visibleIcons = useMemo(() => icons.slice(0, visibleCount), [icons, visibleCount]);
 | 
			
		||||
 | 
			
		||||
  // Group icons by category for category view - always compute, use conditionally
 | 
			
		||||
  const iconsByCategory = useMemo(() => {
 | 
			
		||||
    if (!showCategories) return [];
 | 
			
		||||
    const grouped = new Map<string, IconData[]>();
 | 
			
		||||
@@ -155,7 +146,7 @@ function VirtualizedIconGrid({ icons, onIconSelect, renderIcon, showCategories =
 | 
			
		||||
          {iconsByCategory.map(([category, categoryIcons]) => (
 | 
			
		||||
            <div key={category}>
 | 
			
		||||
              <Badge variant="secondary" className="text-xs mb-3">
 | 
			
		||||
                {t("categoryBadge", {
 | 
			
		||||
                {t("iconPicker.categoryBadge", {
 | 
			
		||||
                  category,
 | 
			
		||||
                  count: icons.filter((icon) => icon.category === category).length,
 | 
			
		||||
                })}
 | 
			
		||||
@@ -177,10 +168,10 @@ function VirtualizedIconGrid({ icons, onIconSelect, renderIcon, showCategories =
 | 
			
		||||
 | 
			
		||||
          {/* Loading indicator and sentinel */}
 | 
			
		||||
          <div ref={sentinelRef} className="flex justify-center py-4">
 | 
			
		||||
            {isLoading && <div className="text-sm text-muted-foreground">{t("loadingMore")}</div>}
 | 
			
		||||
            {isLoading && <div className="text-sm text-muted-foreground">{t("iconPicker.loadingMore")}</div>}
 | 
			
		||||
            {visibleCount >= icons.length && icons.length > 0 && (
 | 
			
		||||
              <div className="text-sm text-muted-foreground">
 | 
			
		||||
                {t("allIconsLoaded", { count: icons.length.toLocaleString() })}
 | 
			
		||||
                {t("iconPicker.allIconsLoaded", { count: icons.length.toLocaleString() })}
 | 
			
		||||
              </div>
 | 
			
		||||
            )}
 | 
			
		||||
          </div>
 | 
			
		||||
@@ -189,7 +180,6 @@ function VirtualizedIconGrid({ icons, onIconSelect, renderIcon, showCategories =
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Simple grid view for search results and specific tabs
 | 
			
		||||
  return (
 | 
			
		||||
    <div ref={scrollRef} className="max-h-[600px] overflow-y-auto overflow-x-hidden pr-2">
 | 
			
		||||
      <div className="grid grid-cols-8 sm:grid-cols-12 lg:grid-cols-16 xl:grid-cols-20 gap-2 sm:gap-3">
 | 
			
		||||
@@ -205,12 +195,11 @@ function VirtualizedIconGrid({ icons, onIconSelect, renderIcon, showCategories =
 | 
			
		||||
        ))}
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      {/* Loading indicator and sentinel */}
 | 
			
		||||
      <div ref={sentinelRef} className="flex justify-center py-4">
 | 
			
		||||
        {isLoading && <div className="text-sm text-muted-foreground">{t("loadingMore")}</div>}
 | 
			
		||||
        {isLoading && <div className="text-sm text-muted-foreground">{t("iconPicker.loadingMore")}</div>}
 | 
			
		||||
        {visibleCount >= icons.length && icons.length > 0 && (
 | 
			
		||||
          <div className="text-sm text-muted-foreground">
 | 
			
		||||
            {t("allIconsLoaded", { count: icons.length.toLocaleString() })}
 | 
			
		||||
            {t("iconPicker.allIconsLoaded", { count: icons.length.toLocaleString() })}
 | 
			
		||||
          </div>
 | 
			
		||||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
@@ -218,7 +207,6 @@ function VirtualizedIconGrid({ icons, onIconSelect, renderIcon, showCategories =
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Popular icons for quick access
 | 
			
		||||
const POPULAR_ICONS = [
 | 
			
		||||
  "FaGoogle",
 | 
			
		||||
  "FaGithub",
 | 
			
		||||
@@ -252,7 +240,6 @@ const POPULAR_ICONS = [
 | 
			
		||||
  "TbKey",
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
// Auth provider specific icons
 | 
			
		||||
const AUTH_PROVIDER_ICONS = [
 | 
			
		||||
  "SiGoogle",
 | 
			
		||||
  "SiGithub",
 | 
			
		||||
@@ -277,14 +264,12 @@ const AUTH_PROVIDER_ICONS = [
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
export function IconPicker({ value, onChange, placeholder }: IconPickerProps) {
 | 
			
		||||
  const t = useTranslations("iconPicker");
 | 
			
		||||
  const t = useTranslations();
 | 
			
		||||
  const [open, setOpen] = useState(false);
 | 
			
		||||
  const [search, setSearch] = useState("");
 | 
			
		||||
 | 
			
		||||
  // Use translation for placeholder if not provided
 | 
			
		||||
  const displayPlaceholder = placeholder || t("placeholder");
 | 
			
		||||
  const displayPlaceholder = placeholder || t("iconPicker.placeholder");
 | 
			
		||||
 | 
			
		||||
  // Combine ALL icon libraries
 | 
			
		||||
  const allIcons = useMemo(() => {
 | 
			
		||||
    const iconSets = [
 | 
			
		||||
      { icons: AiIcons, prefix: "Ai", category: "Ant Design Icons" },
 | 
			
		||||
@@ -344,23 +329,19 @@ export function IconPicker({ value, onChange, placeholder }: IconPickerProps) {
 | 
			
		||||
    return icons;
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  // Filter icons based on search
 | 
			
		||||
  const filteredIcons = useMemo(() => {
 | 
			
		||||
    if (!search) return allIcons;
 | 
			
		||||
    return allIcons.filter((icon) => icon.name.toLowerCase().includes(search.toLowerCase()));
 | 
			
		||||
  }, [allIcons, search]);
 | 
			
		||||
 | 
			
		||||
  // Get popular icons
 | 
			
		||||
  const popularIcons = useMemo(() => {
 | 
			
		||||
    return POPULAR_ICONS.map((name) => allIcons.find((icon) => icon.name === name)).filter(Boolean) as IconData[];
 | 
			
		||||
  }, [allIcons]);
 | 
			
		||||
 | 
			
		||||
  // Get auth provider icons
 | 
			
		||||
  const authProviderIcons = useMemo(() => {
 | 
			
		||||
    return AUTH_PROVIDER_ICONS.map((name) => allIcons.find((icon) => icon.name === name)).filter(Boolean) as IconData[];
 | 
			
		||||
  }, [allIcons]);
 | 
			
		||||
 | 
			
		||||
  // Get current icon component
 | 
			
		||||
  const currentIcon = useMemo(() => {
 | 
			
		||||
    if (!value) return null;
 | 
			
		||||
    return allIcons.find((icon) => icon.name === value);
 | 
			
		||||
@@ -380,7 +361,6 @@ export function IconPicker({ value, onChange, placeholder }: IconPickerProps) {
 | 
			
		||||
    return <IconComponent className={className} size={size || 32} />;
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  // Get unique categories for display
 | 
			
		||||
  const categories = useMemo(() => {
 | 
			
		||||
    const uniqueCategories = Array.from(new Set(allIcons.map((icon) => icon.category)));
 | 
			
		||||
    return uniqueCategories.sort();
 | 
			
		||||
@@ -406,9 +386,9 @@ export function IconPicker({ value, onChange, placeholder }: IconPickerProps) {
 | 
			
		||||
      <DialogContent className="max-w-5xl xl:max-w-6xl max-h-[90vh] overflow-hidden">
 | 
			
		||||
        <div className="space-y-4 overflow-hidden">
 | 
			
		||||
          <div className="flex items-center justify-between">
 | 
			
		||||
            <DialogTitle>{t("title")}</DialogTitle>
 | 
			
		||||
            <DialogTitle>{t("iconPicker.title")}</DialogTitle>
 | 
			
		||||
            <div className="text-sm text-muted-foreground">
 | 
			
		||||
              {t("stats", {
 | 
			
		||||
              {t("iconPicker.stats", {
 | 
			
		||||
                iconCount: allIcons.length.toLocaleString(),
 | 
			
		||||
                libraryCount: categories.length,
 | 
			
		||||
              })}
 | 
			
		||||
@@ -419,7 +399,7 @@ export function IconPicker({ value, onChange, placeholder }: IconPickerProps) {
 | 
			
		||||
          <div className="relative">
 | 
			
		||||
            <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
 | 
			
		||||
            <Input
 | 
			
		||||
              placeholder={t("searchPlaceholder")}
 | 
			
		||||
              placeholder={t("iconPicker.searchPlaceholder")}
 | 
			
		||||
              value={search}
 | 
			
		||||
              onChange={(e) => setSearch(e.target.value)}
 | 
			
		||||
              className="pl-8"
 | 
			
		||||
@@ -438,9 +418,9 @@ export function IconPicker({ value, onChange, placeholder }: IconPickerProps) {
 | 
			
		||||
 | 
			
		||||
          <Tabs defaultValue="all" className="w-full overflow-hidden">
 | 
			
		||||
            <TabsList className="grid w-full grid-cols-3">
 | 
			
		||||
              <TabsTrigger value="all">{t("tabs.all")}</TabsTrigger>
 | 
			
		||||
              <TabsTrigger value="popular">{t("tabs.popular")}</TabsTrigger>
 | 
			
		||||
              <TabsTrigger value="auth">{t("tabs.auth")}</TabsTrigger>
 | 
			
		||||
              <TabsTrigger value="all">{t("iconPicker.tabs.all")}</TabsTrigger>
 | 
			
		||||
              <TabsTrigger value="popular">{t("iconPicker.tabs.popular")}</TabsTrigger>
 | 
			
		||||
              <TabsTrigger value="auth">{t("iconPicker.tabs.auth")}</TabsTrigger>
 | 
			
		||||
            </TabsList>
 | 
			
		||||
 | 
			
		||||
            {/* All Icons */}
 | 
			
		||||
@@ -490,7 +470,7 @@ export function IconPicker({ value, onChange, placeholder }: IconPickerProps) {
 | 
			
		||||
          {search && filteredIcons.length === 0 && (
 | 
			
		||||
            <div className="text-center py-8 text-muted-foreground">
 | 
			
		||||
              <Search className="mx-auto h-12 w-12 opacity-50 mb-2" />
 | 
			
		||||
              <p>{t("noIconsFound", { search })}</p>
 | 
			
		||||
              <p>{t("iconPicker.noIconsFound", { search })}</p>
 | 
			
		||||
            </div>
 | 
			
		||||
          )}
 | 
			
		||||
        </div>
 | 
			
		||||
@@ -499,7 +479,6 @@ export function IconPicker({ value, onChange, placeholder }: IconPickerProps) {
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Utility function to render an icon by name
 | 
			
		||||
export function renderIconByName(iconName: string, className = "w-5 h-5") {
 | 
			
		||||
  const iconSets = [
 | 
			
		||||
    AiIcons,
 | 
			
		||||
@@ -542,6 +521,5 @@ export function renderIconByName(iconName: string, className = "w-5 h-5") {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Fallback to a default icon
 | 
			
		||||
  return React.createElement(FaIcons.FaCog, { className });
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user