development #4
3
bun.lock
3
bun.lock
@@ -30,6 +30,7 @@
|
|||||||
"browser-image-compression": "^2.0.2",
|
"browser-image-compression": "^2.0.2",
|
||||||
"dotenv": "^17.2.2",
|
"dotenv": "^17.2.2",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
|
"facehash": "^0.0.7",
|
||||||
"framer-motion": "^12.23.12",
|
"framer-motion": "^12.23.12",
|
||||||
"ioredis": "^5.7.0",
|
"ioredis": "^5.7.0",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
@@ -710,6 +711,8 @@
|
|||||||
|
|
||||||
"exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="],
|
"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-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=="],
|
"fast-equals": ["fast-equals@5.4.0", "", {}, "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw=="],
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
"browser-image-compression": "^2.0.2",
|
"browser-image-compression": "^2.0.2",
|
||||||
"dotenv": "^17.2.2",
|
"dotenv": "^17.2.2",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
|
"facehash": "^0.0.7",
|
||||||
"framer-motion": "^12.23.12",
|
"framer-motion": "^12.23.12",
|
||||||
"ioredis": "^5.7.0",
|
"ioredis": "^5.7.0",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
|
|||||||
85
src/components/player-avatar.tsx
Normal file
85
src/components/player-avatar.tsx
Normal 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;
|
||||||
@@ -5,7 +5,7 @@ import { useAllPlayerStats } from "../queries";
|
|||||||
import { useSheet } from "@/hooks/use-sheet";
|
import { useSheet } from "@/hooks/use-sheet";
|
||||||
import Sheet from "@/components/sheet/sheet";
|
import Sheet from "@/components/sheet/sheet";
|
||||||
import PlayerHeadToHeadSheet from "./player-head-to-head-sheet";
|
import PlayerHeadToHeadSheet from "./player-head-to-head-sheet";
|
||||||
import Avatar from "@/components/avatar";
|
import PlayerAvatar from "@/components/player-avatar";
|
||||||
|
|
||||||
const LeagueHeadToHead = () => {
|
const LeagueHeadToHead = () => {
|
||||||
const [player1Id, setPlayer1Id] = useState<string | null>(null);
|
const [player1Id, setPlayer1Id] = useState<string | null>(null);
|
||||||
@@ -89,7 +89,7 @@ const LeagueHeadToHead = () => {
|
|||||||
{player1Id ? (
|
{player1Id ? (
|
||||||
<>
|
<>
|
||||||
<Stack gap={4} align="center" style={{ flex: 1 }}>
|
<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}>
|
<Text size="xs" fw={600} ta="center" lineClamp={1}>
|
||||||
{player1Name}
|
{player1Name}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -110,7 +110,7 @@ const LeagueHeadToHead = () => {
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Stack gap={4} align="center">
|
<Stack gap={4} align="center">
|
||||||
<Avatar size={36} />
|
<PlayerAvatar size={36} disableFullscreen />
|
||||||
<Text size="xs" c="dimmed" fw={500}>
|
<Text size="xs" c="dimmed" fw={500}>
|
||||||
Player 1
|
Player 1
|
||||||
</Text>
|
</Text>
|
||||||
@@ -145,7 +145,7 @@ const LeagueHeadToHead = () => {
|
|||||||
{player2Id ? (
|
{player2Id ? (
|
||||||
<>
|
<>
|
||||||
<Stack gap={4} align="center" style={{ flex: 1 }}>
|
<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}>
|
<Text size="xs" fw={600} ta="center" lineClamp={1}>
|
||||||
{player2Name}
|
{player2Name}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -166,7 +166,7 @@ const LeagueHeadToHead = () => {
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Stack gap={4} align="center">
|
<Stack gap={4} align="center">
|
||||||
<Avatar size={36} />
|
<PlayerAvatar size={36} disableFullscreen />
|
||||||
<Text size="xs" c="dimmed" fw={500}>
|
<Text size="xs" c="dimmed" fw={500}>
|
||||||
Player 2
|
Player 2
|
||||||
</Text>
|
</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 }}>
|
<Box style={{ flex: 1, minWidth: 0 }}>
|
||||||
<Text size="sm" fw={600} truncate>
|
<Text size="sm" fw={600} truncate>
|
||||||
{player.player_name}
|
{player.player_name}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { List, ListItem, Skeleton, Text } from "@mantine/core";
|
import { List, ListItem, Skeleton, Text } from "@mantine/core";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
import Avatar from "@/components/avatar";
|
import PlayerAvatar from "@/components/player-avatar";
|
||||||
import { Player } from "@/features/players/types";
|
import { Player } from "@/features/players/types";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ const PlayerList = ({ players, loading = false }: PlayerListProps) => {
|
|||||||
{players?.map((player) => (
|
{players?.map((player) => (
|
||||||
<ListItem key={player.id}
|
<ListItem key={player.id}
|
||||||
py='xs'
|
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' }}
|
style={{ cursor: 'pointer' }}
|
||||||
onClick={() => handleClick(player.id)}
|
onClick={() => handleClick(player.id)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
InfoIcon,
|
InfoIcon,
|
||||||
} from "@phosphor-icons/react";
|
} from "@phosphor-icons/react";
|
||||||
import { PlayerStats } from "../types";
|
import { PlayerStats } from "../types";
|
||||||
import Avatar from "@/components/avatar";
|
import PlayerAvatar from "@/components/player-avatar";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
import { useAllPlayerStats } from "../queries";
|
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' }}>
|
<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 }}>
|
<Stack gap={2} style={{ flexGrow: 1, overflow: 'hidden', minWidth: 0 }}>
|
||||||
<Group gap='xs'>
|
<Group gap='xs'>
|
||||||
<Text size="sm" fw={600}>
|
<Text size="sm" fw={600}>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Flex, Title, ActionIcon, Stack, Button, Box } from "@mantine/core";
|
|||||||
import { PencilIcon, FootballHelmetIcon } from "@phosphor-icons/react";
|
import { PencilIcon, FootballHelmetIcon } from "@phosphor-icons/react";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import NameUpdateForm from "./name-form";
|
import NameUpdateForm from "./name-form";
|
||||||
import Avatar from "@/components/avatar";
|
import PlayerAvatar from "@/components/player-avatar";
|
||||||
import { useSheet } from "@/hooks/use-sheet";
|
import { useSheet } from "@/hooks/use-sheet";
|
||||||
import { Player } from "../../types";
|
import { Player } from "../../types";
|
||||||
import PlayerHeadToHeadSheet from "../player-head-to-head-sheet";
|
import PlayerHeadToHeadSheet from "../player-head-to-head-sheet";
|
||||||
@@ -41,7 +41,7 @@ const Header = ({ player }: HeaderProps) => {
|
|||||||
<>
|
<>
|
||||||
<Stack gap="sm" align="center" pt="md">
|
<Stack gap="sm" align="center" pt="md">
|
||||||
<Flex h="15dvh" px='xl' w='100%' align='self-end' gap='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%'>
|
<Flex align='center' justify='center' gap={4} pb={20} w='100%'>
|
||||||
<Title ta='center' style={{ fontSize, lineHeight: 1.2 }}>{name}</Title>
|
<Title ta='center' style={{ fontSize, lineHeight: 1.2 }}>{name}</Title>
|
||||||
<ActionIcon display={owner ? 'block' : 'none'} radius='xl' variant='subtle' onClick={nameSheet.open}>
|
<ActionIcon display={owner ? 'block' : 'none'} radius='xl' variant='subtle' onClick={nameSheet.open}>
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ import { defineConfig } from 'vite'
|
|||||||
import tsConfigPaths from 'vite-tsconfig-paths'
|
import tsConfigPaths from 'vite-tsconfig-paths'
|
||||||
import react from '@vitejs/plugin-react';
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig(({ mode }) => ({
|
||||||
server: {
|
server: {
|
||||||
port: 3000,
|
port: 3000,
|
||||||
allowedHosts: ["dev.flexxon.app", "flexxon.app"]
|
allowedHosts: ["dev.flexxon.app", "flexxon.app"]
|
||||||
},
|
},
|
||||||
ssr: {
|
ssr: {
|
||||||
noExternal: true,
|
noExternal: mode === 'production' ? true : ['facehash'],
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
tsConfigPaths({
|
tsConfigPaths({
|
||||||
@@ -20,4 +20,4 @@ export default defineConfig({
|
|||||||
}),
|
}),
|
||||||
react()
|
react()
|
||||||
]
|
]
|
||||||
})
|
}))
|
||||||
|
|||||||
Reference in New Issue
Block a user