Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
"@stellar/freighter-api": "^1.7.1",
"@walletconnect/sign-client": "^2.23.9",
"@walletconnect/types": "^2.23.8",
"boring-avatars": "^2.0.4",
"canvas-confetti": "^1.9.4",
"confetti": "^3.0.4",
"event-source-polyfill": "^1.0.31",
Expand Down
23 changes: 7 additions & 16 deletions frontend/src/components/MerchantProfileCard.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import Avatar from "boring-avatars";
import { Avatar } from "@/components/ui/Avatar";
import Link from "next/link";
import {
useMerchantMetadata,
Expand All @@ -23,7 +23,8 @@ export default function MerchantProfileCard() {
// If no merchant data, show anonymous profile
const displayName = merchant?.business_name || merchant?.email || "Merchant";
const email = merchant?.email || "";
const avatarSeed = email || "anonymous";
const avatarName = merchant?.business_name || merchant?.email || "Merchant";
const logoUrl = merchant?.logo_url || null;

const handleLogout = () => {
logout();
Expand All @@ -41,10 +42,8 @@ export default function MerchantProfileCard() {
>
<Avatar
size={36}
name={avatarSeed}
variant="beam"
colors={["#5ef2c0", "#b8ffe2", "#0f1a2b", "#0b0c10", "#ffffff"]}
square={false}
name={avatarName}
src={logoUrl}
/>
<div className="hidden text-left sm:block">
<p className="truncate text-sm font-medium text-white">
Expand Down Expand Up @@ -82,16 +81,8 @@ export default function MerchantProfileCard() {
<div className="mb-4 flex items-center gap-3 border-b border-white/10 pb-4">
<Avatar
size={48}
name={avatarSeed}
variant="beam"
colors={[
"#5ef2c0",
"#b8ffe2",
"#0f1a2b",
"#0b0c10",
"#ffffff",
]}
square={false}
name={avatarName}
src={logoUrl}
/>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-semibold text-white">
Expand Down
100 changes: 100 additions & 0 deletions frontend/src/components/ui/Avatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"use client";

import React from "react";
import Image from "next/image";

interface AvatarProps {
name: string;
src?: string | null;
size?: number;
className?: string;
}

export function Avatar({ name, src, size = 40, className = "" }: AvatarProps) {
const getInitials = (str: string) => {
const parts = str.split(" ").filter(Boolean);
if (parts.length === 0) return "?";
if (parts.length === 1) {
const s = parts[0];
if (s.length >= 2) return s.substring(0, 2).toUpperCase();
return s.toUpperCase();
}
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
};

const getColor = (str: string) => {
const colors = [
{ bg: "hsl(210, 80%, 60%)", text: "hsl(210, 80%, 98%)" },
{ bg: "hsl(160, 70%, 45%)", text: "hsl(160, 70%, 98%)" },
{ bg: "hsl(260, 70%, 65%)", text: "hsl(260, 70%, 98%)" },
{ bg: "hsl(340, 75%, 60%)", text: "hsl(340, 75%, 98%)" },
{ bg: "hsl(200, 80%, 50%)", text: "hsl(200, 80%, 98%)" },
{ bg: "hsl(280, 65%, 55%)", text: "hsl(280, 65%, 98%)" },
{ bg: "hsl(180, 75%, 40%)", text: "hsl(180, 75%, 98%)" },
{ bg: "hsl(230, 70%, 60%)", text: "hsl(230, 70%, 98%)" },
];

let hash = 0;
const s = str || "default";
for (let i = 0; i < s.length; i++) {
hash = s.charCodeAt(i) + ((hash << 5) - hash);
}
const index = Math.abs(hash) % colors.length;
return colors[index];
};

const initials = getInitials(name);
const color = getColor(name);

const containerStyle: React.CSSProperties = {
width: `${size}px`,
height: `${size}px`,
minWidth: `${size}px`,
minHeight: `${size}px`,
position: "relative",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "9999px",
overflow: "hidden",
flexShrink: 0,
};

if (src) {
return (
<div
className={`bg-slate-800 border border-white/10 ${className}`}
style={containerStyle}
>
<Image
src={src}
alt={name}
fill
sizes={`${size}px`}
className="object-cover"
/>
</div>
);
}

return (
<div
className={`font-semibold tracking-tight shadow-sm relative group select-none ${className}`}
style={{
...containerStyle,
backgroundColor: color.bg,
color: color.text,
fontSize: `${size / 2.6}px`,
textShadow: "0 1px 2px rgba(0,0,0,0.1)",
}}
aria-label={name}
>
<span className="relative z-10 leading-none">{initials}</span>

<div className="absolute inset-0 bg-gradient-to-br from-white/30 to-transparent opacity-40 pointer-events-none" />
<div className="absolute inset-0 rounded-full border border-white/20 pointer-events-none" />

<div className="absolute inset-0 bg-white/0 transition-colors group-hover:bg-white/10 pointer-events-none" />
</div>
);
}
38 changes: 37 additions & 1 deletion frontend/src/lib/framer-motion-shim.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,45 @@ type MotionProps = {
[key: string]: unknown;
};

const MOTION_PROPS = [
"initial",
"animate",
"exit",
"transition",
"viewport",
"whileInView",
"whileHover",
"whileTap",
"whileFocus",
"whileDrag",
"variants",
"layout",
"layoutId",
"onAnimationStart",
"onAnimationComplete",
"onUpdate",
"custom",
"drag",
"dragConstraints",
"dragElastic",
"dragMomentum",
"dragListener",
"dragControls",
"onDragStart",
"onDragEnd",
"onDrag",
"onDirectionLock",
"onDragTransitionEnd",
];

function createMotionComponent(tag: string) {
return function MotionShim({ children, ...props }: MotionProps) {
return createElement(tag, props, children);
const cleanProps = { ...props };
MOTION_PROPS.forEach((prop) => {
delete cleanProps[prop];
});

return createElement(tag, cleanProps, children);
};
}

Expand Down
1 change: 1 addition & 0 deletions frontend/src/lib/merchant-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export interface MerchantMetadata {
notification_email: string;
api_key: string;
webhook_secret: string;
logo_url?: string | null;
branding_config?: {
primary_color?: string;
secondary_color?: string;
Expand Down
Loading