Skip to content
This repository has been archived by the owner on Feb 5, 2024. It is now read-only.

Commit

Permalink
Merge pull request #26 from gedorinku/contest-page
Browse files Browse the repository at this point in the history
[WIP]Contest page
  • Loading branch information
gedorinku authored May 14, 2018
2 parents deb426a + 42394ce commit 48d4d0d
Show file tree
Hide file tree
Showing 11 changed files with 608 additions and 7 deletions.
1 change: 1 addition & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
<script defer src="https://use.fontawesome.com/releases/v5.0.7/js/all.js"></script>
</html>
16 changes: 16 additions & 0 deletions src/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,22 @@ export default {
const res = await axios.get(`${API_ENDPOINT}/contests/${contestID}`, getConfig(sessionID));
return res;
},
async getContestProblems(sessionID, contestID) {
const res = await axios.get(`${API_ENDPOINT}/contests/${contestID}/problems`, getConfig(sessionID));
return res;
},
async getContestStatuses(sessionID, contestID) {
const res = await axios.get(`${API_ENDPOINT}/contests/${contestID}/statuses`, getConfig(sessionID));
return res;
},
async getContestStandings(sessionID, contestID) {
const res = await axios.get(`${API_ENDPOINT}/contests/${contestID}/standings`, getConfig(sessionID));
return res;
},
async enterContest(sessionID, contestID) {
const res = await axios.post(`${API_ENDPOINT}/contests/${contestID}/enter`, {}, getConfig(sessionID));
return res;
},
async updateContest(sessionID, contest) {
const res = await axios.put(`${API_ENDPOINT}/contests/${contest.id}`, contest, getConfig(sessionID));
return res;
Expand Down
31 changes: 31 additions & 0 deletions src/components/common/Modal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<template>
<div class="modal" :class="{ 'is-active': isActive }">
<div class="modal-background" @click="$emit('close')"></div>
<div class="modal-card" :class="{wide: isWide}">
<header class="modal-card-head">
<p class="modal-card-title">{{title}}</p>
<button class="delete" aria-label="close" @click="$emit('close')"></button>
</header>
<section class="modal-card-body">
<slot></slot>
</section>
<footer class="modal-card-foot"></footer>
</div>
</div>
</template>

<script>
export default {
name: 'modal',
props: {
title: String,
isActive: Boolean,
isWide: Boolean,
},
};
</script>
<style scoped>
.wide{
width: 90%;
}
</style>
18 changes: 18 additions & 0 deletions src/components/common/Problem.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<template>
<div>
<h1 class="title underline"><a href="#">#1</a>進捗どうですか</h1>
</div>
</template>

<script>
export default {
name: 'Problem',
};
</script>

<style scoped>
.underline{
padding-bottom:10px;
border-bottom: 1px solid gray;
}
</style>
301 changes: 300 additions & 1 deletion src/components/contest/Contest.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,309 @@
<template>
<h1>Contest Page だよ</h1>
<div>
<div class="container margin-bottom">
<section class="hero is-small is-light">
<div class="hero-body">
<nav class="navbar">
<div class="navbar-brand">
<div>
<h1 class="title">{{title}}</h1>
<div class="subtitle is-6">
<span class="is-size-5 has-text-weight-bold">
期間: {{formatDate(startAt)}} ~ {{formatDate(endAt)}}<br>
</span>
<span class="is-size-7">
作成日時: {{formatDate(createdAt)}}<br>
最終更新: {{formatDate(updatedAt)}}
</span>
</div>
</div>
</div>
<div class="navbar-end">
<span v-if="canEnter && !isEntered && problems === null" class="navbar-item">
<button @click="enter" class="button is-large is-outlined">
コンテストに参加する
</button>
</span>
<span v-else class="navbar-item">
<button
@click="showSubmitList"
class="button is-outlined"
:disabled="problems === null || problems.length === 0"
>
提出一覧
</button>
<button
@click="showStandings"
class="button is-outlined"
:disabled="problems === null || problems.length === 0"
>
順位
</button>
</span>
</div>
</nav>
</div>
</section>
<article class="message is-light">
<div class="message-header">
<button class="button is-text is-small" @click="toggleDescription">
{{ showDescription ? "閉じる" : "詳細" }}
</button>
</div>
<div class="message-body content" v-show="showDescription">
<h3 class="is-size-6">作問者</h3>
<ul>
<li v-for="(writer, index) in writers" :key="index">{{writer.displayName}}</li>
</ul>
<p>{{description}}</p>
</div>
</article>
</div>
<div class="container">
<ErrorNotification :error="error"/>
<template v-if="problems !== null">
<div class="columns is-mobile" v-if="problems.length !== 0">
<div class="column is-1 tab">
<aside>
<ul class="menu-list">
<li v-for="(problem, index) in problems" :key="index">
<a
:href="'#' + num2alpha(index)"
:class="['tab-button', index == activeTab ? 'active-tab-button': '']"
@click="activeTab = index"
>
{{ num2alpha(index).toUpperCase() }}
<tag :status="problem.status"/>
</a>
</li>
</ul>
</aside>
</div>
<div class="column">
<Problem :problem="problems[activeTab]"/>
</div>
<Modal
:isActive="showStandingsModal"
@close="showStandingsModal = false"
isWide
title="順位"
>
<table class="table is-fullwidth fixed-table">
<thead>
<tr>
<th>順位</th>
<th>名前</th>
<th v-for="(_, i) in problems" :key="i">
{{num2alpha(i).toUpperCase()}}
</th>
<th>合計</th>
</tr>
</thead>
<tbody>
<tr v-for="(column, i) in standings" :key="i">
<td>{{i + 1}}</td>
<td>{{column.displayName}}</td>
<td
v-for="(detail, j) in column.details"
:key="j"
v-if="detail"
:class="{'is-success': detail.accepted}"
>
{{detail.point}}<br>
<span v-if="detail.accepted">
{{getElapsedTime(detail.updatedAt)}}<br>
</span>
<span class="has-text-danger" v-if="detail.wrongCount !== 0">
(-{{detail.wrongCount}})
</span>
</td>
<td v-else>-</td>
<td>{{column.totalPoint}}</td>
</tr>
</tbody>
</table>
</Modal>
<Modal :isActive="showSubmitListModal" @close="showSubmitListModal = false" title="提出一覧">
<h1>bbb</h1>
</Modal>
</div>
</template>
<div v-else class="columns is-mobile">
<div class="column is-4">
<div class="tile is-ancestor">
<div class="tile is-parent">
<div class="tile is-child box content">
<p class="title">参加者一覧</p>
<ul>
<li v-for="(participant, index) in participants" :key="index">
{{participant.displayName}}
</li>
</ul>
</div>
</div>
</div>
</div>
<div class="column">
<div class="tile is-ancestor">
<div class="tile is-parent">
<div class="tile is-child box">
<p class="title has-text-centered">
<span v-if="countDownTimer == 'Already started'">コンテスト開催中</span>
<span v-else>コンテスト開催まで {{countDownTimer}}</span>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>

<script>
import { mapActions, mapState, mapGetters, mapMutations } from 'vuex';
import moment from 'moment';
import Problem from '@/components/problem/Problem';
import ErrorNotification from '@/components/common/ErrorNotification';
import Modal from '@/components/common/Modal';
import Tag from './Tag';
export default {
name: 'Contest',
data() {
return {
showDescription: false,
showStandingsModal: false,
showSubmitListModal: false,
activeTab: 0,
diff: 300000,
};
},
computed: {
...mapState('koneko/contests', [
'createdAt',
'updatedAt',
'title',
'description',
'startAt',
'endAt',
'writers',
'problems',
'standings',
'participants',
'id',
'error',
]),
...mapState('koneko/timeDiff', [
'timeDiff',
]),
...mapGetters('koneko/contests', [
'canEnter',
'isEntered',
]),
countDownTimer() {
if (this.diff < 0) return 'Already started';
const DD = `00${Math.floor(this.diff / 1000 / 60 / 60 / 24)}`.slice(-2);
const HH = `00${Math.floor(this.diff / 1000 / 60 / 60) % 24}`.slice(-2);
const mm = `00${Math.floor(this.diff / 1000 / 60) % 60}`.slice(-2);
const ss = `00${Math.floor(this.diff / 1000) % 60}`.slice(-2);
return `${DD}${HH}時間${mm}${ss}`;
},
},
async created() {
this.activeTab = this.$route.hash ? this.$route.hash.charCodeAt(1) - 97 : 0;
await this.getContest(this.$route.params.id);
if (this.id === null) this.$router.push({ name: 'NotFound' });
this.statusesWatcher();
const intervalId = setInterval(() => {
const serverTime = moment().add(this.timeDiff);
this.diff = moment(this.startAt).diff(serverTime);
if (this.problems !== null) {
clearInterval(intervalId);
return false;
}
if (this.diff <= 0 && this.isEntered) {
this.getProblems();
clearInterval(intervalId);
return false;
}
return false;
}, 1000);
},
beforeDestroy() {
this.setRequiredWatching(false);
},
methods: {
...mapActions('koneko/contests', [
'getContest',
'statusesWatcher',
'updateContest',
'getProblems',
'getStandings',
'enter',
]),
...mapMutations('koneko/contests', [
'setRequiredWatching',
]),
toggleDescription() {
this.showDescription = !this.showDescription;
},
showSubmitList() {
this.showSubmitListModal = true;
},
showStandings() {
this.showStandingsModal = true;
this.getStandings();
},
num2alpha(num) {
return String.fromCharCode(97 + num);
},
formatDate(date, format) {
return moment(date)
.locale('ja')
.format(format || 'YYYY/MM/DD(ddd) HH:mm')
;
},
getElapsedTime(data) {
const diff = moment(data).diff(this.startAt);
const mm = `${Math.floor(diff / 1000 / 60)}`;
const ss = `00${Math.floor(diff / 1000) % 60}`.slice(-2);
return `${mm}:${ss}`;
},
},
components: {
Tag,
Modal,
Problem,
ErrorNotification,
},
};
</script>

<style scoped>
.tab {
padding-right: 0;
border-right: 5px solid hsl(204, 86%, 53%);
min-width: 100px;
}
.tab-button{
border: 1px solid silver;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
background-color: white;
}
.active-tab-button{
border-right-color: hsl(204, 86%, 53%);
background-color: hsl(204, 86%, 53%);
}
.active-tab-button:hover{
background-color: hsl(204, 86%, 53%);
}
.margin-bottom{
margin-bottom: 20px;
}
.fixed-table{
table-layout: fixed;
}
</style>
Loading

0 comments on commit 48d4d0d

Please sign in to comment.