Skip to content

Commit

Permalink
Create leaderboard info component (#606)
Browse files Browse the repository at this point in the history
* Use type imports in lib/api

* Add compoments, elements, and most of the styles

* Update vitest recommended extension

* REBASE: Add components

* Swap component use and add styles

* Allow styles to be passed to DropDown

* Rename LeaderboardDetails to LeaderboardInfo

* Create separate components for mobile and desktop views

* Miscellaneous style and attribute tweaks

* Add missing icons
And some substitutes, really

* Rename components

* Use icons in components

* Move socials array into Desktop component

* Fix component styling
Sorry this is a huge commit

* Add event handling for DropDownItem

* Rework component tree

* Apply roundabout way to get dropdown borders to work

* Add tests

* Remove unnecessary state check in DropDownItem

* Change class name

* Rename files and fix first Item padding

* Fix... lint errors?

* Uh

* Replace prop with event emit

* Added tests in components I forgot about lol

* Rename files to new casing

* Address comments

* Make component reactive to window resizing

---------

Co-authored-by: Edward Runkel <[email protected]>
  • Loading branch information
zysim and erunks authored May 11, 2024
1 parent 35df70d commit cbdae41
Show file tree
Hide file tree
Showing 38 changed files with 2,898 additions and 1,585 deletions.
2 changes: 1 addition & 1 deletion .vscode/extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@
"bradlc.vscode-tailwindcss", // Tooling for Tailwind
"csstools.postcss", // PostCSS as Tailwind is built on top of it
"Vue.volar", // Vue 3 Language support for VSC https://github.com/johnsoncodehk/volar
"ZixuanChen.vitest-explorer" // Vitest extension to run tests inside of VSCode
"vitest.explorer" // Vitest extension to run tests inside of VSCode
]
}
4 changes: 4 additions & 0 deletions assets/sprite/svg/book.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions assets/sprite/svg/link.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ export {}

declare module 'vue' {
export interface GlobalComponents {
ISvgBook: typeof import('~icons/svg/book')['default']
ISvgCircleCheck: typeof import('~icons/svg/circle-check')['default']
ISvgCircleExclamation: typeof import('~icons/svg/circle-exclamation')['default']
ISvgCircleInfo: typeof import('~icons/svg/circle-info')['default']
ISvgClock: typeof import('~icons/svg/clock')['default']
ISvgClose: typeof import('~icons/svg/close')['default']
ISvgEyeHidden: typeof import('~icons/svg/eye-hidden')['default']
ISvgEyeVisible: typeof import('~icons/svg/eye-visible')['default']
ISvgLink: typeof import('~icons/svg/link')['default']
ISvgMenu: typeof import('~icons/svg/menu')['default']
ISvgSearch: typeof import('~icons/svg/search')['default']
ISvgSpinner: typeof import('~icons/svg/spinner')['default']
Expand Down
36 changes: 36 additions & 0 deletions components/blocks/LeaderboardInfo/Desktop/Desktop.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { enableAutoUnmount, mount } from '@vue/test-utils'
import { vi } from 'vitest'
import { getByTestId } from 'root/testUtils'
import Desktop from './Desktop.vue'

describe('<Desktop />', () => {
function getDesktopWrapper() {
return mount(Desktop, {
props: {
leaderboard: {
categories: [{ id: 1, name: 'Any%', slug: 'any' }],
id: 1,
name: 'Stuck in the Train Simulator 2',
slug: 'stuck-in-the-train-sim-2',
},
todoPlatforms: ['XBox', 'PC'],
},
})
}

afterEach(() => {
vi.restoreAllMocks()
})

enableAutoUnmount(afterEach)

it('should render without crashing', () => {
const wrapper = getDesktopWrapper()
expect(wrapper.isVisible()).toBe(true)

expect(getByTestId(wrapper, 'title').text()).toBe(
'Stuck in the Train Simulator 2',
)
expect(getByTestId(wrapper, 'tag').text()).toBe('Any%')
})
})
141 changes: 141 additions & 0 deletions components/blocks/LeaderboardInfo/Desktop/Desktop.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
<script setup lang="ts">
import Dropdown from 'elements/buttons/Dropdown/Dropdown.vue'
import DropdownItem from 'elements/buttons/Dropdown/DropdownItem.vue'
import ButtonLink from 'elements/buttons/ButtonLink/ButtonLink.vue'
import FollowButton from 'elements/buttons/FollowButton/FollowButton.vue'
import SocialButtons from 'elements/buttons/SocialButtons/SocialButtons.vue'
import Tag from 'elements/tags/Tag/Tag.vue'
import PlatformTags from '../PlatformTags/PlatformTags.vue'
import type { LeaderboardInfoProps } from '../LeaderboardInfo.vue'
interface Props extends LeaderboardInfoProps {
todoPlatforms: string[]
}
const emit = defineEmits<{
(event: 'follow'): void
}>()
// TODO: Get actual links tied to the leaderboard
const socials: Socials[] = [
{
icon: 'twitch',
name: 'Twitch',
url: 'https://twitch.tv',
},
{
icon: 'globe',
name: 'Site',
url: '#',
},
{
icon: 'discord',
name: 'Discord',
url: 'https://discord.gg',
},
{
icon: 'message',
name: 'Message',
url: '#',
},
]
defineProps<Props>()
</script>

<template>
<div class="leaderboard-info">
<!-- TODO: Do not provide defaults and instead return nothing if on production -->
<div class="title" data-testid="title">
{{ leaderboard.name || 'Game Name' }}
</div>
<div class="body">
<img
src="https://via.placeholder.com/220x220"
alt="game-art"
class="body__game-art"
/>
<Tag data-testid="tag">{{
leaderboard.categories[0]?.name ?? 'TODO'
}}</Tag>
<span class="body__published-year">YEAR</span>
<PlatformTags :tags="todoPlatforms" />
<FollowButton
class="body__follow"
data-testid="body__follow"
@follow="emit('follow')"
/>
<div class="body__divider" />
<div class="body__internal-link-buttons">
<ButtonLink class="body__internal-link-buttons__button" to="#">
<i-svg-book />
<span>Guides</span>
</ButtonLink>
<ButtonLink class="body__internal-link-buttons__button" to="#">
<i-svg-link />
<div>Resources</div>
</ButtonLink>
<Dropdown class-name="body__internal-link-buttons__button">
<DropdownItem>First</DropdownItem>
<DropdownItem>Second</DropdownItem>
<DropdownItem>Third</DropdownItem>
</Dropdown>
</div>
<SocialButtons :socials="socials" />
</div>
</div>
</template>

<style lang="postcss" scoped>
.icon {
@apply w-2;
}
.leaderboard-info {
@apply h-[fit-content] min-h-[28.125rem] w-[fit-content] relative;
@apply flex flex-col items-center;
@apply border-gray-300 border rounded;
& .title {
@apply w-full text-center;
@apply font-bold text-lg p-3;
@apply border-b-gray-200 border-b-2;
}
& .body {
@apply m-4;
@apply flex flex-col items-center gap-1;
& .body__game-art {
@apply my-1 px-8;
}
& .body__published-year {
@apply text-sm;
}
& .body__follow {
@apply mt-3;
}
& .body__divider {
@apply border-b border-gray-300 w-full my-3;
}
& .body__internal-link-buttons {
@apply w-full flex flex-col mb-4 gap-2;
& .body__internal-link-buttons__button {
@apply w-full flex justify-center items-center gap-2 bg-white hover:bg-gray-100;
@apply border-gray-200 border-2 rounded;
}
& :deep(.body__internal-link-buttons__button) {
@apply w-full bg-white hover:bg-gray-100;
@apply border-gray-200 border-2 rounded;
}
}
}
}
</style>
elements/buttons/Dropdown/Dropdown.vueelements/buttons/Dropdown/DropdownItem.vue
67 changes: 67 additions & 0 deletions components/blocks/LeaderboardInfo/LeaderboardInfo.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { enableAutoUnmount, mount } from '@vue/test-utils'
import { vi } from 'vitest'
import { getByTestId } from 'root/testUtils'
import LeaderboardInfo from './LeaderboardInfo.vue'

describe('<LeaderboardInfo />', () => {
function getLeaderboardInfoWrapper() {
return mount(LeaderboardInfo, {
props: {
leaderboard: {
categories: [],
id: 1,
name: 'Stuck in the Train Simulator 2',
slug: 'stuck-in-the-train-sim-2',
socials: [
{
icon: 'disc',
name: 'discord',
url: 'https://discord.gg/leaderboards.gg',
},
{
icon: 'youtube',
name: 'Socials',
url: 'https://youtube.com',
},
{
icon: 'twit',
name: 'twitter',
url: 'https://twitter.com/bestofdyingtwit',
},
],
},
},
})
}

afterEach(() => {
vi.restoreAllMocks()
vi.unstubAllGlobals()
})

enableAutoUnmount(afterEach)

it('should render without crashing', () => {
const wrapper = getLeaderboardInfoWrapper()
expect(wrapper.isVisible()).toBe(true)
})

it('should render <Desktop /> if device width is large', () => {
vi.stubGlobal('innerWidth', 1980)
const wrapper = getLeaderboardInfoWrapper()
expect(wrapper.html()).toContain('Guides')
})

it('should render <Mobile /> if device width is small', () => {
vi.stubGlobal('innerWidth', 600)
const wrapper = getLeaderboardInfoWrapper()
expect(wrapper.html()).toContain('Submit Run')
})

// TODO: The follow event doesn't trigger in the test, somehow
it.skip('should emit event when the Follow Button is triggered', async () => {
const wrapper = getLeaderboardInfoWrapper()
await getByTestId(wrapper, 'child').trigger('follow')
expect(wrapper.emitted().follow).toBeTruthy()
})
})
55 changes: 55 additions & 0 deletions components/blocks/LeaderboardInfo/LeaderboardInfo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<script setup lang="ts">
import { useThrottleFn } from '@vueuse/core'
import resolveConfig from 'tailwindcss/resolveConfig'
import { ref } from 'vue'
import tailwindConfig from 'root/tailwind.config'
import Desktop from './Desktop/Desktop.vue'
import Mobile from './Mobile/Mobile.vue'
import type { LeaderboardViewModel } from 'lib/api/data-contracts'
export interface LeaderboardInfoProps {
leaderboard: LeaderboardViewModel
}
// TODO: Remove this. Get from model instead.
const todoPlatforms = ['PS4', 'PC', 'XboxSeriesX']
// TODO: Implement listeners
const emit = defineEmits<{
(event: 'follow', leaderboardId: number): void
}>()
const mobileWidth = parseInt(
resolveConfig(tailwindConfig).theme.screens.sm.replace('px', ''),
10,
)
const isMobile = ref(window.innerWidth <= mobileWidth)
function checkIsMobile() {
isMobile.value = window.innerWidth <= mobileWidth
}
window.addEventListener('resize', useThrottleFn(checkIsMobile, 20))
defineProps<LeaderboardInfoProps>()
</script>

<template>
<Mobile
v-if="isMobile.valueOf()"
data-testid="child"
:leaderboard="leaderboard"
:todo-platforms="todoPlatforms"
@follow="emit('follow', leaderboard.id)"
/>
<Desktop
v-else
data-testid="child"
:leaderboard="leaderboard"
:todo-platforms="todoPlatforms"
@follow="emit('follow', leaderboard.id)"
/>
</template>

<style lang="postcss" scoped></style>
36 changes: 36 additions & 0 deletions components/blocks/LeaderboardInfo/Mobile/Mobile.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { enableAutoUnmount, mount } from '@vue/test-utils'
import { vi } from 'vitest'
import { getByTestId } from 'root/testUtils'
import Mobile from './Mobile.vue'

describe('<Mobile />', () => {
function getMobileWrapper() {
return mount(Mobile, {
props: {
leaderboard: {
categories: [{ id: 1, name: 'Any%', slug: 'any' }],
id: 1,
name: 'Stuck in the Train Simulator 2',
slug: 'stuck-in-the-train-sim-2',
},
todoPlatforms: ['XBox', 'PC'],
},
})
}

afterEach(() => {
vi.restoreAllMocks()
})

enableAutoUnmount(afterEach)

it('should render without crashing', () => {
const wrapper = getMobileWrapper()
expect(wrapper.isVisible()).toBe(true)

expect(getByTestId(wrapper, 'title').text()).toBe(
'Stuck in the Train Simulator 2',
)
expect(getByTestId(wrapper, 'tag').text()).toBe('Any%')
})
})
Loading

0 comments on commit cbdae41

Please sign in to comment.