diff --git a/.vscode/extensions.json b/.vscode/extensions.json index ffa80745..5d14fc65 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -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 ] } diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..ba921f6e --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,21 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "firefox", + "request": "launch", + "name": "Vue", + "url": "http://localhost:3000", + "webRoot": "${workspaceFolder}", + "pathMappings": [ + { + "url": "webpack:///src/", + "path": "${webRoot}/" + } + ] + } + ] +} diff --git a/assets/sprite/svg/book.svg b/assets/sprite/svg/book.svg new file mode 100644 index 00000000..27304272 --- /dev/null +++ b/assets/sprite/svg/book.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/sprite/svg/link.svg b/assets/sprite/svg/link.svg new file mode 100644 index 00000000..ecca03ec --- /dev/null +++ b/assets/sprite/svg/link.svg @@ -0,0 +1,4 @@ + + + + diff --git a/components.d.ts b/components.d.ts index 0c95ff40..3683fc44 100644 --- a/components.d.ts +++ b/components.d.ts @@ -7,6 +7,7 @@ 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'] @@ -14,6 +15,7 @@ declare module 'vue' { 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'] diff --git a/components/blocks/LeaderboardInfo/Desktop/Desktop.test.ts b/components/blocks/LeaderboardInfo/Desktop/Desktop.test.ts new file mode 100644 index 00000000..b041c42f --- /dev/null +++ b/components/blocks/LeaderboardInfo/Desktop/Desktop.test.ts @@ -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('', () => { + 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%') + }) +}) diff --git a/components/blocks/LeaderboardInfo/Desktop/Desktop.vue b/components/blocks/LeaderboardInfo/Desktop/Desktop.vue new file mode 100644 index 00000000..dae18f2d --- /dev/null +++ b/components/blocks/LeaderboardInfo/Desktop/Desktop.vue @@ -0,0 +1,141 @@ + + + + + + + {{ leaderboard.name || 'Game Name' }} + + + + {{ + leaderboard.categories[0]?.name ?? 'TODO' + }} + YEAR + + + + + + + Guides + + + + Resources + + + First + Second + Third + + + + + + + + +elements/buttons/Dropdown/Dropdown.vueelements/buttons/Dropdown/DropdownItem.vue diff --git a/components/blocks/LeaderboardInfo/LeaderboardInfo.test.ts b/components/blocks/LeaderboardInfo/LeaderboardInfo.test.ts new file mode 100644 index 00000000..10549dcf --- /dev/null +++ b/components/blocks/LeaderboardInfo/LeaderboardInfo.test.ts @@ -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('', () => { + 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 if device width is large', () => { + vi.stubGlobal('innerWidth', 1980) + const wrapper = getLeaderboardInfoWrapper() + expect(wrapper.html()).toContain('Guides') + }) + + it('should render 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() + }) +}) diff --git a/components/blocks/LeaderboardInfo/LeaderboardInfo.vue b/components/blocks/LeaderboardInfo/LeaderboardInfo.vue new file mode 100644 index 00000000..a32058df --- /dev/null +++ b/components/blocks/LeaderboardInfo/LeaderboardInfo.vue @@ -0,0 +1,44 @@ + + + + + + + + diff --git a/components/blocks/LeaderboardInfo/Mobile/Mobile.test.ts b/components/blocks/LeaderboardInfo/Mobile/Mobile.test.ts new file mode 100644 index 00000000..045248ff --- /dev/null +++ b/components/blocks/LeaderboardInfo/Mobile/Mobile.test.ts @@ -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('', () => { + 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%') + }) +}) diff --git a/components/blocks/LeaderboardInfo/Mobile/Mobile.vue b/components/blocks/LeaderboardInfo/Mobile/Mobile.vue new file mode 100644 index 00000000..99c16642 --- /dev/null +++ b/components/blocks/LeaderboardInfo/Mobile/Mobile.vue @@ -0,0 +1,84 @@ + + + + + + + + {{ leaderboard.name || 'Game Name' }} + + + + + Submit Run + + + + + + {{ + leaderboard.categories[0]?.name ?? 'TODO' + }} + YEAR + + + + + + + diff --git a/components/blocks/LeaderboardInfo/PlatformTags/PlatformTags.test.ts b/components/blocks/LeaderboardInfo/PlatformTags/PlatformTags.test.ts new file mode 100644 index 00000000..2ff7c8f8 --- /dev/null +++ b/components/blocks/LeaderboardInfo/PlatformTags/PlatformTags.test.ts @@ -0,0 +1,23 @@ +import { mount } from '@vue/test-utils' +import PlatformTags from './PlatformTags.vue' + +describe('', () => { + const tags = ['XBox', 'PS4', 'Amiga DS'] + + it('should render without crashing', () => { + const wrapper = mount(PlatformTags, { + props: { + tags, + }, + }) + + expect(wrapper.isVisible()).toBe(true) + + wrapper + .getComponent('div.platform-tags') + .findAllComponents('div') + .forEach((c, i) => { + expect(c.text()).toBe(tags[i]) + }) + }) +}) diff --git a/components/blocks/LeaderboardInfo/PlatformTags/PlatformTags.vue b/components/blocks/LeaderboardInfo/PlatformTags/PlatformTags.vue new file mode 100644 index 00000000..ae069c42 --- /dev/null +++ b/components/blocks/LeaderboardInfo/PlatformTags/PlatformTags.vue @@ -0,0 +1,22 @@ + + + + + + {{ tag }} + + + + + diff --git a/components/elements/buttons/ButtonLink/ButtonLink.vue b/components/elements/buttons/ButtonLink/ButtonLink.vue index 9e93d234..e06763ac 100644 --- a/components/elements/buttons/ButtonLink/ButtonLink.vue +++ b/components/elements/buttons/ButtonLink/ButtonLink.vue @@ -1,7 +1,12 @@ - + Placeholder Link Text diff --git a/components/elements/buttons/DropDown/DropDown.test.ts b/components/elements/buttons/DropDown/DropDown.test.ts new file mode 100644 index 00000000..2f633cdd --- /dev/null +++ b/components/elements/buttons/DropDown/DropDown.test.ts @@ -0,0 +1,45 @@ +import { enableAutoUnmount, mount } from '@vue/test-utils' +import { getByTestId } from 'root/testUtils' +import Dropdown from './Dropdown.vue' +import DropdownItem from './DropdownItem.vue' +import type { ComponentMountingOptions } from '@vue/test-utils' + +enableAutoUnmount(afterEach) + +function mountDropdown(options?: ComponentMountingOptions) { + return mount(Dropdown, options) +} + +describe('', () => { + it('should render without crashing', () => { + const wrapper = mountDropdown({ + props: { className: 'test' }, + }) + expect(wrapper.isVisible()).toBe(true) + expect(getByTestId(wrapper, 'toggler').classes()).toContain('test') + }) + + describe('when the toggler is clicked', () => { + it('should render the slot item, then hide it on a second click', async () => { + const itemWrapper = mount(DropdownItem) + + const wrapper = mountDropdown({ + slots: { default: itemWrapper.html() }, + }) + + await getByTestId(wrapper, 'toggler').trigger('click') + expect(wrapper.html()).toContain(itemWrapper.html()) + + await getByTestId(wrapper, 'toggler').trigger('click') + expect(wrapper.html()).not.toContain(itemWrapper.html()) + }) + + it('should apply the style to the dropdown arrow', async () => { + const wrapper = mountDropdown() + + await getByTestId(wrapper, 'toggler').trigger('click') + + expect(getByTestId(wrapper, 'arrow').classes()).toContain('isOpen') + }) + }) +}) diff --git a/components/elements/buttons/DropDown/DropDown.vue b/components/elements/buttons/DropDown/DropDown.vue new file mode 100644 index 00000000..f416ceaa --- /dev/null +++ b/components/elements/buttons/DropDown/DropDown.vue @@ -0,0 +1,75 @@ + + + + + + + + More + + + + + + + + + + diff --git a/components/elements/buttons/DropDown/DropDownItem.test.ts b/components/elements/buttons/DropDown/DropDownItem.test.ts new file mode 100644 index 00000000..2092e148 --- /dev/null +++ b/components/elements/buttons/DropDown/DropDownItem.test.ts @@ -0,0 +1,29 @@ +import { enableAutoUnmount, shallowMount } from '@vue/test-utils' +import { getByTestId } from 'root/testUtils' +import DropdownItem from './DropdownItem.vue' +import type { ComponentMountingOptions } from '@vue/test-utils' + +enableAutoUnmount(afterEach) + +function mountDropdownItem( + options?: ComponentMountingOptions, +) { + return shallowMount(DropdownItem, options) +} + +describe('', () => { + it('should render without crashing', () => { + const wrapper = mountDropdownItem({ + props: { slots: 'test' }, + }) + expect(wrapper.isVisible()).toBe(true) + }) + + it("should trigger the click event when it's, well, clicked on", async () => { + const wrapper = mountDropdownItem({}) + + await getByTestId(wrapper, 'container').trigger('click') + + expect(wrapper.emitted().click).toBeTruthy() + }) +}) diff --git a/components/elements/buttons/DropDown/DropDownItem.vue b/components/elements/buttons/DropDown/DropDownItem.vue new file mode 100644 index 00000000..247ec19a --- /dev/null +++ b/components/elements/buttons/DropDown/DropDownItem.vue @@ -0,0 +1,31 @@ + + + + + + + + + diff --git a/components/elements/buttons/FollowButton/FollowButton.vue b/components/elements/buttons/FollowButton/FollowButton.vue index eed25169..9bfda74f 100644 --- a/components/elements/buttons/FollowButton/FollowButton.vue +++ b/components/elements/buttons/FollowButton/FollowButton.vue @@ -1,19 +1,15 @@ Follow diff --git a/components/elements/buttons/SocialButtons/SocialButtons.vue b/components/elements/buttons/SocialButtons/SocialButtons.vue index bb249d54..7eba33f4 100644 --- a/components/elements/buttons/SocialButtons/SocialButtons.vue +++ b/components/elements/buttons/SocialButtons/SocialButtons.vue @@ -22,21 +22,15 @@ defineProps() diff --git a/composables/api/useGetLeaderboardDetail/index.ts b/composables/api/useGetLeaderboardDetail/index.ts index ef1dd406..124c2de2 100644 --- a/composables/api/useGetLeaderboardDetail/index.ts +++ b/composables/api/useGetLeaderboardDetail/index.ts @@ -1,6 +1,7 @@ import { ref } from 'vue' -import { ApiResponse, optionalParameters, useApi } from 'composables/useApi' +import { useApi } from 'composables/useApi' import { Leaderboards } from 'lib/api/Leaderboards' +import type { ApiResponse, optionalParameters } from 'composables/useApi' import type { LeaderboardViewModel } from 'lib/api/data-contracts' export async function useGetLeaderboardDetail( diff --git a/composables/useApi/index.ts b/composables/useApi/index.ts index 6607764e..7c0dd341 100644 --- a/composables/useApi/index.ts +++ b/composables/useApi/index.ts @@ -9,6 +9,7 @@ import type { HttpResponse } from 'lib/api/http-client' /** * @property {T} [data] * @property {ProblemDetails | null} error + * @property {ValidationProblemDetails | null} errors * @property {boolean} loading */ export interface ApiResponse { diff --git a/lib/api/Account.ts b/lib/api/Account.ts index 8568d7f3..630ba8fa 100644 --- a/lib/api/Account.ts +++ b/lib/api/Account.ts @@ -9,7 +9,7 @@ * --------------------------------------------------------------- */ -import { +import type { ChangePasswordRequest, LoginRequest, LoginResponse, @@ -19,7 +19,8 @@ import { UserViewModel, ValidationProblemDetails, } from './data-contracts' -import { ContentType, HttpClient, RequestParams } from './http-client' +import type { RequestParams } from './http-client' +import { ContentType, HttpClient } from './http-client' export class Account< SecurityDataType = unknown, diff --git a/lib/api/AccountRoute.ts b/lib/api/AccountRoute.ts index f27622b8..1f2f3c21 100644 --- a/lib/api/AccountRoute.ts +++ b/lib/api/AccountRoute.ts @@ -9,7 +9,7 @@ * --------------------------------------------------------------- */ -import { +import type { ChangePasswordRequest, LoginRequest, LoginResponse, diff --git a/lib/api/Categories.ts b/lib/api/Categories.ts index fdbead3c..47f00144 100644 --- a/lib/api/Categories.ts +++ b/lib/api/Categories.ts @@ -9,13 +9,14 @@ * --------------------------------------------------------------- */ -import { +import type { CategoryViewModel, CreateCategoryRequest, ProblemDetails, ValidationProblemDetails, } from './data-contracts' -import { ContentType, HttpClient, RequestParams } from './http-client' +import type { RequestParams } from './http-client' +import { ContentType, HttpClient } from './http-client' export class Categories< SecurityDataType = unknown, diff --git a/lib/api/CategoriesRoute.ts b/lib/api/CategoriesRoute.ts index 5129b183..0f49fd56 100644 --- a/lib/api/CategoriesRoute.ts +++ b/lib/api/CategoriesRoute.ts @@ -9,7 +9,7 @@ * --------------------------------------------------------------- */ -import { CategoryViewModel, CreateCategoryRequest } from './data-contracts' +import type { CategoryViewModel, CreateCategoryRequest } from './data-contracts' export namespace Categories { /** diff --git a/lib/api/Leaderboards.ts b/lib/api/Leaderboards.ts index 9474c30c..d007ff94 100644 --- a/lib/api/Leaderboards.ts +++ b/lib/api/Leaderboards.ts @@ -9,14 +9,15 @@ * --------------------------------------------------------------- */ -import { +import type { CreateLeaderboardRequest, LeaderboardViewModel, LeaderboardsListParams, ProblemDetails, ValidationProblemDetails, } from './data-contracts' -import { ContentType, HttpClient, RequestParams } from './http-client' +import type { RequestParams } from './http-client' +import { ContentType, HttpClient } from './http-client' export class Leaderboards< SecurityDataType = unknown, diff --git a/lib/api/LeaderboardsRoute.ts b/lib/api/LeaderboardsRoute.ts index f9b9e4cd..9b043987 100644 --- a/lib/api/LeaderboardsRoute.ts +++ b/lib/api/LeaderboardsRoute.ts @@ -9,7 +9,7 @@ * --------------------------------------------------------------- */ -import { +import type { CreateLeaderboardRequest, LeaderboardViewModel, } from './data-contracts' diff --git a/lib/api/Runs.ts b/lib/api/Runs.ts index 66b18c43..ef614e8b 100644 --- a/lib/api/Runs.ts +++ b/lib/api/Runs.ts @@ -9,14 +9,15 @@ * --------------------------------------------------------------- */ -import { +import type { CategoryViewModel, CreateRunRequest, ProblemDetails, RunViewModel, ValidationProblemDetails, } from './data-contracts' -import { ContentType, HttpClient, RequestParams } from './http-client' +import type { RequestParams } from './http-client' +import { ContentType, HttpClient } from './http-client' export class Runs< SecurityDataType = unknown, diff --git a/lib/api/RunsRoute.ts b/lib/api/RunsRoute.ts index 7527347e..de244e2b 100644 --- a/lib/api/RunsRoute.ts +++ b/lib/api/RunsRoute.ts @@ -9,7 +9,7 @@ * --------------------------------------------------------------- */ -import { +import type { CategoryViewModel, CreateRunRequest, RunViewModel, diff --git a/lib/api/Users.ts b/lib/api/Users.ts index 4ccf98cb..40498df8 100644 --- a/lib/api/Users.ts +++ b/lib/api/Users.ts @@ -9,8 +9,9 @@ * --------------------------------------------------------------- */ -import { ProblemDetails, UserViewModel } from './data-contracts' -import { HttpClient, RequestParams } from './http-client' +import type { ProblemDetails, UserViewModel } from './data-contracts' +import type { RequestParams } from './http-client' +import { HttpClient } from './http-client' export class Users< SecurityDataType = unknown, diff --git a/lib/api/UsersRoute.ts b/lib/api/UsersRoute.ts index e5afa2c3..c603334a 100644 --- a/lib/api/UsersRoute.ts +++ b/lib/api/UsersRoute.ts @@ -9,7 +9,7 @@ * --------------------------------------------------------------- */ -import { UserViewModel } from './data-contracts' +import type { UserViewModel } from './data-contracts' export namespace Users { /** diff --git a/pages/board/[slug].vue b/pages/board/[slug].vue index 175aad71..f6c2a2e1 100644 --- a/pages/board/[slug].vue +++ b/pages/board/[slug].vue @@ -1,4 +1,6 @@ - Leaderboard Name: {{ leaderboardDetail?.data?.name || 'ERROR' }} + + + console.log(id)" + /> + diff --git a/public/icons/arrows/down.svg b/public/icons/arrows/down.svg new file mode 100644 index 00000000..4b3d7822 --- /dev/null +++ b/public/icons/arrows/down.svg @@ -0,0 +1,10 @@ + + + diff --git a/public/icons/social/globe.svg b/public/icons/social/globe.svg new file mode 100644 index 00000000..279e597e --- /dev/null +++ b/public/icons/social/globe.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/icons/social/message.svg b/public/icons/social/message.svg new file mode 100644 index 00000000..638e1713 --- /dev/null +++ b/public/icons/social/message.svg @@ -0,0 +1,4 @@ + + + +
Leaderboard Name: {{ leaderboardDetail?.data?.name || 'ERROR' }}