glitch effect avatar
This commit is contained in:
29
pb_migrations/1759594431_updated_tournaments.js
Normal file
29
pb_migrations/1759594431_updated_tournaments.js
Normal file
@@ -0,0 +1,29 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_340646327")
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(10, new Field({
|
||||
"hidden": false,
|
||||
"id": "file538556518",
|
||||
"maxSelect": 1,
|
||||
"maxSize": 0,
|
||||
"mimeTypes": [],
|
||||
"name": "glitch_logo",
|
||||
"presentable": false,
|
||||
"protected": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"thumbs": [],
|
||||
"type": "file"
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_340646327")
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("file538556518")
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
42
pb_migrations/1759594880_updated_tournaments.js
Normal file
42
pb_migrations/1759594880_updated_tournaments.js
Normal file
@@ -0,0 +1,42 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_340646327")
|
||||
|
||||
// update field
|
||||
collection.fields.addAt(10, new Field({
|
||||
"hidden": false,
|
||||
"id": "file538556518",
|
||||
"maxSelect": 1,
|
||||
"maxSize": 6000000,
|
||||
"mimeTypes": [],
|
||||
"name": "glitch_logo",
|
||||
"presentable": false,
|
||||
"protected": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"thumbs": [],
|
||||
"type": "file"
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_340646327")
|
||||
|
||||
// update field
|
||||
collection.fields.addAt(10, new Field({
|
||||
"hidden": false,
|
||||
"id": "file538556518",
|
||||
"maxSelect": 1,
|
||||
"maxSize": 0,
|
||||
"mimeTypes": [],
|
||||
"name": "glitch_logo",
|
||||
"presentable": false,
|
||||
"protected": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"thumbs": [],
|
||||
"type": "file"
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
179
src/components/glitch-avatar.tsx
Normal file
179
src/components/glitch-avatar.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { Paper, Box } from "@mantine/core";
|
||||
import {
|
||||
Avatar as MantineAvatar,
|
||||
AvatarProps as MantineAvatarProps,
|
||||
} from "@mantine/core";
|
||||
|
||||
interface GlitchAvatarProps
|
||||
extends Omit<MantineAvatarProps, "radius" | "color" | "size"> {
|
||||
name: string;
|
||||
src?: string;
|
||||
glitchSrc?: string;
|
||||
size?: number;
|
||||
radius?: string | number;
|
||||
withBorder?: boolean;
|
||||
contain?: boolean;
|
||||
children?: React.ReactNode;
|
||||
px?: string | number;
|
||||
}
|
||||
|
||||
const GlitchAvatar = ({
|
||||
name,
|
||||
src,
|
||||
glitchSrc,
|
||||
size = 35,
|
||||
radius = "100%",
|
||||
withBorder = true,
|
||||
contain = false,
|
||||
children,
|
||||
px,
|
||||
...props
|
||||
}: GlitchAvatarProps) => {
|
||||
const [showGlitch, setShowGlitch] = useState(false);
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!glitchSrc) return;
|
||||
|
||||
const scheduleNextGlitch = () => {
|
||||
const delay = Math.random() * 10000 + 5000;
|
||||
return setTimeout(() => {
|
||||
setShowGlitch(true);
|
||||
setIsPlaying(true);
|
||||
|
||||
setTimeout(() => {
|
||||
setShowGlitch(false);
|
||||
setIsPlaying(false);
|
||||
scheduleNextGlitch();
|
||||
}, 4000);
|
||||
}, delay);
|
||||
};
|
||||
|
||||
const timeoutId = scheduleNextGlitch();
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [glitchSrc]);
|
||||
|
||||
useEffect(() => {
|
||||
const video = videoRef.current;
|
||||
if (!video) return;
|
||||
|
||||
const handleEnded = () => {
|
||||
setShowGlitch(false);
|
||||
setIsPlaying(false);
|
||||
};
|
||||
|
||||
video.addEventListener("ended", handleEnded);
|
||||
return () => video.removeEventListener("ended", handleEnded);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const video = videoRef.current;
|
||||
if (!video) return;
|
||||
|
||||
video.load();
|
||||
}, [glitchSrc]);
|
||||
|
||||
useEffect(() => {
|
||||
const video = videoRef.current;
|
||||
if (!video || !showGlitch || !isPlaying) return;
|
||||
|
||||
video.currentTime = 0;
|
||||
video.play().catch((err) => {
|
||||
console.error("Failed to play glitch", err);
|
||||
});
|
||||
}, [showGlitch, isPlaying]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
style={{
|
||||
padding: "8px",
|
||||
borderRadius:
|
||||
typeof radius === "number"
|
||||
? `${radius + 8}px`
|
||||
: "calc(var(--mantine-radius-md) + 8px)",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
style={{
|
||||
opacity: showGlitch ? 0 : 1,
|
||||
transition: "opacity 0.05s ease-in-out",
|
||||
}}
|
||||
>
|
||||
<Paper
|
||||
py={size / 12.5}
|
||||
px={size / 20}
|
||||
bg="var(--mantine-color-default-border)"
|
||||
radius={radius}
|
||||
withBorder={false}
|
||||
style={{
|
||||
cursor: "default",
|
||||
}}
|
||||
>
|
||||
<MantineAvatar
|
||||
alt={name}
|
||||
key={name}
|
||||
name={name}
|
||||
color="initials"
|
||||
size={size}
|
||||
radius={radius}
|
||||
w={size}
|
||||
styles={{
|
||||
image: {
|
||||
objectFit: contain ? "contain" : "cover",
|
||||
},
|
||||
}}
|
||||
src={src}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</MantineAvatar>
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
{glitchSrc && (
|
||||
<Box
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "8px",
|
||||
left: "8px",
|
||||
opacity: showGlitch ? 1 : 0,
|
||||
visibility: showGlitch ? "visible" : "hidden",
|
||||
transition: "opacity 0.05s ease-in-out",
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
>
|
||||
<Paper
|
||||
py={size / 12.5}
|
||||
px={size / 20}
|
||||
bg="var(--mantine-color-default-border)"
|
||||
radius={radius}
|
||||
withBorder={false}
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<video
|
||||
ref={videoRef}
|
||||
src={glitchSrc}
|
||||
style={{
|
||||
width: `${size}px`,
|
||||
height: `${size}px`,
|
||||
objectFit: contain ? "contain" : "cover",
|
||||
borderRadius: typeof radius === "number" ? `${radius}px` : radius,
|
||||
display: "block",
|
||||
}}
|
||||
muted
|
||||
playsInline
|
||||
preload="auto"
|
||||
/>
|
||||
</Paper>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default GlitchAvatar;
|
||||
@@ -1,4 +1,6 @@
|
||||
import GlitchAvatar from '@/components/glitch-avatar';
|
||||
import useVisualViewportSize from '@/features/core/hooks/use-visual-viewport-size';
|
||||
import { useCurrentTournament } from '@/features/tournaments/queries';
|
||||
import { AppShell, Flex, Paper, em, Title, Stack } from '@mantine/core';
|
||||
import { useMediaQuery, useViewportSize } from '@mantine/hooks';
|
||||
import { TrophyIcon } from '@phosphor-icons/react';
|
||||
@@ -8,6 +10,7 @@ const Layout: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
const isMobile = useMediaQuery(`(max-width: ${em(450)})`);
|
||||
const visualViewport = useVisualViewportSize();
|
||||
const viewport = useViewportSize();
|
||||
const { data: tournament } = useCurrentTournament();
|
||||
|
||||
return (
|
||||
<AppShell>
|
||||
@@ -31,8 +34,27 @@ const Layout: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
radius='md'
|
||||
>
|
||||
<Stack align='center' gap='xs' mb='md'>
|
||||
<TrophyIcon size={75} />
|
||||
<Title order={1} ta='center'>Welcome to Flexxon</Title>
|
||||
<GlitchAvatar
|
||||
name={tournament.name}
|
||||
contain
|
||||
src={
|
||||
tournament.logo
|
||||
? `/api/files/tournaments/${tournament.id}/${tournament.logo}`
|
||||
: undefined
|
||||
}
|
||||
glitchSrc={
|
||||
tournament.glitch_logo
|
||||
? `/api/files/tournaments/${tournament.id}/${tournament.glitch_logo}`
|
||||
: undefined
|
||||
}
|
||||
radius="md"
|
||||
size={250}
|
||||
px="xs"
|
||||
withBorder={false}
|
||||
>
|
||||
<TrophyIcon size={32} />
|
||||
</GlitchAvatar>
|
||||
<Title order={1} ta='center'>Welcome to FLXN</Title>
|
||||
</Stack>
|
||||
{children}
|
||||
</Paper>
|
||||
|
||||
@@ -11,7 +11,7 @@ const Header = ({ tournament }: HeaderProps) => {
|
||||
return (
|
||||
<>
|
||||
<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="sm" 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={1}>{tournament.name}</Title>
|
||||
</Flex>
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { Group, Stack, ThemeIcon, Text, Flex } from "@mantine/core";
|
||||
import { Tournament } from "../../types";
|
||||
import Avatar from "@/components/avatar";
|
||||
import {
|
||||
CalendarIcon,
|
||||
MapPinIcon,
|
||||
TrophyIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
import { CalendarIcon, MapPinIcon, TrophyIcon } from "@phosphor-icons/react";
|
||||
import { useMemo } from "react";
|
||||
import GlitchAvatar from "@/components/glitch-avatar";
|
||||
|
||||
const Header = ({ tournament }: { tournament: Tournament }) => {
|
||||
const tournamentStart = useMemo(
|
||||
@@ -16,7 +12,7 @@ const Header = ({ tournament }: { tournament: Tournament }) => {
|
||||
|
||||
return (
|
||||
<Stack px="sm" align="center" gap={0}>
|
||||
<Avatar
|
||||
<GlitchAvatar
|
||||
name={tournament.name}
|
||||
contain
|
||||
src={
|
||||
@@ -24,13 +20,18 @@ const Header = ({ tournament }: { tournament: Tournament }) => {
|
||||
? `/api/files/tournaments/${tournament.id}/${tournament.logo}`
|
||||
: undefined
|
||||
}
|
||||
glitchSrc={
|
||||
tournament.glitch_logo
|
||||
? `/api/files/tournaments/${tournament.id}/${tournament.glitch_logo}`
|
||||
: undefined
|
||||
}
|
||||
radius="md"
|
||||
size={200}
|
||||
size={250}
|
||||
px="xs"
|
||||
withBorder={false}
|
||||
>
|
||||
<TrophyIcon size={24} />
|
||||
</Avatar>
|
||||
<TrophyIcon size={32} />
|
||||
</GlitchAvatar>
|
||||
<Flex gap="xs" direction="row" wrap="wrap" justify="space-around">
|
||||
{tournament.location && (
|
||||
<Group gap="xs">
|
||||
|
||||
@@ -28,13 +28,13 @@ const EnrollFreeAgent = ({ tournamentId }: {tournamentId: string} ) => {
|
||||
<Sheet title="Free Agent Enrollment" opened={isOpen} onChange={toggle}>
|
||||
<Stack gap="xs">
|
||||
<Text size="md">
|
||||
Enrolling as a free agent will enter you in a pool of players wanting to play but don't have a teammate yet.
|
||||
Enrolling as a free agent adds you to a pool of players looking for teammates.
|
||||
</Text>
|
||||
<Text size="sm" c='dimmed'>
|
||||
You will be able to see a list of other enrolled free agents, as well as their contact information for organizing your team and walkout song. By enrolling, your phone number will be visible to other free agents.
|
||||
Once enrolled, you can view other free agents and their phone number in order to coordinate teams and walkout songs.
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
Note: this does not guarantee you a spot in the tournament. One person from your team must enroll in the app and choose a walkout song in order to secure a spot.
|
||||
Important: Enrolling as a free agent does not guarantee a tournament spot. To secure a spot, one team member must register through the app and select a walkout song.
|
||||
</Text>
|
||||
<Button onClick={handleEnroll}>Confirm</Button>
|
||||
<Button variant="subtle" color="red" onClick={toggle}>Cancel</Button>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Group, Stack, ThemeIcon, Text, Flex } from "@mantine/core";
|
||||
import { Tournament } from "../../types";
|
||||
import Avatar from "@/components/avatar";
|
||||
import GlitchAvatar from "@/components/glitch-avatar";
|
||||
import {
|
||||
CalendarIcon,
|
||||
MapPinIcon,
|
||||
@@ -16,8 +16,8 @@ const Header = ({ tournament }: { tournament: Tournament }) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<Stack align="center" gap={0}>
|
||||
<Avatar
|
||||
<Stack align="center" gap={16}>
|
||||
<GlitchAvatar
|
||||
name={tournament.name}
|
||||
contain
|
||||
src={
|
||||
@@ -25,13 +25,18 @@ const Header = ({ tournament }: { tournament: Tournament }) => {
|
||||
? `/api/files/tournaments/${tournament.id}/${tournament.logo}`
|
||||
: undefined
|
||||
}
|
||||
glitchSrc={
|
||||
tournament.glitch_logo
|
||||
? `/api/files/tournaments/${tournament.id}/${tournament.glitch_logo}`
|
||||
: undefined
|
||||
}
|
||||
radius="md"
|
||||
size={300}
|
||||
px="xs"
|
||||
withBorder={false}
|
||||
>
|
||||
<TrophyIcon size={32} />
|
||||
</Avatar>
|
||||
</GlitchAvatar>
|
||||
<Flex gap="xs" direction="column" justify="space-around">
|
||||
{tournament.location && (
|
||||
<Group gap="xs">
|
||||
|
||||
@@ -25,6 +25,7 @@ export interface TournamentInfo {
|
||||
start_time?: string;
|
||||
end_time?: string;
|
||||
logo?: string;
|
||||
glitch_logo?: string;
|
||||
first_place?: TeamInfo;
|
||||
second_place?: TeamInfo;
|
||||
third_place?: TeamInfo;
|
||||
@@ -37,6 +38,7 @@ export interface Tournament {
|
||||
desc?: string;
|
||||
rules?: string;
|
||||
logo?: string;
|
||||
glitch_logo?: string;
|
||||
enroll_time?: string;
|
||||
start_time: string;
|
||||
end_time?: string;
|
||||
|
||||
@@ -59,9 +59,7 @@ export const transformMatch = (record: any, isAdmin: boolean = false): Match =>
|
||||
}
|
||||
|
||||
export const transformTournamentInfo = (record: any): TournamentInfo => {
|
||||
// Check if tournament is complete by looking at matches
|
||||
const matches = record.expand?.matches || [];
|
||||
// Filter out bye matches (tbd status with bye=true) when checking completion
|
||||
const nonByeMatches = matches.filter((match: any) => !(match.status === 'tbd' && match.bye === true));
|
||||
const isComplete = nonByeMatches.length > 0 && nonByeMatches.every((match: any) => match.status === 'ended');
|
||||
|
||||
@@ -108,6 +106,7 @@ export const transformTournamentInfo = (record: any): TournamentInfo => {
|
||||
start_time: record.start_time,
|
||||
end_time: record.end_time,
|
||||
logo: record.logo,
|
||||
glitch_logo: record.glitch_logo,
|
||||
first_place,
|
||||
second_place,
|
||||
third_place,
|
||||
@@ -258,6 +257,7 @@ export function transformTournament(record: any, isAdmin: boolean = false): Tour
|
||||
desc: record.desc,
|
||||
rules: record.rules,
|
||||
logo: record.logo,
|
||||
glitch_logo: record.glitch_logo,
|
||||
enroll_time: record.enroll_time,
|
||||
start_time: record.start_time,
|
||||
end_time: record.end_time,
|
||||
|
||||
Reference in New Issue
Block a user