bug fixes, new fonts, etc
This commit is contained in:
@@ -63,7 +63,17 @@ export const Route = createRootRouteWithContext<{
|
||||
{ rel: 'stylesheet', href: mantineCssUrl },
|
||||
{ rel: 'stylesheet', href: mantineCarouselCssUrl },
|
||||
{ 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) => {
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface TypeaheadProps<T> {
|
||||
debounceMs?: number;
|
||||
disabled?: boolean;
|
||||
initialValue?: string;
|
||||
maxHeight?: number | string;
|
||||
}
|
||||
|
||||
const Typeahead = <T,>({
|
||||
@@ -26,7 +27,8 @@ const Typeahead = <T,>({
|
||||
placeholder = "Search...",
|
||||
debounceMs = 300,
|
||||
disabled = false,
|
||||
initialValue = ""
|
||||
initialValue = "",
|
||||
maxHeight = 200,
|
||||
}: TypeaheadProps<T>) => {
|
||||
const [searchQuery, setSearchQuery] = useState(initialValue);
|
||||
const [searchResults, setSearchResults] = useState<TypeaheadOption<T>[]>([]);
|
||||
@@ -36,13 +38,7 @@ const Typeahead = <T,>({
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const debouncedSearch = useDebouncedCallback(async (query: string) => {
|
||||
if (!query.trim()) {
|
||||
setSearchResults([]);
|
||||
setIsOpen(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const performSearch = async (query: string) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const results = await searchFn(query);
|
||||
@@ -56,7 +52,9 @@ const Typeahead = <T,>({
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, debounceMs);
|
||||
};
|
||||
|
||||
const debouncedSearch = useDebouncedCallback(performSearch, debounceMs);
|
||||
|
||||
const handleSearchChange = (value: string) => {
|
||||
setSearchQuery(value);
|
||||
@@ -114,8 +112,12 @@ const Typeahead = <T,>({
|
||||
value={searchQuery}
|
||||
onChange={(event) => handleSearchChange(event.currentTarget.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onFocus={() => {
|
||||
if (searchResults.length > 0) setIsOpen(true);
|
||||
onFocus={async () => {
|
||||
if (searchResults.length > 0) {
|
||||
setIsOpen(true);
|
||||
return;
|
||||
}
|
||||
await performSearch(searchQuery);
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
rightSection={isLoading ? <Loader size="xs" /> : null}
|
||||
@@ -133,7 +135,7 @@ const Typeahead = <T,>({
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 9999,
|
||||
maxHeight: '160px',
|
||||
maxHeight,
|
||||
overflowY: 'auto',
|
||||
WebkitOverflowScrolling: 'touch',
|
||||
touchAction: 'pan-y',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { useAllBadges } from "@/features/badges/queries";
|
||||
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);
|
||||
setSelectedBadgeId(null);
|
||||
} catch (error) {
|
||||
toast.error("Failed to award badge");
|
||||
} finally {
|
||||
@@ -48,48 +52,59 @@ const AwardBadges = () => {
|
||||
label: badge.name,
|
||||
}));
|
||||
|
||||
const selectedBadge = manualBadges.find((b) => b.id === selectedBadgeId);
|
||||
|
||||
return (
|
||||
<Box p="md">
|
||||
<Card withBorder radius="md" p="md">
|
||||
<Stack gap="md">
|
||||
<Stack gap="lg">
|
||||
<Box>
|
||||
<Text size="lg" fw={600} mb="xs">
|
||||
Award Manual Badge
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
Select a player and a manual badge to award
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Select
|
||||
label="Player"
|
||||
placeholder="Select a player"
|
||||
data={playerOptions}
|
||||
value={selectedPlayerId}
|
||||
onChange={setSelectedPlayerId}
|
||||
searchable
|
||||
clearable
|
||||
/>
|
||||
|
||||
<Select
|
||||
label="Badge"
|
||||
label="Badge Type"
|
||||
placeholder="Select a badge"
|
||||
data={badgeOptions}
|
||||
value={selectedBadgeId}
|
||||
onChange={setSelectedBadgeId}
|
||||
searchable
|
||||
clearable
|
||||
size="md"
|
||||
/>
|
||||
|
||||
<Group justify="flex-end">
|
||||
<Button
|
||||
onClick={handleAwardBadge}
|
||||
disabled={!selectedPlayerId || !selectedBadgeId}
|
||||
loading={isAwarding}
|
||||
>
|
||||
Award Badge
|
||||
</Button>
|
||||
</Group>
|
||||
{selectedBadgeId && (
|
||||
<>
|
||||
<Divider />
|
||||
|
||||
<Stack gap="md">
|
||||
<Select
|
||||
label="Select Player"
|
||||
placeholder="Choose a player"
|
||||
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>
|
||||
</Card>
|
||||
</Box>
|
||||
|
||||
@@ -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 { useAuth } from "@/contexts/auth-context";
|
||||
import { Badge, BadgeProgress } from "../types";
|
||||
@@ -202,25 +202,23 @@ const BadgeShowcase = ({ playerId }: BadgeShowcaseProps) => {
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Text
|
||||
size="xs"
|
||||
<Title
|
||||
order={6}
|
||||
fw={display.earned ? 600 : 500}
|
||||
ta="center"
|
||||
c={display.earned ? undefined : 'dimmed'}
|
||||
style={{
|
||||
lineHeight: 1.2,
|
||||
}}
|
||||
style={{ lineHeight: 1.1 }}
|
||||
>
|
||||
{display.badge.name}
|
||||
</Text>
|
||||
</Title>
|
||||
</Box>
|
||||
</Box>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown>
|
||||
<Box>
|
||||
<Text size="sm" fw={600} mb="xs">
|
||||
<Title order={5}>
|
||||
{display.badge.name}
|
||||
</Text>
|
||||
</Title>
|
||||
<Text size="xs" c="dimmed" mb={isCurrentUser ? "sm" : undefined}>
|
||||
{display.badge.description}
|
||||
</Text>
|
||||
|
||||
@@ -8,11 +8,12 @@ const Header = ({ collapsed, title, withBackButton }: HeaderProps) => {
|
||||
return (
|
||||
<AppShell.Header
|
||||
id='app-header'
|
||||
display={collapsed ? 'none' : 'block'}
|
||||
display={collapsed ? 'none' : 'flex'}
|
||||
style={{ alignItems: 'center', justifyContent: 'center' }}
|
||||
>
|
||||
{ withBackButton && <BackButton /> }
|
||||
<Flex justify='center' align='center' h='100%' px='md'>
|
||||
<Title order={2}>{title}</Title>
|
||||
<Flex justify='center' px='md' mt={8}>
|
||||
<Title order={1}>{title?.toLocaleUpperCase()}</Title>
|
||||
</Flex>
|
||||
</AppShell.Header>
|
||||
);
|
||||
|
||||
@@ -32,7 +32,7 @@ const Layout: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
>
|
||||
<Stack align='center' gap='xs' mb='md'>
|
||||
<TrophyIcon size={75} />
|
||||
<Title order={4} ta='center'>Welcome to FLXN</Title>
|
||||
<Title order={1} ta='center'>Welcome to Flexxon</Title>
|
||||
</Stack>
|
||||
{children}
|
||||
</Paper>
|
||||
|
||||
@@ -20,8 +20,8 @@ const Header = ({ player }: HeaderProps) => {
|
||||
const name = useMemo(() => `${player.first_name} ${player.last_name}`, [player.first_name, player.last_name]);
|
||||
|
||||
const fontSize = useMemo(() => {
|
||||
const baseSize = 24;
|
||||
const maxLength = 20;
|
||||
const baseSize = 28;
|
||||
const maxLength = 24;
|
||||
|
||||
if (name.length <= maxLength) {
|
||||
return `${baseSize}px`;
|
||||
|
||||
@@ -4,7 +4,8 @@ import {
|
||||
Group,
|
||||
Box,
|
||||
Stack,
|
||||
Divider
|
||||
Divider,
|
||||
Title
|
||||
} from "@mantine/core";
|
||||
import { useTeam } from "../queries";
|
||||
import Avatar from "@/components/avatar";
|
||||
@@ -56,9 +57,9 @@ const TeamCard = ({ teamId }: TeamCardProps) => {
|
||||
}}
|
||||
/>
|
||||
<Box style={{ flex: 1, minWidth: 0 }}>
|
||||
<Text size="md" fw={600} lineClamp={1} mb={2}>
|
||||
<Title order={5} lineClamp={1}>
|
||||
{team.name}
|
||||
</Text>
|
||||
</Title>
|
||||
<Text size="sm" c="dimmed" lineClamp={1}>
|
||||
{team.players?.map(p => `${p.first_name} ${p.last_name}`).join(', ')}
|
||||
</Text>
|
||||
|
||||
@@ -9,10 +9,10 @@ interface SongSearchProps {
|
||||
|
||||
const SongSearch = ({ onChange, placeholder = "Search for songs..." }: SongSearchProps) => {
|
||||
const searchSpotifyTracks = async (query: string): Promise<TypeaheadOption<SpotifyTrack>[]> => {
|
||||
if (!query.trim()) return [];
|
||||
|
||||
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) {
|
||||
throw new Error('Search failed');
|
||||
|
||||
@@ -19,7 +19,7 @@ const Header = ({ name, logo, id }: HeaderProps) => {
|
||||
src={logo && id ? `/api/files/teams/${id}/${logo}` : undefined}
|
||||
/>
|
||||
<Flex align="center" justify="center" gap={4} pb={20} w="100%">
|
||||
<Title ta="center" order={2}>
|
||||
<Title ta="center" order={1}>
|
||||
{name}
|
||||
</Title>
|
||||
</Flex>
|
||||
|
||||
@@ -13,7 +13,7 @@ const Header = ({ tournament }: HeaderProps) => {
|
||||
<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}`} />
|
||||
<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>
|
||||
</>
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
Group,
|
||||
UnstyledButton,
|
||||
Badge,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { TournamentInfo } from "@/features/tournaments/types";
|
||||
import {
|
||||
@@ -57,7 +58,7 @@ export const TournamentCard = ({ tournament }: TournamentCardProps) => {
|
||||
<Group justify="space-between" align="center">
|
||||
<Group gap="md" align="center">
|
||||
<Avatar
|
||||
size={90}
|
||||
size={95}
|
||||
radius="sm"
|
||||
name={tournament.name}
|
||||
contain
|
||||
@@ -70,9 +71,9 @@ export const TournamentCard = ({ tournament }: TournamentCardProps) => {
|
||||
<TrophyIcon size={20} />
|
||||
</Avatar>
|
||||
<Stack gap="xs">
|
||||
<Text fw={600} size="lg" lineClamp={2}>
|
||||
<Title mb={-6} order={3} lineClamp={2}>
|
||||
{tournament.name}
|
||||
</Text>
|
||||
</Title>
|
||||
{(tournament.first_place || tournament.second_place || tournament.third_place) && (
|
||||
<Stack gap={6} >
|
||||
{tournament.first_place && (
|
||||
|
||||
@@ -15,8 +15,6 @@ const TeamSelectionView: React.FC<TeamSelectionViewProps> = React.memo(({
|
||||
const [selectedTeam, setSelectedTeam] = React.useState<ComboboxItem | null>(null);
|
||||
|
||||
const searchTeams = async (query: string): Promise<TypeaheadOption<ComboboxItem>[]> => {
|
||||
if (!query.trim()) return [];
|
||||
|
||||
const filtered = options.filter(option =>
|
||||
option.label.toLowerCase().includes(query.toLowerCase())
|
||||
);
|
||||
@@ -66,6 +64,7 @@ const TeamSelectionView: React.FC<TeamSelectionViewProps> = React.memo(({
|
||||
searchFn={searchTeams}
|
||||
renderOption={renderTeamOption}
|
||||
format={formatTeam}
|
||||
maxHeight={80}
|
||||
/>
|
||||
|
||||
<Button
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Suspense, useCallback, useMemo } from "react";
|
||||
import { Tournament } from "../../types";
|
||||
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 ListLink from "@/components/list-link";
|
||||
import ListButton from "@/components/list-button";
|
||||
@@ -56,11 +56,11 @@ const UpcomingTournament: React.FC<{ tournament: Tournament }> = ({
|
||||
|
||||
<Card withBorder radius="lg" p="lg">
|
||||
<Stack gap="xs">
|
||||
<Group mb="sm" gap="xs" align="center">
|
||||
<Group gap="xs" align="center">
|
||||
<UsersIcon size={16} />
|
||||
<Text size="sm" fw={500}>
|
||||
<Title mt={4} order={5}>
|
||||
Enrollment
|
||||
</Text>
|
||||
</Title>
|
||||
{isEnrollmentOpen && (
|
||||
<Box ml="auto">
|
||||
<Countdown
|
||||
|
||||
@@ -17,6 +17,8 @@ const commonInputStyles = {
|
||||
|
||||
const theme = createTheme({
|
||||
defaultRadius: "sm",
|
||||
fontFamily: '"Inter", sans-serif',
|
||||
headings: { fontFamily: '"League Spartan", sans-serif' },
|
||||
components: {
|
||||
TextInput: {
|
||||
styles: commonInputStyles,
|
||||
|
||||
@@ -18,6 +18,8 @@ class PocketBaseAdminClient {
|
||||
this.pb = new PocketBase(process.env.POCKETBASE_URL);
|
||||
|
||||
this.pb.beforeSend = async (url, options) => {
|
||||
await this.authPromise;
|
||||
|
||||
if (this.pb.authStore.isValid && this.isTokenExpiringSoon()) {
|
||||
try {
|
||||
await this.refreshAuth();
|
||||
@@ -39,17 +41,16 @@ class PocketBaseAdminClient {
|
||||
};
|
||||
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.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();
|
||||
});
|
||||
}
|
||||
@@ -62,7 +63,6 @@ class PocketBaseAdminClient {
|
||||
process.env.POCKETBASE_ADMIN_EMAIL!,
|
||||
process.env.POCKETBASE_ADMIN_PASSWORD!
|
||||
);
|
||||
console.log('PocketBase admin authenticated successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to authenticate PocketBase admin:', error);
|
||||
throw error;
|
||||
@@ -72,7 +72,6 @@ class PocketBaseAdminClient {
|
||||
private async refreshAuth() {
|
||||
try {
|
||||
await this.pb.collection("_superusers").authRefresh();
|
||||
console.log('PocketBase admin token refreshed');
|
||||
} catch (error) {
|
||||
console.error('Failed to refresh PocketBase admin token:', error);
|
||||
throw error;
|
||||
|
||||
Reference in New Issue
Block a user