Commit e826de37 authored by Johan Vervloet's avatar Johan Vervloet

Merge branch 'develop' into 'master'

Let's release 0.6.

See merge request !89
parents ea97b043 39868bfb
Pipeline #190095537 passed with stages
in 7 minutes and 22 seconds
......@@ -35,5 +35,7 @@ MERCURE_PUBLISH_URL=http://mercure:3000/.well-known/mercure
MERCURE_JWT_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOltdfX0.FFSjymJCGRDWrmAmPJDoVGuYwnx5FRTjRFkkYfvLkUg
###< symfony/mercure-bundle ###
# url pattern for video call. %s will be substituted by table-ID.
VIDEO_CALL_URL=https://praatbox.be?kamer=wdebelek_%s
# you can put the app in maintenance mode by setting this to true in .env.local:
MAINTENANCE=false
......@@ -43,7 +43,7 @@ install php dev dependencies:
install javascript dependencies:
stage: build
image: node:8
image: node:10
script:
- yarn config set cache-folder .yarn
# I don't know what's the --ignore-engines thing 😕
......@@ -219,7 +219,7 @@ deploy to staging:
- echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
- chmod 644 ~/.ssh/known_hosts
- rsync -pave ssh --checksum --delete --exclude-from=.rsyncignore ./ "$SSH_USER_SERVER:$SERVER_STAGING_DIR/"
# - ssh "$SSH_USER_SERVER" "cd $SERVER_TARGET_DIR && ./bin/console cache:clear && make testdata"
# - ssh "$SSH_USER_SERVER" "cd $SERVER_TARGET_DIR && make replay"
- ssh "$SSH_USER_SERVER" "cd $SERVER_STAGING_DIR && ./bin/console cache:clear"
- ssh "$SSH_USER_SERVER" "cd $SERVER_STAGING_DIR && make replay"
# Changelog
## [0.6.1] - 2020-08-15
## [0.6] - 2020-09-15
### Added
- Indicating the card game you'll be playing at your table, #110.
- Provide links to a video chat per table, #92.
- Publishing an invitation, #108.
- Accept published invitations from front page, #109.
- (Translated) about page, #123.
### Changed
- Show player name and card game on invitation page, #38.
- Use Symfony 5.1, #115
- If you know the table secret, you can still manage the table after you left, #114.
- Nice error message when your invitation is already gone, #87.
## [0.5.1] - 2020-08-15
### Changed
- Bugfix (workaround) voting own card as winner on phone/tablet, #82.
- Bugfix for problem can only reveil cards once, #113.
- Bugfix for problem can only reveal cards once, #113.
## [0.5] - 2020-08-07
......
......@@ -44,17 +44,25 @@ Run the following from your project root:
```
docker run --rm --interactive --tty -v "$PWD":/app composer install
docker run -it --rm -v "$PWD":/usr/src/app -w /usr/src/app node:8 yarn install
docker run -it --rm -v "$PWD":/usr/src/app -w /usr/src/app node:8 yarn encore dev
docker run -it --rm -v "$PWD":/usr/src/app -w /usr/src/app node:10 yarn install
docker run -it --rm -v "$PWD":/usr/src/app -w /usr/src/app node:10 yarn encore dev
```
### Create the database
You can run the following from within your php container:
You can run the following from the outside of your php container:
```
docker-compose exec php make reset
```
### Mercure
The application will listen to http://mercure:3000 for server sent events.
So for this to work, you'll have to modify your `/etc/hosts` files so that
mercure resolves to 127.0.0.1.
### Access the application
Your application should now be at http://localhost:8080
### Run the tests
......
......@@ -10,7 +10,8 @@
"cardsOnTheTable": "show hand to everybody",
"revealTrump": "reveal trump",
"showToSpectator": "show cards to {spectator}",
"showInitial": "show initial hand to everybody"
"showInitial": "show initial hand to everybody",
"joinVideoCall": "If you haven't done so already, join the <a href='{url}' target='_blank'>video call</a> for this table."
},
"nl": {
"max_4": "Deze frontend werkt (tot nog toe) niet met meer dan 4 actieve spelers.",
......@@ -22,7 +23,8 @@
"cardsOnTheTable": "gooi kaarten op tafel",
"revealTrump": "troef blekken",
"showToSpectator": "kaarten tonen aan {spectator}",
"showInitial": "gedeelde kaarten tonen"
"showInitial": "gedeelde kaarten tonen",
"joinVideoCall": "Als u dat nog niet gedaan zou hebben, neem dan deel aan het <a href='{url}' target='_blank'>videogesprek</a> voor deze tafel."
}
}
</i18n>
......@@ -33,7 +35,8 @@
<div>
<b-alert show v-if="!gameId">
{{$t('waitingForGame')}} {{gameId}}
{{$t('waitingForGame')}}
<div v-html="$t('joinVideoCall', {url: videoCallUrl})"></div>
</b-alert>
<div class="h-100" v-else>
......@@ -52,10 +55,13 @@
:game-id="gameId"
>
</trick-counter>
<b-navbar-nav class="ml-auto" v-if="!spectator">
<b-navbar-nav class="ml-auto">
<b-nav-item :href="videoCallUrl" target="_blank" right>
<i class="fa fa-video"></i>
</b-nav-item>
<!-- show cards -->
<b-nav-item-dropdown right>
<b-nav-item-dropdown right v-if="!spectator">
<template v-slot:button-content>
<i class="fa fa-eye"></i>
</template>
......@@ -74,7 +80,7 @@
</b-nav-item-dropdown>
<!-- conclude game -->
<b-nav-item-dropdown right>
<b-nav-item-dropdown right v-if="!spectator">
<template v-slot:button-content>
<span :class="{blink: conclusionVoteCount}" class="conclusion-request">
<i class="fa fa-fast-forward"></i>
......@@ -253,6 +259,7 @@
import helper from "../helper";
import revealedCardsApi from "../api/revealedCardsApi";
import teamsApi from "../api/teamsApi";
import routing from "../routing";
export default {
name: "CardMat",
......@@ -529,6 +536,13 @@
return pl;
}
)
},
videoCallUrl() {
return routing.getRoute(
'wdebelek.chat', {
tableId: this.tableId
}
)
}
},
mounted() {
......
<i18n>
{
"en": {
"players_at_table": "Your table",
"invite": "Invite player",
"name": "Name",
"status": "Status",
"invitation": "Invitation",
"unknown": "unknown",
"accepted": "ready to play",
"share": "share",
"share_title": "Wanna play cards?",
"share_text": "You are invited to play cards! #wdebelek",
"invitation_info": "You created invitations. Please share the invitation links to your fellow players.",
"dealer": "dealer",
"selectInitialDealer": "Dealer?",
"prepareGame": "Start game",
"goToTable": "To your table!",
"fiftyTwoCards": "52 cards",
"thirtyTwoCards": "32 cards"
},
"nl": {
"players_at_table": "Uw tafel",
"invite": "Speler uitnodigen",
"name": "Naam",
"status": "Status",
"invitation": "Uitnodiging",
"unknown": "uitgenodigd",
"accepted": "klaar om te spelen",
"share": "delen",
"share_title": "Kaartje leggen?",
"share_text": "U bent uitgenodigd om mee te kaarten! #wdebelek",
"invitation_info": "U maakte uitnodigingen aan. Bezorg de uitnodigingslinks aan uw medespelers.",
"dealer": "deler",
"selectInitialDealer": "Wie deelt?",
"prepareGame": "Spel starten",
"goToTable": "Kaarten!",
"fiftyTwoCards": "52 kaarten",
"thirtyTwoCards": "32 kaarten"
}
"players_at_table": "Your table",
"invite": "Invite player",
"name": "Name",
"status": "Status",
"invitation": "Invitation",
"unknown": "unknown",
"accepted": "ready to play",
"share": "share",
"share_title": "Wanna play cards?",
"share_text": "You are invited to play cards! #wdebelek",
"invitation_info": "You created invitations. For private invitations, you should share the invitation links to your fellow players. Public invitations will be announced on the home page.",
"dealer": "dealer",
"selectInitialDealer": "Dealer?",
"prepareGame": "Start game",
"goToTable": "To your table!",
"fiftyTwoCards": "52 cards",
"thirtyTwoCards": "32 cards",
"game_name": "Which game are you going to play?",
"game_name_info": "This will be shown on the invitations for your fellow players.",
"video_call": "Video call",
"privacy": "Privacy",
"private": "private",
"public": "public"
},
"nl": {
"players_at_table": "Uw tafel",
"invite": "Speler uitnodigen",
"name": "Naam",
"status": "Status",
"invitation": "Uitnodiging",
"unknown": "uitgenodigd",
"accepted": "klaar om te spelen",
"share": "delen",
"share_title": "Kaartje leggen?",
"share_text": "U bent uitgenodigd om mee te kaarten! #wdebelek",
"invitation_info": "U maakte uitnodigingen aan. Bezorg de uitnodigingslinks van privéuitnodigingen aan uw medespelers. Publieke uitnodigingen worden aangekondigd op de homepagina.",
"dealer": "deler",
"selectInitialDealer": "Wie deelt?",
"prepareGame": "Spel starten",
"goToTable": "Kaarten!",
"fiftyTwoCards": "52 kaarten",
"thirtyTwoCards": "32 kaarten",
"game_name": "Welk kaartspel wilt u spelen?",
"game_name_info": "Dit zal getoond worden in de uitnodigingen voor uw medespelers.",
"video_call": "Videogesprek",
"privacy": "Privacy",
"private": "privé",
"public": "openbaar"
}
}
</i18n>
......@@ -48,16 +60,32 @@
<h1>{{$t("players_at_table")}}</h1>
</div>
</div>
<div class="row">
<div class="col">
<label for="input-game-name">{{ $t('game_name') }}</label>
<b-form-input
id="input-game-name"
v-model="gameName"
aria-describedby="input-game-name-help"
maxLength="30"
@blur="gameNameChanged"
:state="gameNameState"
></b-form-input>
<b-form-text id="input-game-name-help">{{$t("game_name_info")}}</b-form-text>
<br />
</div>
</div>
<div class="row">
<div class="col">
<b-button
<b-button
v-if="seats.length < 6"
@click="invitePlayer"
>{{ $t('invite') }}</b-button>
>{{ $t('invite') }}</b-button>
<b-button v-if="gameStarted" variant="primary" :href="playerUrl" target="_blank">
<b-button :href="videoCallUrl" target="_blank">{{ $t('video_call') }}</b-button>
<b-button v-if="gameStarted" variant="primary" :href="playerUrl" target="_blank">
{{$t('goToTable')}}
</b-button>
</b-button>
</div>
</div>
<div class="row">
......@@ -68,27 +96,38 @@
:fields="playerFields"
primary-key="seat"
>
<template v-slot:cell(invitationUrl)="data">
<span v-if="data.value.trim()">
<div class="input-group">
<input
readonly="readonly"
type="text"
class="form-control"
:value="data.value.trim()"
:name="'invitation_' + data.index"
/>
<b-button v-if="canShare()" @click="share(data.value.trim())"><i class="fa fa-share"></i></b-button>
</div>
</span>
<span class="text-success" v-else>
({{$t('accepted')}})
</span>
</template>
<template v-slot:cell(playerName)="data">
{{ data.value ? data.value : ($t('unknown')) }}
<button class="btn" v-if="data.value" @click="kickPlayer(data.item.playerIdentifier)">🗑</button>
</template>
<template v-slot:cell(invitationUrl)="data">
<span v-if="data.value.trim()">
<div class="input-group">
<input
readonly="readonly"
type="text"
class="form-control"
:value="data.value.trim()"
:name="'invitation_' + data.index"
/>
<b-button v-if="canShare()" @click="share(data.value.trim())"><i class="fa fa-share"></i></b-button>
</div>
</span>
<span class="text-success" v-else>
({{$t('accepted')}})
</span>
</template>
<template v-slot:cell(playerName)="data">
{{ data.value ? data.value : ($t('unknown')) }}
<button class="btn" v-if="data.value" @click="kickPlayer(data.item.playerIdentifier)">🗑</button>
</template>
<template v-slot:cell(invitationPrivacy)="data">
<!-- InvitationUrl is cleared when the invitation is accepted, invitation itself isn't.
So check for invitationUrl. -->
<b-form-select
v-if="data.item.invitationUrl"
v-model="data.value"
:options="privacyOptions"
@change="invitationPrivacyChanged(data.value, data.item)"
></b-form-select>
</template>
</b-table>
</div>
</div>
......@@ -120,6 +159,7 @@
<script>
import organizerApi from "../api/organizerApi";
import tableApi from "../api/tableApi";
import invitationApi from "../api/invitationApi";
import routing from "../routing";
export default {
......@@ -149,35 +189,51 @@
data() {
this.$i18n.locale = this.lang;
return {
seats: [],
newInvitation: '',
gameStarted: false,
dealer: null,
numberOfCards: 52
seats: [],
newInvitation: '',
gameStarted: false,
dealer: null,
numberOfCards: 52,
gameName: '',
privacyOptions: [
{ value: 'private', text: this.$t('private') },
{ value: 'public', text: this.$t('public') }
]
}
},
computed: {
playerFields() {
playerFields() {
return [
{key: 'playerName', label: this.$t('name')},
{key: 'invitationUrl', label: this.$t('invitation')}
{key: 'playerName', label: this.$t('name')},
{key: 'invitationUrl', label: this.$t('invitation')},
{key: 'invitationPrivacy', label: this.$t('privacy')}
]
},
hasPendingInvitations() {
},
hasPendingInvitations() {
return this.seats.some(seat => seat.invitationUrl.trim());
},
playerOptions() {
},
playerOptions() {
return this.seats.filter(seat => seat.playerIdentifier).map(function (player) {
return {value: player.playerIdentifier, text: player.playerName};
})
},
playerUrl() {
},
playerUrl() {
return routing.getRoute('wdebelek.player', {
tableId: this.tableId,
playerSecret: this.playerSecret,
_locale: this.lang
});
}
},
gameNameState() {
return this.gameName.length < 30 ? null : false;
},
videoCallUrl() {
return routing.getRoute(
'wdebelek.chat', {
tableId: this.tableId
}
)
}
},
methods: {
// Thank you stackoverflow :-)
......@@ -194,7 +250,8 @@
},
invitePlayer() {
let self = this;
organizerApi.invitePlayer(this.tableSecret, this.newInvitation, this.playerSecret).then(result => {
// FIXME: react on PlayerInvited event instead of refreshing instantly.
invitationApi.invitePlayer(this.tableSecret, this.newInvitation, this.playerSecret, this.lang).then(result => {
self.newInvitation = self.generateUuid();
// Explicit refresh, because there will not be a server sent event if the table is not suspended,
// i.e. when the deleted player wasn't playing.
......@@ -238,8 +295,29 @@
)
},
kickPlayer(id) {
organizerApi.kickPlayer(this.tableSecret, id, this.playerSecret).then(this.refreshSeats)
}
organizerApi.kickPlayer(this.tableSecret, id, this.playerSecret)
},
gameNameChanged() {
organizerApi.chooseGame(this.tableSecret, this.playerSecret, this.gameName)
},
invitationPrivacyChanged(newPrivacy, item) {
switch (newPrivacy) {
case 'public':
invitationApi.publishInvitation(
this.tableSecret,
item.invitation,
this.playerSecret
);
break;
case 'private':
invitationApi.unpublishInvitation(
this.tableSecret,
item.invitation,
this.playerSecret
);
break;
}
}
},
mounted() {
this.newInvitation = this.generateUuid();
......@@ -259,10 +337,15 @@
const es = new EventSource(u);
es.onmessage = e => {
console.log('data: ');
console.log(JSON.parse(e.data));
this.refreshSeats();
this.getGameState()
let ev = JSON.parse(e.data);
if (
ev.eventType === "App\\Domain\\WriteModel\\Table\\Event\\InvitationAccepted"
|| ev.eventType === "App\\Domain\\WriteModel\\Table\\Event\\PlayerKicked"
) {
this.refreshSeats();
this.getGameState()
}
}
}
)
......
<i18n>
{
"en": {
"about": "Event sourced card playing",
"published_invitations": "Join an existing table?",
"published_invitations_info": "These people are looking for fellow players at their tables:",
"card_game": "Card game",
"host_name": "Table host",
"publication_date": "Waiting since",
"language": "Language",
"video_call": "Video call",
"join": "Join table",
"empty_table": "Start with an empty table?",
"enter_name": "Enter your name",
"go_to_empty_table": "Go to empty table"
},
"nl": {
"about": "Event sourced kaarttafel",
"published_invitations": "Aansluiten bij een bestaande tafel?",
"published_invitations_info": "Deze mensen zoeken nog extra kaarters:",
"card_game": "Kaartspel",
"host_name": "Gastheer/-vrouw",
"publication_date": "Tijd",
"language": "Taal",
"video_call": "Videogesprek",
"join": "Meekaarten",
"empty_table": "Beginnen met een lege tafel?",
"enter_name": "Uw naam a.u.b.",
"go_to_empty_table": "Naar een lege tafel"
}
}
</i18n>
<template>
<div>
<div class="row">
<div class="col">
<h1>WDEBELEK 🍻</h1>
<h2>{{$t('about')}}</h2>
</div>
</div>
<div class="row">
<div class="col">
<!--
This is a hack to get the url in the footer text to link to the about
page in the correct language.
--->
<b-button-group>
<b-button :href="getEnglishPage()" :pressed="lang==='en'">English</b-button>
<b-button :href="getDutchPage()" :pressed="lang==='nl'">Nederlands</b-button>
</b-button-group>
</div>
</div>
<div v-if="publishedInvitations.length">
<div class="row">
<div class="col">
<h3>{{$t('published_invitations')}}</h3>
<p>{{$t('published_invitations_info')}}</p>
<b-table
responsive