-
Notifications
You must be signed in to change notification settings - Fork 0
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
Contestの詳細ページの作成 #51
Contestの詳細ページの作成 #51
Changes from 7 commits
24d3754
8f6b417
2532054
94f2e16
5d3c3ef
1e23302
927f2c8
4ef03e9
00c440a
39f6cda
e599df3
545b07a
6b1c179
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
<script lang="ts" setup> | ||
import UserIcons from '/@/components/UI/UserIcons.vue' | ||
import type { ContestTeam } from '/@/lib/apis' | ||
|
||
interface Props { | ||
contestId: string | ||
contestTeam: ContestTeam | ||
} | ||
|
||
defineProps<Props>() | ||
/* todo:サーバーからmembersが返ってくるようになったらcontestTeam.members.map(member=>member.id)を使う */ | ||
const userIds = ['sapphi_red', 'toshi00', 'tesso', 'mehm8128'] | ||
</script> | ||
|
||
<template> | ||
<router-link | ||
:to="`/contests/${contestId}/teams/${contestTeam.id}/edit`" | ||
:class="$style.link" | ||
> | ||
<div :class="$style.container"> | ||
<div> | ||
<p :class="$style.name">{{ contestTeam.name }}</p> | ||
<p :class="$style.result">{{ contestTeam.result }}</p> | ||
</div> | ||
<user-icons :user-ids="userIds" /> | ||
</div> | ||
</router-link> | ||
</template> | ||
|
||
<style lang="scss" module> | ||
.link { | ||
color: inherit; | ||
text-decoration: none; | ||
} | ||
.container { | ||
padding: 0.5rem; | ||
display: flex; | ||
align-items: center; | ||
justify-content: space-between; | ||
} | ||
.name { | ||
color: $color-primary; | ||
font-size: 1.125rem; | ||
} | ||
.result { | ||
margin-top: 2rem; | ||
} | ||
</style> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
<script lang="ts" setup> | ||
import BaseButton from '/@/components/UI/BaseButton.vue' | ||
|
||
import FormInput from '/@/components/UI/FormInput.vue' | ||
import ContestTeamItem from '/@/components/Contest/ContestTeamItem.vue' | ||
import { RouterLink } from 'vue-router' | ||
import { ContestTeam } from '/@/lib/apis' | ||
import { ref } from 'vue' | ||
|
||
interface Props { | ||
contestId: string | ||
contestTeams: ContestTeam[] | ||
} | ||
|
||
defineProps<Props>() | ||
const emit = defineEmits<{ | ||
(e: 'input', value: string): void | ||
}>() | ||
|
||
const searchQuery = ref('') | ||
</script> | ||
|
||
<template> | ||
<div> | ||
<div :class="$style.searchFormContainer"> | ||
<div :class="$style.searchForm"> | ||
<p :class="$style.searchFormDescriptionText">検索</p> | ||
<form-input | ||
:model-value="searchQuery" | ||
placeholder="チーム名" | ||
icon="magnify" | ||
@update:model-value="emit('input', $event)" | ||
/> | ||
</div> | ||
<div :class="$style.newTeamLink"> | ||
<p :class="$style.searchFormDescriptionText">チーム作成</p> | ||
<router-link | ||
:to="`/contests/${contestId}/teams/new`" | ||
:class="$style.link" | ||
> | ||
<base-button type="primary" icon="mdi:plus">New</base-button> | ||
</router-link> | ||
</div> | ||
</div> | ||
<ul :class="$style.teamList"> | ||
<li v-for="contestTeam in contestTeams" :key="contestTeam.id"> | ||
<contest-team-item | ||
:contest-id="contestId" | ||
:contest-team="contestTeam" | ||
/> | ||
</li> | ||
</ul> | ||
</div> | ||
</template> | ||
|
||
<style lang="scss" module> | ||
.searchFormContainer { | ||
display: flex; | ||
align-items: center; | ||
margin-top: 0.5rem; | ||
} | ||
.header { | ||
margin: 4rem 0 2rem; | ||
} | ||
.searchFormDescriptionText { | ||
color: $color-secondary; | ||
font-size: 0.875rem; | ||
} | ||
.searchForm { | ||
flex-grow: 1; | ||
} | ||
.newTeamLink { | ||
margin-left: 0.5rem; | ||
} | ||
.link { | ||
text-decoration: none; | ||
color: inherit; | ||
} | ||
.teamList { | ||
list-style: none; | ||
padding: 0.5rem 0; | ||
li { | ||
border: 1px solid $color-primary-text; | ||
border-radius: 8px; | ||
margin-bottom: 0.5rem; | ||
&:last-child { | ||
margin-bottom: 0; | ||
} | ||
&:hover { | ||
background-color: $color-background-dim; | ||
} | ||
} | ||
} | ||
</style> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
<script lang="ts" setup> | ||
import UserIcon from '/@/components/UI/UserIcon.vue' | ||
interface Props { | ||
userIds: string[] | ||
} | ||
|
||
defineProps<Props>() | ||
</script> | ||
|
||
<template> | ||
<div :class="$style.userIcons"> | ||
<UserIcon | ||
v-for="(userId, i) in userIds.slice(0, 3)" | ||
:key="userId" | ||
:user-id="userId" | ||
:class="$style.userIcon" | ||
:style="{ left: `${i * 16}px` }" | ||
/> | ||
<span v-if="userIds.length > 3">+{{ userIds.length - 3 }}</span> | ||
</div> | ||
</template> | ||
|
||
<style lang="scss" module> | ||
.userIcons { | ||
width: 80px; | ||
position: relative; | ||
text-align: right; | ||
} | ||
.userIcon { | ||
position: absolute; | ||
} | ||
</style> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,20 +1,118 @@ | ||
<script lang="ts" setup> | ||
import ContentHeader from '/@/components/Layout/ContentHeader.vue' | ||
import PageContainer from '/@/components/Layout/PageContainer.vue' | ||
import BaseButton from '/@/components/UI/BaseButton.vue' | ||
|
||
import apis, { ContestDetail, ContestTeam } from '/@/lib/apis' | ||
import { RouterLink } from 'vue-router' | ||
import { getDisplayDuration } from '/@/lib/date' | ||
import useParam from '/@/use/param' | ||
import useFetcher from '/@/use/fetcher' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. exportしてる関数名と違って分かりづらいので、 |
||
import ContestTeamsComponent from '/@/components/Contest/ContestTeams.vue' | ||
|
||
const contestId = useParam('id') | ||
const { data: contest } = useFetcher<ContestDetail>(contestId, () => | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. こんな感じに書いてもいいかも? fetcherState 使ってなさそうなので const contest = ref<ContestDetail>()
watchEffect(async () => {
contest.value = (await apis.getContest(contestId.value)).data
}) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fetcherStateを使うことにしました |
||
apis.getContest(contestId.value) | ||
) | ||
const { data: contestTeams } = useFetcher<ContestTeam[]>(contestId, () => | ||
apis.getContestTeams(contestId.value) | ||
) | ||
|
||
const searchContestTeams = (serachQuery: string) => { | ||
// todo: serverでやるかも | ||
contestTeams.value = | ||
contestTeams.value?.filter(contestTeam => { | ||
const regexp = new RegExp(serachQuery, 'i') | ||
return regexp.test(contestTeam.name) | ||
}) ?? [] | ||
} | ||
</script> | ||
|
||
<template> | ||
<page-container> | ||
<div>ContestDetail</div> | ||
<div :class="$style.headerContainer"> | ||
<content-header | ||
icon-name="mdi:trophy-outline" | ||
:header-texts="[ | ||
{ title: 'Contests', url: '/contests' }, | ||
{ title: contest?.name ?? '', url: `/contests/${contestId}` } | ||
]" | ||
detail="コンテストの詳細です。" | ||
:class="$style.header" | ||
/> | ||
<router-link :to="`/contests/${contestId}/edit`" :class="$style.link"> | ||
<base-button type="primary" icon="mdi:pencil">Edit</base-button> | ||
</router-link> | ||
</div> | ||
<div v-if="contest !== undefined"> | ||
<section :class="$style.section"> | ||
<h2 :class="$style.h2">コンテスト名</h2> | ||
<p :class="$style.content">{{ contest.name }}</p> | ||
</section> | ||
<section :class="$style.section"> | ||
<h2 :class="$style.h2">日時</h2> | ||
<p :class="$style.content"> | ||
{{ getDisplayDuration(contest.duration) }} | ||
</p> | ||
</section> | ||
<section :class="$style.section"> | ||
<h2 :class="$style.h2">リンク</h2> | ||
<p :class="$style.content"> | ||
<a :href="contest.link">{{ contest.link }}</a> | ||
</p> | ||
</section> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
<section :class="$style.section"> | ||
<h2 :class="$style.h2">説明</h2> | ||
<p :class="$style.content">{{ contest.description }}</p> | ||
</section> | ||
<section :class="$style.section"> | ||
<h2 :class="$style.h2">チーム</h2> | ||
<contest-teams-component | ||
v-if="contestTeams !== undefined" | ||
:class="$style.content" | ||
:contest-id="contestId" | ||
:contest-teams="contestTeams" | ||
@input="searchContestTeams($event)" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 絞り込んだあとのチームを表示する責任があるのは、
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. サーバーで検索機能実装してもらえたら There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. そしたら、 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ほんとですね... |
||
/> | ||
</section> | ||
</div> | ||
<router-link to="/contests" :class="$style.link"> | ||
<base-button | ||
:class="$style.backButton" | ||
type="secondary" | ||
icon="mdi:arrow-left" | ||
> | ||
Back | ||
</base-button> | ||
</router-link> | ||
</page-container> | ||
</template> | ||
|
||
<script lang="ts"> | ||
import { defineComponent } from 'vue' | ||
import PageContainer from '../components/Layout/PageContainer.vue' | ||
|
||
export default defineComponent({ | ||
name: 'Contest', | ||
components: { | ||
PageContainer | ||
}, | ||
setup() { | ||
return {} | ||
} | ||
}) | ||
</script> | ||
<style lang="scss" module> | ||
.headerContainer { | ||
display: flex; | ||
justify-content: space-between; | ||
align-items: center; | ||
} | ||
.header { | ||
margin: 4rem 0 2rem; | ||
} | ||
.link { | ||
text-decoration: none; | ||
color: inherit; | ||
} | ||
.section { | ||
margin-bottom: 2rem; | ||
} | ||
.h2 { | ||
font-weight: bold; | ||
font-size: 20px; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
} | ||
.content { | ||
margin-top: 0.5rem; | ||
padding-left: 0.5rem; | ||
} | ||
.backButton { | ||
margin-top: 2rem; | ||
} | ||
</style> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,39 +1,25 @@ | ||
import { ref, Ref } from 'vue' | ||
import { AxiosResponse } from 'axios' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fetcher.tsは traPortfolio-UIから移植するときにコピーされたもので、Vuexの状態変化にしか使ってなさそうなので、元々の Dashboard では、こいつを fetcher という名前で 運用しても悪くない感じがした。 |
||
import { ref, Ref, watchEffect } from 'vue' | ||
|
||
export type FetcherState = 'loading' | 'loaded' | 'error' | ||
|
||
/** | ||
* @param value この値がnullでなくなったときに正常に取得できたとする | ||
* @param fetchFunc 返り値がTである必要はないが基本的には一致する必要があるので指定してる。 | ||
* もし一致しない場合で利用する場合はas unknown as Tすること | ||
*/ | ||
const useFetcher = <T>( | ||
value: Readonly<Ref<T | null>>, | ||
fetchFunc: () => Promise<T> | ||
): { fetcherState: Ref<FetcherState> } => { | ||
const isLoadedBeforeInit = value.value !== null | ||
const state = ref<FetcherState>('loaded') | ||
|
||
if (isLoadedBeforeInit) { | ||
// 取得済み | ||
return { fetcherState: state } | ||
} | ||
|
||
// キャッシュがなかったので取得する | ||
;(async () => { | ||
state.value = 'loading' | ||
const useDataFetcher = <T>( | ||
id: Ref<string>, | ||
fetch: (id: string) => Promise<AxiosResponse<T>> | ||
): { data: Ref<T | undefined>; fetcherState: Ref<FetcherState> } => { | ||
const data = ref<T>() | ||
const state = ref<FetcherState>('loading') | ||
watchEffect(async () => { | ||
try { | ||
await fetchFunc() | ||
data.value = (await fetch(id.value)).data | ||
state.value = 'loaded' | ||
} catch (e) { | ||
state.value = 'error' | ||
|
||
// eslint-disable-next-line no-console | ||
console.error(e) | ||
} | ||
})() | ||
|
||
return { fetcherState: state } | ||
}) | ||
return { data: data, fetcherState: state } | ||
} | ||
|
||
export default useFetcher | ||
export default useDataFetcher |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
使ってなさそう