From 95a50ee7a7b28212b0f9647fe37769b56a4c77ff Mon Sep 17 00:00:00 2001 From: yohlo Date: Sat, 4 Oct 2025 18:41:46 -0500 Subject: [PATCH] glitch effect avatar --- .../1759594431_updated_tournaments.js | 29 +++ .../1759594880_updated_tournaments.js | 42 ++++ src/components/glitch-avatar.tsx | 179 ++++++++++++++++++ src/features/login/components/layout.tsx | 26 ++- .../tournaments/components/profile/header.tsx | 2 +- .../components/started-tournament/header.tsx | 23 +-- .../upcoming-tournament/enroll-free-agent.tsx | 6 +- .../components/upcoming-tournament/header.tsx | 13 +- src/features/tournaments/types.ts | 2 + src/lib/pocketbase/util/transform-types.ts | 4 +- 10 files changed, 303 insertions(+), 23 deletions(-) create mode 100644 pb_migrations/1759594431_updated_tournaments.js create mode 100644 pb_migrations/1759594880_updated_tournaments.js create mode 100644 src/components/glitch-avatar.tsx diff --git a/pb_migrations/1759594431_updated_tournaments.js b/pb_migrations/1759594431_updated_tournaments.js new file mode 100644 index 0000000..9c77c4c --- /dev/null +++ b/pb_migrations/1759594431_updated_tournaments.js @@ -0,0 +1,29 @@ +/// +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) +}) diff --git a/pb_migrations/1759594880_updated_tournaments.js b/pb_migrations/1759594880_updated_tournaments.js new file mode 100644 index 0000000..29c39e6 --- /dev/null +++ b/pb_migrations/1759594880_updated_tournaments.js @@ -0,0 +1,42 @@ +/// +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) +}) diff --git a/src/components/glitch-avatar.tsx b/src/components/glitch-avatar.tsx new file mode 100644 index 0000000..576eabb --- /dev/null +++ b/src/components/glitch-avatar.tsx @@ -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 { + 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(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 ( + + + + + {children} + + + + + {glitchSrc && ( + + + + + )} + + ); +}; + +export default GlitchAvatar; diff --git a/src/features/login/components/layout.tsx b/src/features/login/components/layout.tsx index 271fc1c..ef6a4ba 100644 --- a/src/features/login/components/layout.tsx +++ b/src/features/login/components/layout.tsx @@ -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 = ({ children }) => { const isMobile = useMediaQuery(`(max-width: ${em(450)})`); const visualViewport = useVisualViewportSize(); const viewport = useViewportSize(); + const { data: tournament } = useCurrentTournament(); return ( @@ -31,8 +34,27 @@ const Layout: React.FC = ({ children }) => { radius='md' > - - Welcome to Flexxon + + + + Welcome to FLXN {children} diff --git a/src/features/tournaments/components/profile/header.tsx b/src/features/tournaments/components/profile/header.tsx index cf742c5..f718b0f 100644 --- a/src/features/tournaments/components/profile/header.tsx +++ b/src/features/tournaments/components/profile/header.tsx @@ -11,7 +11,7 @@ const Header = ({ tournament }: HeaderProps) => { return ( <> - + {tournament.name} diff --git a/src/features/tournaments/components/started-tournament/header.tsx b/src/features/tournaments/components/started-tournament/header.tsx index 6f04fc7..94e48c0 100644 --- a/src/features/tournaments/components/started-tournament/header.tsx +++ b/src/features/tournaments/components/started-tournament/header.tsx @@ -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 ( - { ? `/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} > - - + + {tournament.location && ( @@ -65,4 +66,4 @@ const Header = ({ tournament }: { tournament: Tournament }) => { ); }; -export default Header; \ No newline at end of file +export default Header; diff --git a/src/features/tournaments/components/upcoming-tournament/enroll-free-agent.tsx b/src/features/tournaments/components/upcoming-tournament/enroll-free-agent.tsx index 69b000c..75b5c74 100644 --- a/src/features/tournaments/components/upcoming-tournament/enroll-free-agent.tsx +++ b/src/features/tournaments/components/upcoming-tournament/enroll-free-agent.tsx @@ -28,13 +28,13 @@ const EnrollFreeAgent = ({ tournamentId }: {tournamentId: string} ) => { - 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. - 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. - 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. diff --git a/src/features/tournaments/components/upcoming-tournament/header.tsx b/src/features/tournaments/components/upcoming-tournament/header.tsx index 0ec00ba..9d936df 100644 --- a/src/features/tournaments/components/upcoming-tournament/header.tsx +++ b/src/features/tournaments/components/upcoming-tournament/header.tsx @@ -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 ( - - + { ? `/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} > - + {tournament.location && ( diff --git a/src/features/tournaments/types.ts b/src/features/tournaments/types.ts index bda7175..e684645 100644 --- a/src/features/tournaments/types.ts +++ b/src/features/tournaments/types.ts @@ -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; diff --git a/src/lib/pocketbase/util/transform-types.ts b/src/lib/pocketbase/util/transform-types.ts index 4dc615c..b80a4e0 100644 --- a/src/lib/pocketbase/util/transform-types.ts +++ b/src/lib/pocketbase/util/transform-types.ts @@ -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,