bug fixes, new fonts, etc

This commit is contained in:
yohlo
2025-10-02 14:49:29 -05:00
parent 2dfb7c63d3
commit 8579ec36ca
16 changed files with 111 additions and 83 deletions

View File

@@ -63,7 +63,17 @@ export const Route = createRootRouteWithContext<{
{ rel: 'stylesheet', href: mantineCssUrl }, { rel: 'stylesheet', href: mantineCssUrl },
{ rel: 'stylesheet', href: mantineCarouselCssUrl }, { rel: 'stylesheet', href: mantineCarouselCssUrl },
{ rel: 'stylesheet', href: mantineDatesCssUrl }, { rel: 'stylesheet', href: mantineDatesCssUrl },
{ rel: 'stylesheet', href: mantineTiptapCssUrl } { rel: 'stylesheet', href: mantineTiptapCssUrl },
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
{
rel: "preconnect",
href: "https://fonts.gstatic.com",
crossOrigin: "anonymous",
},
{
rel: "stylesheet",
href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=League+Spartan:wght@100..900&display=swap",
}
], ],
}), }),
errorComponent: (props) => { errorComponent: (props) => {

View File

@@ -16,6 +16,7 @@ export interface TypeaheadProps<T> {
debounceMs?: number; debounceMs?: number;
disabled?: boolean; disabled?: boolean;
initialValue?: string; initialValue?: string;
maxHeight?: number | string;
} }
const Typeahead = <T,>({ const Typeahead = <T,>({
@@ -26,7 +27,8 @@ const Typeahead = <T,>({
placeholder = "Search...", placeholder = "Search...",
debounceMs = 300, debounceMs = 300,
disabled = false, disabled = false,
initialValue = "" initialValue = "",
maxHeight = 200,
}: TypeaheadProps<T>) => { }: TypeaheadProps<T>) => {
const [searchQuery, setSearchQuery] = useState(initialValue); const [searchQuery, setSearchQuery] = useState(initialValue);
const [searchResults, setSearchResults] = useState<TypeaheadOption<T>[]>([]); const [searchResults, setSearchResults] = useState<TypeaheadOption<T>[]>([]);
@@ -36,13 +38,7 @@ const Typeahead = <T,>({
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const debouncedSearch = useDebouncedCallback(async (query: string) => { const performSearch = async (query: string) => {
if (!query.trim()) {
setSearchResults([]);
setIsOpen(false);
return;
}
setIsLoading(true); setIsLoading(true);
try { try {
const results = await searchFn(query); const results = await searchFn(query);
@@ -56,7 +52,9 @@ const Typeahead = <T,>({
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
}, debounceMs); };
const debouncedSearch = useDebouncedCallback(performSearch, debounceMs);
const handleSearchChange = (value: string) => { const handleSearchChange = (value: string) => {
setSearchQuery(value); setSearchQuery(value);
@@ -114,8 +112,12 @@ const Typeahead = <T,>({
value={searchQuery} value={searchQuery}
onChange={(event) => handleSearchChange(event.currentTarget.value)} onChange={(event) => handleSearchChange(event.currentTarget.value)}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
onFocus={() => { onFocus={async () => {
if (searchResults.length > 0) setIsOpen(true); if (searchResults.length > 0) {
setIsOpen(true);
return;
}
await performSearch(searchQuery);
}} }}
placeholder={placeholder} placeholder={placeholder}
rightSection={isLoading ? <Loader size="xs" /> : null} rightSection={isLoading ? <Loader size="xs" /> : null}
@@ -133,7 +135,7 @@ const Typeahead = <T,>({
left: 0, left: 0,
right: 0, right: 0,
zIndex: 9999, zIndex: 9999,
maxHeight: '160px', maxHeight,
overflowY: 'auto', overflowY: 'auto',
WebkitOverflowScrolling: 'touch', WebkitOverflowScrolling: 'touch',
touchAction: 'pan-y', touchAction: 'pan-y',

View File

@@ -1,5 +1,5 @@
import { useState } from "react"; import { useState } from "react";
import { Box, Card, Text, Select, Button, Group, Stack } from "@mantine/core"; import { Box, Card, Text, Select, Button, Group, Stack, Badge, Divider } from "@mantine/core";
import { awardManualBadge } from "@/features/badges/server"; import { awardManualBadge } from "@/features/badges/server";
import { useAllBadges } from "@/features/badges/queries"; import { useAllBadges } from "@/features/badges/queries";
import toast from "@/lib/sonner"; import toast from "@/lib/sonner";
@@ -27,10 +27,14 @@ const AwardBadges = () => {
}, },
}); });
toast.success("Badge awarded successfully"); const selectedPlayer = players.find((p) => p.id === selectedPlayerId);
const playerName = selectedPlayer
? `${selectedPlayer.first_name} ${selectedPlayer.last_name}`
: "Player";
toast.success(`Badge awarded to ${playerName}`);
setSelectedPlayerId(null); setSelectedPlayerId(null);
setSelectedBadgeId(null);
} catch (error) { } catch (error) {
toast.error("Failed to award badge"); toast.error("Failed to award badge");
} finally { } finally {
@@ -48,48 +52,59 @@ const AwardBadges = () => {
label: badge.name, label: badge.name,
})); }));
const selectedBadge = manualBadges.find((b) => b.id === selectedBadgeId);
return ( return (
<Box p="md"> <Box p="md">
<Card withBorder radius="md" p="md"> <Card withBorder radius="md" p="md">
<Stack gap="md"> <Stack gap="lg">
<Box> <Box>
<Text size="lg" fw={600} mb="xs"> <Text size="lg" fw={600} mb="xs">
Award Manual Badge Award Manual Badge
</Text> </Text>
<Text size="sm" c="dimmed">
Select a player and a manual badge to award
</Text>
</Box> </Box>
<Select <Select
label="Player" label="Badge Type"
placeholder="Select a player"
data={playerOptions}
value={selectedPlayerId}
onChange={setSelectedPlayerId}
searchable
clearable
/>
<Select
label="Badge"
placeholder="Select a badge" placeholder="Select a badge"
data={badgeOptions} data={badgeOptions}
value={selectedBadgeId} value={selectedBadgeId}
onChange={setSelectedBadgeId} onChange={setSelectedBadgeId}
searchable searchable
clearable clearable
size="md"
/> />
<Group justify="flex-end"> {selectedBadgeId && (
<Button <>
onClick={handleAwardBadge} <Divider />
disabled={!selectedPlayerId || !selectedBadgeId}
loading={isAwarding} <Stack gap="md">
> <Select
Award Badge label="Select Player"
</Button> placeholder="Choose a player"
</Group> data={playerOptions}
value={selectedPlayerId}
onChange={setSelectedPlayerId}
searchable
clearable
size="md"
autoFocus
/>
<Group justify="flex-end">
<Button
onClick={handleAwardBadge}
disabled={!selectedPlayerId}
loading={isAwarding}
size="md"
>
Award Badge
</Button>
</Group>
</Stack>
</>
)}
</Stack> </Stack>
</Card> </Card>
</Box> </Box>

View File

@@ -1,4 +1,4 @@
import { Box, Text, Popover, Progress } from "@mantine/core"; import { Box, Text, Popover, Progress, Title } from "@mantine/core";
import { usePlayerBadges, useAllBadges } from "../queries"; import { usePlayerBadges, useAllBadges } from "../queries";
import { useAuth } from "@/contexts/auth-context"; import { useAuth } from "@/contexts/auth-context";
import { Badge, BadgeProgress } from "../types"; import { Badge, BadgeProgress } from "../types";
@@ -202,25 +202,23 @@ const BadgeShowcase = ({ playerId }: BadgeShowcaseProps) => {
</Box> </Box>
)} )}
<Text <Title
size="xs" order={6}
fw={display.earned ? 600 : 500} fw={display.earned ? 600 : 500}
ta="center" ta="center"
c={display.earned ? undefined : 'dimmed'} c={display.earned ? undefined : 'dimmed'}
style={{ style={{ lineHeight: 1.1 }}
lineHeight: 1.2,
}}
> >
{display.badge.name} {display.badge.name}
</Text> </Title>
</Box> </Box>
</Box> </Box>
</Popover.Target> </Popover.Target>
<Popover.Dropdown> <Popover.Dropdown>
<Box> <Box>
<Text size="sm" fw={600} mb="xs"> <Title order={5}>
{display.badge.name} {display.badge.name}
</Text> </Title>
<Text size="xs" c="dimmed" mb={isCurrentUser ? "sm" : undefined}> <Text size="xs" c="dimmed" mb={isCurrentUser ? "sm" : undefined}>
{display.badge.description} {display.badge.description}
</Text> </Text>

View File

@@ -8,11 +8,12 @@ const Header = ({ collapsed, title, withBackButton }: HeaderProps) => {
return ( return (
<AppShell.Header <AppShell.Header
id='app-header' id='app-header'
display={collapsed ? 'none' : 'block'} display={collapsed ? 'none' : 'flex'}
style={{ alignItems: 'center', justifyContent: 'center' }}
> >
{ withBackButton && <BackButton /> } { withBackButton && <BackButton /> }
<Flex justify='center' align='center' h='100%' px='md'> <Flex justify='center' px='md' mt={8}>
<Title order={2}>{title}</Title> <Title order={1}>{title?.toLocaleUpperCase()}</Title>
</Flex> </Flex>
</AppShell.Header> </AppShell.Header>
); );

View File

@@ -32,7 +32,7 @@ const Layout: React.FC<PropsWithChildren> = ({ children }) => {
> >
<Stack align='center' gap='xs' mb='md'> <Stack align='center' gap='xs' mb='md'>
<TrophyIcon size={75} /> <TrophyIcon size={75} />
<Title order={4} ta='center'>Welcome to FLXN</Title> <Title order={1} ta='center'>Welcome to Flexxon</Title>
</Stack> </Stack>
{children} {children}
</Paper> </Paper>

View File

@@ -20,8 +20,8 @@ const Header = ({ player }: HeaderProps) => {
const name = useMemo(() => `${player.first_name} ${player.last_name}`, [player.first_name, player.last_name]); const name = useMemo(() => `${player.first_name} ${player.last_name}`, [player.first_name, player.last_name]);
const fontSize = useMemo(() => { const fontSize = useMemo(() => {
const baseSize = 24; const baseSize = 28;
const maxLength = 20; const maxLength = 24;
if (name.length <= maxLength) { if (name.length <= maxLength) {
return `${baseSize}px`; return `${baseSize}px`;

View File

@@ -4,7 +4,8 @@ import {
Group, Group,
Box, Box,
Stack, Stack,
Divider Divider,
Title
} from "@mantine/core"; } from "@mantine/core";
import { useTeam } from "../queries"; import { useTeam } from "../queries";
import Avatar from "@/components/avatar"; import Avatar from "@/components/avatar";
@@ -56,9 +57,9 @@ const TeamCard = ({ teamId }: TeamCardProps) => {
}} }}
/> />
<Box style={{ flex: 1, minWidth: 0 }}> <Box style={{ flex: 1, minWidth: 0 }}>
<Text size="md" fw={600} lineClamp={1} mb={2}> <Title order={5} lineClamp={1}>
{team.name} {team.name}
</Text> </Title>
<Text size="sm" c="dimmed" lineClamp={1}> <Text size="sm" c="dimmed" lineClamp={1}>
{team.players?.map(p => `${p.first_name} ${p.last_name}`).join(', ')} {team.players?.map(p => `${p.first_name} ${p.last_name}`).join(', ')}
</Text> </Text>

View File

@@ -9,10 +9,10 @@ interface SongSearchProps {
const SongSearch = ({ onChange, placeholder = "Search for songs..." }: SongSearchProps) => { const SongSearch = ({ onChange, placeholder = "Search for songs..." }: SongSearchProps) => {
const searchSpotifyTracks = async (query: string): Promise<TypeaheadOption<SpotifyTrack>[]> => { const searchSpotifyTracks = async (query: string): Promise<TypeaheadOption<SpotifyTrack>[]> => {
if (!query.trim()) return [];
try { try {
const response = await fetch(`/api/spotify/search?q=${encodeURIComponent(query)}`); // Use a default search term when query is empty to show popular tracks
const searchTerm = query.trim() || 'top hits';
const response = await fetch(`/api/spotify/search?q=${encodeURIComponent(searchTerm)}`);
if (!response.ok) { if (!response.ok) {
throw new Error('Search failed'); throw new Error('Search failed');

View File

@@ -19,7 +19,7 @@ const Header = ({ name, logo, id }: HeaderProps) => {
src={logo && id ? `/api/files/teams/${id}/${logo}` : undefined} src={logo && id ? `/api/files/teams/${id}/${logo}` : undefined}
/> />
<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" order={2}> <Title ta="center" order={1}>
{name} {name}
</Title> </Title>
</Flex> </Flex>

View File

@@ -13,7 +13,7 @@ const Header = ({ tournament }: HeaderProps) => {
<Flex h="20dvh" px='xl' w='100%' align='self-end' gap='md'> <Flex h="20dvh" px='xl' w='100%' align='self-end' gap='md'>
<Avatar contain name={tournament.name} radius={0} withBorder={false} size={150} src={`/api/files/tournaments/${tournament.id}/${tournament.logo}`} /> <Avatar contain name={tournament.name} radius={0} withBorder={false} size={150} src={`/api/files/tournaments/${tournament.id}/${tournament.logo}`} />
<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' order={2}>{tournament.name}</Title> <Title ta='center' order={1}>{tournament.name}</Title>
</Flex> </Flex>
</Flex> </Flex>
</> </>

View File

@@ -5,6 +5,7 @@ import {
Group, Group,
UnstyledButton, UnstyledButton,
Badge, Badge,
Title,
} from "@mantine/core"; } from "@mantine/core";
import { TournamentInfo } from "@/features/tournaments/types"; import { TournamentInfo } from "@/features/tournaments/types";
import { import {
@@ -57,7 +58,7 @@ export const TournamentCard = ({ tournament }: TournamentCardProps) => {
<Group justify="space-between" align="center"> <Group justify="space-between" align="center">
<Group gap="md" align="center"> <Group gap="md" align="center">
<Avatar <Avatar
size={90} size={95}
radius="sm" radius="sm"
name={tournament.name} name={tournament.name}
contain contain
@@ -70,9 +71,9 @@ export const TournamentCard = ({ tournament }: TournamentCardProps) => {
<TrophyIcon size={20} /> <TrophyIcon size={20} />
</Avatar> </Avatar>
<Stack gap="xs"> <Stack gap="xs">
<Text fw={600} size="lg" lineClamp={2}> <Title mb={-6} order={3} lineClamp={2}>
{tournament.name} {tournament.name}
</Text> </Title>
{(tournament.first_place || tournament.second_place || tournament.third_place) && ( {(tournament.first_place || tournament.second_place || tournament.third_place) && (
<Stack gap={6} > <Stack gap={6} >
{tournament.first_place && ( {tournament.first_place && (

View File

@@ -15,8 +15,6 @@ const TeamSelectionView: React.FC<TeamSelectionViewProps> = React.memo(({
const [selectedTeam, setSelectedTeam] = React.useState<ComboboxItem | null>(null); const [selectedTeam, setSelectedTeam] = React.useState<ComboboxItem | null>(null);
const searchTeams = async (query: string): Promise<TypeaheadOption<ComboboxItem>[]> => { const searchTeams = async (query: string): Promise<TypeaheadOption<ComboboxItem>[]> => {
if (!query.trim()) return [];
const filtered = options.filter(option => const filtered = options.filter(option =>
option.label.toLowerCase().includes(query.toLowerCase()) option.label.toLowerCase().includes(query.toLowerCase())
); );
@@ -66,6 +64,7 @@ const TeamSelectionView: React.FC<TeamSelectionViewProps> = React.memo(({
searchFn={searchTeams} searchFn={searchTeams}
renderOption={renderTeamOption} renderOption={renderTeamOption}
format={formatTeam} format={formatTeam}
maxHeight={80}
/> />
<Button <Button

View File

@@ -1,7 +1,7 @@
import { Suspense, useCallback, useMemo } from "react"; import { Suspense, useCallback, useMemo } from "react";
import { Tournament } from "../../types"; import { Tournament } from "../../types";
import { useAuth } from "@/contexts/auth-context"; import { useAuth } from "@/contexts/auth-context";
import { Box, Button, Card, Divider, Group, Stack, Text } from "@mantine/core"; import { Box, Button, Card, Divider, Group, Stack, Text, Title } from "@mantine/core";
import Countdown from "@/components/countdown"; import Countdown from "@/components/countdown";
import ListLink from "@/components/list-link"; import ListLink from "@/components/list-link";
import ListButton from "@/components/list-button"; import ListButton from "@/components/list-button";
@@ -56,11 +56,11 @@ const UpcomingTournament: React.FC<{ tournament: Tournament }> = ({
<Card withBorder radius="lg" p="lg"> <Card withBorder radius="lg" p="lg">
<Stack gap="xs"> <Stack gap="xs">
<Group mb="sm" gap="xs" align="center"> <Group gap="xs" align="center">
<UsersIcon size={16} /> <UsersIcon size={16} />
<Text size="sm" fw={500}> <Title mt={4} order={5}>
Enrollment Enrollment
</Text> </Title>
{isEnrollmentOpen && ( {isEnrollmentOpen && (
<Box ml="auto"> <Box ml="auto">
<Countdown <Countdown

View File

@@ -17,6 +17,8 @@ const commonInputStyles = {
const theme = createTheme({ const theme = createTheme({
defaultRadius: "sm", defaultRadius: "sm",
fontFamily: '"Inter", sans-serif',
headings: { fontFamily: '"League Spartan", sans-serif' },
components: { components: {
TextInput: { TextInput: {
styles: commonInputStyles, styles: commonInputStyles,

View File

@@ -18,6 +18,8 @@ class PocketBaseAdminClient {
this.pb = new PocketBase(process.env.POCKETBASE_URL); this.pb = new PocketBase(process.env.POCKETBASE_URL);
this.pb.beforeSend = async (url, options) => { this.pb.beforeSend = async (url, options) => {
await this.authPromise;
if (this.pb.authStore.isValid && this.isTokenExpiringSoon()) { if (this.pb.authStore.isValid && this.isTokenExpiringSoon()) {
try { try {
await this.refreshAuth(); await this.refreshAuth();
@@ -39,17 +41,16 @@ class PocketBaseAdminClient {
}; };
this.pb.autoCancellation(false); this.pb.autoCancellation(false);
Object.assign(this, createPlayersService(this.pb));
Object.assign(this, createTeamsService(this.pb));
Object.assign(this, createTournamentsService(this.pb));
Object.assign(this, createMatchesService(this.pb));
Object.assign(this, createReactionsService(this.pb));
Object.assign(this, createActivitiesService(this.pb));
Object.assign(this, createBadgesService(this.pb));
this.authPromise = this.authenticate(); this.authPromise = this.authenticate();
this.authPromise.then(() => { this.authPromise.then(() => {
Object.assign(this, createPlayersService(this.pb));
Object.assign(this, createTeamsService(this.pb));
Object.assign(this, createTournamentsService(this.pb));
Object.assign(this, createMatchesService(this.pb));
Object.assign(this, createReactionsService(this.pb));
Object.assign(this, createActivitiesService(this.pb));
Object.assign(this, createBadgesService(this.pb));
this.startTokenRefresh(); this.startTokenRefresh();
}); });
} }
@@ -62,7 +63,6 @@ class PocketBaseAdminClient {
process.env.POCKETBASE_ADMIN_EMAIL!, process.env.POCKETBASE_ADMIN_EMAIL!,
process.env.POCKETBASE_ADMIN_PASSWORD! process.env.POCKETBASE_ADMIN_PASSWORD!
); );
console.log('PocketBase admin authenticated successfully');
} catch (error) { } catch (error) {
console.error('Failed to authenticate PocketBase admin:', error); console.error('Failed to authenticate PocketBase admin:', error);
throw error; throw error;
@@ -72,7 +72,6 @@ class PocketBaseAdminClient {
private async refreshAuth() { private async refreshAuth() {
try { try {
await this.pb.collection("_superusers").authRefresh(); await this.pb.collection("_superusers").authRefresh();
console.log('PocketBase admin token refreshed');
} catch (error) { } catch (error) {
console.error('Failed to refresh PocketBase admin token:', error); console.error('Failed to refresh PocketBase admin token:', error);
throw error; throw error;