facehash avatars

This commit is contained in:
yohlo
2026-02-09 14:31:55 -06:00
parent f069ba3827
commit 937758bd49
8 changed files with 104 additions and 15 deletions

View File

@@ -30,6 +30,7 @@
"browser-image-compression": "^2.0.2",
"dotenv": "^17.2.2",
"embla-carousel-react": "^8.6.0",
"facehash": "^0.0.7",
"framer-motion": "^12.23.12",
"ioredis": "^5.7.0",
"pg": "^8.16.3",
@@ -710,6 +711,8 @@
"exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="],
"facehash": ["facehash@0.0.7", "", { "peerDependencies": { "@types/react": "", "next": ">=15", "react": ">=18 <20", "react-dom": ">=18 <20" }, "optionalPeers": ["@types/react", "next"] }, "sha512-P4fw6z5DIGMbjtqEaOw7fYvYpQetSOSJOfqy3xuET7cDUI6f9CKlSX0UZIYNrtsPpCoz3LoPP5E8bNbpZBP30A=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-equals": ["fast-equals@5.4.0", "", {}, "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw=="],

View File

@@ -35,6 +35,7 @@
"browser-image-compression": "^2.0.2",
"dotenv": "^17.2.2",
"embla-carousel-react": "^8.6.0",
"facehash": "^0.0.7",
"framer-motion": "^12.23.12",
"ioredis": "^5.7.0",
"pg": "^8.16.3",

View File

@@ -0,0 +1,85 @@
import { Paper, useMantineTheme } from "@mantine/core";
import { Facehash } from "facehash";
interface PlayerAvatarProps {
name?: string;
size?: number;
disableFullscreen?: boolean;
style?: React.CSSProperties;
}
const PlayerAvatar = ({
name = "",
size = 35,
disableFullscreen = false,
style,
}: PlayerAvatarProps) => {
const theme = useMantineTheme();
const getFacehashSize = (size: number): 32 | 48 | 64 | 80 => {
if (size <= 40) return 32;
if (size <= 56) return 48;
if (size <= 72) return 64;
return 80;
};
const facehashSize = getFacehashSize(size);
const colors = [
"hsla(314, 100%, 80%, 1)",
"hsla(58, 93%, 72%, 1)",
"hsla(218, 92%, 72%, 1)",
"hsla(19, 99%, 44%, 1)",
"hsla(156, 86%, 40%, 1)",
"hsla(314, 100%, 85%, 1)",
"hsla(58, 92%, 79%, 1)",
"hsla(218, 91%, 78%, 1)",
"hsla(19, 99%, 50%, 1)",
"hsla(156, 86%, 64%, 1)",
];
return (
<Paper
p={size / 20}
radius="100%"
withBorder
style={{
cursor: !disableFullscreen ? 'pointer' : 'default',
transition: 'transform 0.15s ease',
...style,
}}
onMouseEnter={(e) => {
if (!disableFullscreen) {
e.currentTarget.style.transform = 'scale(1.02)';
}
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = 'scale(1)';
}}
>
<div
style={{
width: size,
height: size,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
borderRadius: '100%',
}}
>
<Facehash
name={name}
size={size}
variant="solid"
colors={colors}
intensity3d="dramatic"
enableBlink
style={{ borderRadius: '100%', overflow: 'hidden', color: 'black' }}
/>
</div>
</Paper>
);
};
export default PlayerAvatar;

View File

@@ -5,7 +5,7 @@ import { useAllPlayerStats } from "../queries";
import { useSheet } from "@/hooks/use-sheet";
import Sheet from "@/components/sheet/sheet";
import PlayerHeadToHeadSheet from "./player-head-to-head-sheet";
import Avatar from "@/components/avatar";
import PlayerAvatar from "@/components/player-avatar";
const LeagueHeadToHead = () => {
const [player1Id, setPlayer1Id] = useState<string | null>(null);
@@ -89,7 +89,7 @@ const LeagueHeadToHead = () => {
{player1Id ? (
<>
<Stack gap={4} align="center" style={{ flex: 1 }}>
<Avatar name={player1Name} size={36} />
<PlayerAvatar name={player1Name} size={36} disableFullscreen />
<Text size="xs" fw={600} ta="center" lineClamp={1}>
{player1Name}
</Text>
@@ -110,7 +110,7 @@ const LeagueHeadToHead = () => {
</>
) : (
<Stack gap={4} align="center">
<Avatar size={36} />
<PlayerAvatar size={36} disableFullscreen />
<Text size="xs" c="dimmed" fw={500}>
Player 1
</Text>
@@ -145,7 +145,7 @@ const LeagueHeadToHead = () => {
{player2Id ? (
<>
<Stack gap={4} align="center" style={{ flex: 1 }}>
<Avatar name={player2Name} size={36} />
<PlayerAvatar name={player2Name} size={36} disableFullscreen />
<Text size="xs" fw={600} ta="center" lineClamp={1}>
{player2Name}
</Text>
@@ -166,7 +166,7 @@ const LeagueHeadToHead = () => {
</>
) : (
<Stack gap={4} align="center">
<Avatar size={36} />
<PlayerAvatar size={36} disableFullscreen />
<Text size="xs" c="dimmed" fw={500}>
Player 2
</Text>
@@ -241,7 +241,7 @@ const LeagueHeadToHead = () => {
},
}}
>
<Avatar name={player.player_name} size={44} />
<PlayerAvatar name={player.player_name} size={44} disableFullscreen />
<Box style={{ flex: 1, minWidth: 0 }}>
<Text size="sm" fw={600} truncate>
{player.player_name}

View File

@@ -1,6 +1,6 @@
import { List, ListItem, Skeleton, Text } from "@mantine/core";
import { useNavigate } from "@tanstack/react-router";
import Avatar from "@/components/avatar";
import PlayerAvatar from "@/components/player-avatar";
import { Player } from "@/features/players/types";
import { useCallback } from "react";
@@ -29,7 +29,7 @@ const PlayerList = ({ players, loading = false }: PlayerListProps) => {
{players?.map((player) => (
<ListItem key={player.id}
py='xs'
icon={<Avatar size={40} name={`${player.first_name} ${player.last_name}`} />}
icon={<PlayerAvatar size={40} name={`${player.first_name} ${player.last_name}`} disableFullscreen />}
style={{ cursor: 'pointer' }}
onClick={() => handleClick(player.id)}
>

View File

@@ -22,7 +22,7 @@ import {
InfoIcon,
} from "@phosphor-icons/react";
import { PlayerStats } from "../types";
import Avatar from "@/components/avatar";
import PlayerAvatar from "@/components/player-avatar";
import { useNavigate } from "@tanstack/react-router";
import { useAllPlayerStats } from "../queries";
@@ -93,7 +93,7 @@ const PlayerListItem = memo(({ stat, onPlayerClick, mmr, onRegisterViewport, onU
}}
>
<Group p={0} gap="sm" align="center" w="100%" wrap="nowrap" style={{ overflow: 'hidden' }}>
<Avatar name={stat.player_name} size={40} style={{ flexShrink: 0 }} />
<PlayerAvatar name={stat.player_name} size={40} style={{ flexShrink: 0 }} disableFullscreen />
<Stack gap={2} style={{ flexGrow: 1, overflow: 'hidden', minWidth: 0 }}>
<Group gap='xs'>
<Text size="sm" fw={600}>

View File

@@ -4,7 +4,7 @@ import { Flex, Title, ActionIcon, Stack, Button, Box } from "@mantine/core";
import { PencilIcon, FootballHelmetIcon } from "@phosphor-icons/react";
import { useMemo } from "react";
import NameUpdateForm from "./name-form";
import Avatar from "@/components/avatar";
import PlayerAvatar from "@/components/player-avatar";
import { useSheet } from "@/hooks/use-sheet";
import { Player } from "../../types";
import PlayerHeadToHeadSheet from "../player-head-to-head-sheet";
@@ -41,7 +41,7 @@ const Header = ({ player }: HeaderProps) => {
<>
<Stack gap="sm" align="center" pt="md">
<Flex h="15dvh" px='xl' w='100%' align='self-end' gap='md'>
<Avatar name={name} size={100} />
<PlayerAvatar name={name} size={100} />
<Flex align='center' justify='center' gap={4} pb={20} w='100%'>
<Title ta='center' style={{ fontSize, lineHeight: 1.2 }}>{name}</Title>
<ActionIcon display={owner ? 'block' : 'none'} radius='xl' variant='subtle' onClick={nameSheet.open}>

View File

@@ -3,13 +3,13 @@ import { defineConfig } from 'vite'
import tsConfigPaths from 'vite-tsconfig-paths'
import react from '@vitejs/plugin-react';
export default defineConfig({
export default defineConfig(({ mode }) => ({
server: {
port: 3000,
allowedHosts: ["dev.flexxon.app", "flexxon.app"]
},
ssr: {
noExternal: true,
noExternal: mode === 'production' ? true : ['facehash'],
},
plugins: [
tsConfigPaths({
@@ -20,4 +20,4 @@ export default defineConfig({
}),
react()
]
})
}))