diff --git a/.gitignore b/.gitignore index d531bab01..ab2fe76c6 100644 --- a/.gitignore +++ b/.gitignore @@ -43,4 +43,7 @@ yarn-error.log* *.pem -.db \ No newline at end of file +.db + +# Development environment +.devcontainer \ No newline at end of file diff --git a/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx b/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx index 47219620f..ad66aa0fa 100644 --- a/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx +++ b/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx @@ -1,7 +1,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; -import { Loader2, User } from "lucide-react"; +import { Loader2, Palette, User } from "lucide-react"; import { useTranslation } from "next-i18next"; -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; @@ -27,6 +27,7 @@ import { import { Input } from "@/components/ui/input"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Switch } from "@/components/ui/switch"; +import { getAvatarType, isSolidColorAvatar } from "@/lib/avatar-utils"; import { generateSHA256Hash, getFallbackAvatarInitials } from "@/lib/utils"; import { api } from "@/utils/api"; import { Configure2FA } from "./configure-2fa"; @@ -74,6 +75,7 @@ export const ProfileForm = () => { } = api.user.update.useMutation(); const { t } = useTranslation("settings"); const [gravatarHash, setGravatarHash] = useState(null); + const colorInputRef = useRef(null); const availableAvatars = useMemo(() => { if (gravatarHash === null) return randomImages; @@ -274,16 +276,8 @@ export const ProfileForm = () => { onValueChange={(e) => { field.onChange(e); }} - defaultValue={ - field.value?.startsWith("data:") - ? "upload" - : field.value - } - value={ - field.value?.startsWith("data:") - ? "upload" - : field.value - } + defaultValue={getAvatarType(field.value)} + value={getAvatarType(field.value)} className="flex flex-row flex-wrap gap-2 max-xl:justify-center" > @@ -370,6 +364,40 @@ export const ProfileForm = () => { /> + + + + + +
+ colorInputRef.current?.click() + } + > + {!isSolidColorAvatar(field.value) && ( + + )} +
+ +
+
{availableAvatars.map((image) => ( diff --git a/apps/dokploy/components/ui/avatar.tsx b/apps/dokploy/components/ui/avatar.tsx index 13b276cd3..f09c31d53 100644 --- a/apps/dokploy/components/ui/avatar.tsx +++ b/apps/dokploy/components/ui/avatar.tsx @@ -1,6 +1,6 @@ import * as AvatarPrimitive from "@radix-ui/react-avatar"; import * as React from "react"; - +import { isSolidColorAvatar } from "@/lib/avatar-utils"; import { cn } from "@/lib/utils"; const Avatar = React.forwardRef< @@ -20,14 +20,33 @@ Avatar.displayName = AvatarPrimitive.Root.displayName; const AvatarImage = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); + React.ComponentPropsWithoutRef & { + src?: string | null; + } +>(({ className, src, ...props }, ref) => { + if (isSolidColorAvatar(src)) { + return ( +
+ ); + } + + return ( + + ); +}); AvatarImage.displayName = AvatarPrimitive.Image.displayName; const AvatarFallback = React.forwardRef< diff --git a/apps/dokploy/lib/avatar-utils.ts b/apps/dokploy/lib/avatar-utils.ts new file mode 100644 index 000000000..c21c0e681 --- /dev/null +++ b/apps/dokploy/lib/avatar-utils.ts @@ -0,0 +1,30 @@ +/** + * Checks if the given avatar value represents a solid color in hexadecimal format. + * + * @param value Avatar value to check. + * + * @return True if the avatar is a solid color, false otherwise. + */ +export function isSolidColorAvatar(value?: string | null) { + return ( + (value?.startsWith("#") && /^#[0-9A-Fa-f]{6}$/.test(value)) || + value?.startsWith("color:") || + false + ); +} + +/** + * Gets the avatar type for form selection (RadioGroup value). + * + * @param value Avatar value. + * + * @return "upload" for base64 images, "color" for solid colors, or the original value for other types. + */ +export function getAvatarType(value?: string | null) { + if (!value) return ""; + + if (value.startsWith("data:")) return "upload"; + if (isSolidColorAvatar(value)) return "color"; + + return value; +}