Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create leaderboard info component #606

Merged
merged 28 commits into from
May 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
5d474e2
Use type imports in lib/api
zysim Mar 16, 2024
c4bc9ce
Add compoments, elements, and most of the styles
zysim Mar 16, 2024
eb64aed
Update vitest recommended extension
zysim Mar 25, 2024
03c6ca0
REBASE: Add components
zysim Mar 25, 2024
f53e733
Swap component use and add styles
zysim Mar 31, 2024
a060f76
Allow styles to be passed to DropDown
zysim Apr 1, 2024
8bf21ca
Rename LeaderboardDetails to LeaderboardInfo
zysim Apr 3, 2024
443bb91
Create separate components for mobile and desktop views
zysim Apr 14, 2024
0721f3b
Miscellaneous style and attribute tweaks
zysim Apr 14, 2024
fde92ea
Add missing icons
zysim Apr 19, 2024
e91dc69
Rename components
zysim Apr 19, 2024
c162b7c
Use icons in components
zysim Apr 19, 2024
7b28b2a
Move socials array into Desktop component
zysim Apr 19, 2024
0caa9bb
Fix component styling
zysim Apr 21, 2024
99895e2
Add event handling for DropDownItem
zysim Apr 21, 2024
71229dc
Rework component tree
zysim May 2, 2024
c838c06
Apply roundabout way to get dropdown borders to work
zysim May 2, 2024
c724e4f
Add tests
zysim May 5, 2024
4fefcbb
Remove unnecessary state check in DropDownItem
zysim May 5, 2024
f8c82d6
Change class name
zysim May 5, 2024
bbb5a9b
Rename files and fix first Item padding
zysim May 5, 2024
5d23b9e
Fix... lint errors?
zysim May 5, 2024
abd3a3f
Uh
zysim May 5, 2024
c777a31
Replace prop with event emit
zysim May 5, 2024
4aaca13
Added tests in components I forgot about lol
zysim May 5, 2024
6c5cfe1
Rename files to new casing
erunks May 7, 2024
ab14177
Address comments
zysim May 10, 2024
40c2a83
Make component reactive to window resizing
zysim May 10, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading