Merge pull request #44 from KevinMidboe/feat/personal-highscore
Feat/personal highscore
This commit is contained in:
		| @@ -77,11 +77,13 @@ const byEpochDate = (req, res) => { | |||||||
|     .then(highscore => getHighscoreByDates(highscore)) |     .then(highscore => getHighscoreByDates(highscore)) | ||||||
|     .then(async (lotteries) => { |     .then(async (lotteries) => { | ||||||
|       const lottery = lotteries[date]; |       const lottery = lotteries[date]; | ||||||
|  |       let highscoreWithResolvedWines = await resolveWineReferences(lottery) | ||||||
|  |       highscoreWithResolvedWines = highscoreWithResolvedWines.reverse() | ||||||
|  |  | ||||||
|       if (lottery != null) { |       if (lottery != null) { | ||||||
|         return res.send({ |         return res.send({ | ||||||
|           message: `Lottery for date: ${dateString}`, |           message: `Lottery for date: ${dateString}`, | ||||||
|           lottery: await resolveWineReferences(lottery) |           lottery: highscoreWithResolvedWines | ||||||
|         }) |         }) | ||||||
|       } else { |       } else { | ||||||
|         return res.status(404).send({ |         return res.status(404).send({ | ||||||
| @@ -93,15 +95,18 @@ const byEpochDate = (req, res) => { | |||||||
|  |  | ||||||
| const byName = (req, res) => { | const byName = (req, res) => { | ||||||
|   const { name } = req.params; |   const { name } = req.params; | ||||||
|  |   const regexName = new RegExp(name, "i"); // lowercase regex of the name | ||||||
|  |  | ||||||
|   return Highscore.find({ name }) |   return Highscore.find({ "name": { $regex : regexName } }) | ||||||
|     .then(async (highscore) => { |     .then(async (highscore) => { | ||||||
|       highscore = highscore[0] |       highscore = highscore[0] | ||||||
|       if (highscore) { |       if (highscore) { | ||||||
|         const highscoreWithResolvedWines = await resolveWineReferences(highscore.wins) |         let highscoreWithResolvedWines = await resolveWineReferences(highscore.wins) | ||||||
|  |         highscoreWithResolvedWines = highscoreWithResolvedWines.reverse() | ||||||
|  |  | ||||||
|         return res.send({ |         return res.send({ | ||||||
|           message: `Lottery winnings by name: ${name}`, |           message: `Lottery winnings by name: ${name}`, | ||||||
|  |           name: name, | ||||||
|           highscore: highscoreWithResolvedWines |           highscore: highscoreWithResolvedWines | ||||||
|          }) |          }) | ||||||
|       } else { |       } else { | ||||||
|   | |||||||
							
								
								
									
										16
									
								
								src/api.js
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								src/api.js
									
									
									
									
									
								
							| @@ -333,6 +333,19 @@ const historyAll = () => { | |||||||
|   }); |   }); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | const getWinnerByName = (name) => { | ||||||
|  |   const encodedName = encodeURIComponent(name) | ||||||
|  |   const url = new URL(`/api/lottery/by-name/${name}`, BASE_URL); | ||||||
|  |  | ||||||
|  |   return fetch(url.href).then(resp => { | ||||||
|  |     if (resp.ok) { | ||||||
|  |       return resp.json(); | ||||||
|  |     } else { | ||||||
|  |       return handleErrors(resp); | ||||||
|  |     } | ||||||
|  |   }) | ||||||
|  | } | ||||||
|  |  | ||||||
| export { | export { | ||||||
|   statistics, |   statistics, | ||||||
|   colorStatistics, |   colorStatistics, | ||||||
| @@ -364,5 +377,6 @@ export { | |||||||
|   finishedDraw, |   finishedDraw, | ||||||
|   getAmIWinner, |   getAmIWinner, | ||||||
|   postWineChosen, |   postWineChosen, | ||||||
|   historyAll |   historyAll, | ||||||
|  |   getWinnerByName | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -1,56 +1,24 @@ | |||||||
| <template> | <template> | ||||||
|   <div> |   <div class="container"> | ||||||
|     <h1 class="text-center">Vinlottis highscore 🏆🍷</h1> |     <h1>Vinlottis highscore</h1> | ||||||
|  |  | ||||||
|     <div class="content-container"> |     <div class="filter flex el-spacing"> | ||||||
|       <div class="highscore">         |       <input type="text" v-model="filterInput" placeholder="Filtrer på navn" /> | ||||||
|         <div> |       <button v-if="filterInput" @click="resetFilter" class="vin-button auto-height margin-left-sm"> | ||||||
|           <h3 >Finn ditt navn:</h3> |         Reset | ||||||
|           <input type="text" v-model="highscoreFilter" placeholder="Filtrer på navn" class="margin-bottom-sm" /> |       </button> | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
|         <ol v-if="highscore.length > 0"> |     <p class="highscore-header margin-bottom-md"><b>Plassering.</b> Navn - Antall vinn</p> | ||||||
|           <li v-for="person in highscore" :key="person" @click="selectWinner(person)"> |  | ||||||
|             {{ person.name }} - {{ person.wins.length }} |     <ol v-if="highscore.length > 0" class="highscore-list"> | ||||||
|  |       <li v-for="person in filteredResults" @click="selectWinner(person)" @keydown.enter="selectWinner(person)" tabindex="0"> | ||||||
|  |         <b>{{ person.rank }}.</b>   {{ person.name }} - {{ person.wins.length }} | ||||||
|       </li> |       </li> | ||||||
|     </ol> |     </ol> | ||||||
|  |  | ||||||
|         <div v-if="highscore.length != highscoreResponse.length" class="flex justify-center align-center"> |     <div class="center desktop-only"> | ||||||
|           <i @click="resetFilter" @keyup.space="resetFilter" |       👈 Se dine vin(n), trykk på navnet ditt | ||||||
|              role="button" aria-pressed="false" tabindex="0">reset filter</i> |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
|  |  | ||||||
|       <div class="winners-vines" v-if="selectedWinner"> |  | ||||||
|         <h1>{{ selectedWinner.name }}</h1> |  | ||||||
|  |  | ||||||
|         <h2>Vinnende farger:</h2> |  | ||||||
|         <div class="winning-colors"> |  | ||||||
|           <div v-for="win in selectedWinner.wins" |  | ||||||
|                class="color-box" :class="win.color" |  | ||||||
|                :style="{ transform: 'rotate(' + getRotation() + 'deg)' }"> |  | ||||||
|           </div> |  | ||||||
|         </div> |  | ||||||
|  |  | ||||||
|         <h2>Flasker vunnet:</h2> |  | ||||||
|         <div v-for="win in selectedWinner.wins" class="single-win"> |  | ||||||
|           <div class="date-won"><b>{{ humanReadableDate(win.date) }}</b> - {{ daysAgo(win.date) }} dager siden</div> |  | ||||||
|           <br/> |  | ||||||
|           <div class="left"> |  | ||||||
|             <h3>Vunnet med:</h3> |  | ||||||
|              |  | ||||||
|             <div class="color-box" :class="win.color" |  | ||||||
|                   :style="{ transform: 'rotate(' + getRotation() + 'deg)' }"></div> |  | ||||||
|           </div> |  | ||||||
|           <div class="left"> |  | ||||||
|             <Wine :wine="win.wine"></Wine> |  | ||||||
|           </div> |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
|  |  | ||||||
|       <div v-else class="center"> |  | ||||||
|         <h1>👈 Se dine vin(n), trykk på navnet ditt</h1> |  | ||||||
|       </div> |  | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
| @@ -58,16 +26,15 @@ | |||||||
| <script> | <script> | ||||||
|  |  | ||||||
| import { highscoreStatistics } from "@/api"; | import { highscoreStatistics } from "@/api"; | ||||||
|  | import { humanReadableDate, daysAgo } from "@/utils"; | ||||||
| import Wine from "@/ui/Wine"; | import Wine from "@/ui/Wine"; | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   components: { Wine }, |   components: { Wine }, | ||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
|       highscoreResponse: [], |  | ||||||
|       highscore: [], |       highscore: [], | ||||||
|       highscoreFilter: '', |       filterInput: '' | ||||||
|       selectedWinner: null |  | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   async mounted() { |   async mounted() { | ||||||
| @@ -78,177 +45,128 @@ export default { | |||||||
|     response = response.filter( |     response = response.filter( | ||||||
|       person => person.name != null && person.name != "" |       person => person.name != null && person.name != "" | ||||||
|     ); |     ); | ||||||
|     this.highscoreResponse = response |     this.highscore = this.generateScoreBoard(response); | ||||||
|     this.highscore = this.highscoreResponse; |  | ||||||
|   }, |   }, | ||||||
|   watch: { |   computed: { | ||||||
|     highscoreFilter(val) { |     filteredResults() { | ||||||
|  |       let highscore = this.highscore; | ||||||
|  |       let val = this.filterInput; | ||||||
|  |  | ||||||
|       if (val.length) { |       if (val.length) { | ||||||
|         val = val.toLowerCase(); |         const nameIncludesString = (person) => person.name.toLowerCase().includes(val); | ||||||
|         this.highscore = this.highscoreResponse.filter(person => person.name.toLowerCase().includes(val)) |         highscore = highscore.filter(nameIncludesString) | ||||||
|       } else { |  | ||||||
|         this.highscore = this.highscoreResponse |  | ||||||
|       } |       } | ||||||
|  |  | ||||||
|  |       return highscore | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|  |     generateScoreBoard(highscore=this.highscore) { | ||||||
|  |       let place = 0; | ||||||
|  |       let highestWinCount = -1; | ||||||
|  |  | ||||||
|  |       return highscore.map(win => { | ||||||
|  |         const wins = win.wins.length | ||||||
|  |         if (wins != highestWinCount) { | ||||||
|  |           place += 1 | ||||||
|  |           highestWinCount = wins | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         const placeString = place.toString().padStart(2, "0"); | ||||||
|  |         win.rank = placeString; | ||||||
|  |         return win | ||||||
|  |       }) | ||||||
|  |     }, | ||||||
|     resetFilter() { |     resetFilter() { | ||||||
|       this.highscore = this.highscoreResponse; |       this.filterInput = ''; | ||||||
|       this.highscoreFilter = ''; |  | ||||||
|       document.getElementsByTagName('input')[0].focus(); |       document.getElementsByTagName('input')[0].focus(); | ||||||
|     }, |     }, | ||||||
|     humanReadableDate(date) { |  | ||||||
|       const options = { year: 'numeric', month: 'long', day: 'numeric' }; |  | ||||||
|       return new Date(date).toLocaleDateString(undefined, options); |  | ||||||
|     }, |  | ||||||
|     daysAgo(date) { |  | ||||||
|       const day = 24 * 60 * 60 * 1000; |  | ||||||
|       return Math.round(Math.abs((new Date() - new Date(date)) / day)); |  | ||||||
|     }, |  | ||||||
|     selectWinner(winner) { |     selectWinner(winner) { | ||||||
|       if (this.selectedWinner != null && this.selectedWinner["name"] == winner["name"]) { |       const path = "/highscore/" + encodeURIComponent(winner.name) | ||||||
|         this.selectedWinner = null; |       this.$router.push(path); | ||||||
|       } else { |  | ||||||
|         let newestFirst = winner.wins.sort((a, b) => a.date < b.date); |  | ||||||
|         winner.wins = newestFirst; |  | ||||||
|         this.selectedWinner = { ...winner }; |  | ||||||
|       } |  | ||||||
|     }, |     }, | ||||||
|     getRotation: function() { |     humanReadableDate: humanReadableDate, | ||||||
|       let num = Math.floor(Math.random() * 12.5); |     daysAgo: daysAgo | ||||||
|       let neg = Math.floor(Math.random() * 2); |  | ||||||
|       return neg == 0 ? -num : num; |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
| }; | }; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| @import "../styles/media-queries.scss"; | @import "./src/styles/media-queries.scss"; | ||||||
| @import "../styles/variables.scss"; | @import "./src/styles/variables.scss"; | ||||||
|  | $elementSpacing: 3.5rem; | ||||||
|  |  | ||||||
|  | .el-spacing { | ||||||
|  |   margin-bottom: $elementSpacing; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .container { | ||||||
|  |   width: 90vw; | ||||||
|  |   margin: 3rem auto; | ||||||
|  |   max-width: 1200px; | ||||||
|  |   margin-bottom: 0; | ||||||
|  |   padding-bottom: 3rem; | ||||||
|  |  | ||||||
|  |   @include desktop { | ||||||
|  |     width: 80vw; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
| h1 { | h1 { | ||||||
|   text-align: center; |   font-size: 3rem; | ||||||
|  |   font-family: "knowit"; | ||||||
|  |   color: $matte-text-color; | ||||||
|  |   font-weight: normal; | ||||||
| } | } | ||||||
|  |  | ||||||
| input[type="text"] { | .filter input { | ||||||
|     width: 100%; |   font-size: 1rem; | ||||||
|     color: black; |   width: 30%; | ||||||
|     border-radius: 4px; |   border-color: black; | ||||||
|     padding: 1rem 1rem; |   border-width: 1.5px; | ||||||
|     border: 1px solid black; |   padding: 0.75rem 1rem; | ||||||
|     max-width: 200px; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| .date-won { | .highscore-header { | ||||||
|  |   margin-bottom: 2rem; | ||||||
|   font-size: 1.3rem; |   font-size: 1.3rem; | ||||||
|   margin-top: 2rem; |   color: $matte-text-color; | ||||||
| } | } | ||||||
|  |  | ||||||
| .color-box { | .highscore-list { | ||||||
|   width: 100px; |   display: flex; | ||||||
|   height: 100px; |   flex-direction: column; | ||||||
|   margin: 10px; |   padding-left: 0; | ||||||
|   -webkit-mask-image: url(/../../public/assets/images/lodd.svg); |  | ||||||
|   background-repeat: no-repeat; |  | ||||||
|   mask-image: url(/../../public/assets/images/lodd.svg); |  | ||||||
|   -webkit-mask-repeat: no-repeat; |  | ||||||
|   mask-repeat: no-repeat; |  | ||||||
|  |  | ||||||
|   @include mobile { |   li { | ||||||
|     width: 60px; |     width: intrinsic; | ||||||
|     height: 60px; |     display: inline-block; | ||||||
|     margin: 10px; |     margin-bottom: calc(1rem - 2px); | ||||||
|  |     font-size: 1.25rem; | ||||||
|  |     color: $matte-text-color; | ||||||
|  |     cursor: pointer; | ||||||
|  |  | ||||||
|  |     border-bottom: 2px solid transparent; | ||||||
|  |     &:hover, &:focus { | ||||||
|  |       border-color: $link-color; | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| .single-win { | .center { | ||||||
|   border-bottom: 1px solid rgb(237, 237, 237); |   position: absolute; | ||||||
|   .left { |   top: 40%; | ||||||
|     display: flex; |   right: 10vw; | ||||||
|     align-items: center; |   max-width: 50vw; | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .green { |   font-size: 2.5rem; | ||||||
|   background-color: $light-green; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .blue { |  | ||||||
|   background-color: $light-blue; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .yellow { |  | ||||||
|   background-color: $light-yellow; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .red { |  | ||||||
|   background-color: $light-red; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .content-container { |  | ||||||
|   display: flex; |  | ||||||
|   flex-direction: column-reverse; |  | ||||||
|   margin: 2em; |  | ||||||
|  |  | ||||||
|   .center { |  | ||||||
|     align-self: center; |  | ||||||
|     h1 { |  | ||||||
|   background-color: $primary; |   background-color: $primary; | ||||||
|       padding: 0.5rem 1rem; |   padding: 1rem 1rem; | ||||||
|   border-radius: 8px; |   border-radius: 8px; | ||||||
|   font-style: italic; |   font-style: italic; | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .winning-colors { |   @include widescreen { | ||||||
|     display: flex; |     right: 20vw; | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @include mobile { |  | ||||||
|     .center { |  | ||||||
|       display: none; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @include tablet { |  | ||||||
|     display: grid; |  | ||||||
|     grid-template-columns: .3fr 1fr; |  | ||||||
|     grid-template-rows: auto-flow min-content; |  | ||||||
|     justify-items: center; |  | ||||||
|     width: 80%; |  | ||||||
|     margin: auto; |  | ||||||
|     grid-gap: 1em; |  | ||||||
|     max-width: 1600px; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| ol { |  | ||||||
|   padding-left: 1.375rem !important; |  | ||||||
|   margin-left: 0; |  | ||||||
|   margin: 0 0 1.5em; |  | ||||||
|   padding: 0; |  | ||||||
|   counter-reset: item; |  | ||||||
|   & > li { |  | ||||||
|     cursor: pointer; |  | ||||||
|     padding: 2.5px 0; |  | ||||||
|     width: max-content; |  | ||||||
|     margin: 0 0 0 -1.25rem; |  | ||||||
|     padding: 0; |  | ||||||
|     list-style-type: none; |  | ||||||
|     counter-increment: item; |  | ||||||
|     &:before { |  | ||||||
|       display: inline-block; |  | ||||||
|       width: 1em; |  | ||||||
|       padding-right: 0.5rem; |  | ||||||
|       font-weight: bold; |  | ||||||
|       text-align: right; |  | ||||||
|       content: counter(item) "."; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @include mobile { |  | ||||||
|       padding: 5px 0; |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|   | |||||||
							
								
								
									
										273
									
								
								src/components/PersonalHighscorePage.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										273
									
								
								src/components/PersonalHighscorePage.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,273 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="container"> | ||||||
|  |     <h1>Vinlottis highscore</h1> | ||||||
|  |  | ||||||
|  |     <div class="backdrop"> | ||||||
|  |       <a @click="navigateBack" @keydown.enter="navigateBack" tabindex="0"> | ||||||
|  |         ⬅ <span class="vin-link navigate-back">Tilbake til {{ previousRoute.name }}</span> | ||||||
|  |       </a> | ||||||
|  |  | ||||||
|  |       <section v-if="winner"> | ||||||
|  |         <h2 class="name">{{ winner.name }}</h2> | ||||||
|  |  | ||||||
|  |         <p class="win-count el-spacing">{{ numberOfWins }} vinn</p> | ||||||
|  |  | ||||||
|  |         <h4 class="margin-bottom-0">Vinnende farger:</h4> | ||||||
|  |         <div class="raffle-container el-spacing"> | ||||||
|  |           <div class="raffle-element" :class="color + `-raffle`" v-for="[color, occurences] in Object.entries(winningColors)"> | ||||||
|  |             {{ occurences }} | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |  | ||||||
|  |         <h4 class="el-spacing">Flasker vunnet:</h4> | ||||||
|  |  | ||||||
|  |         <div v-for="win in winner.highscore"> | ||||||
|  |           <router-link :to="winDateUrl(win.date)" class="days-ago"> | ||||||
|  |             {{ humanReadableDate(win.date) }} - {{ daysAgo(win.date) }} dager siden | ||||||
|  |           </router-link> | ||||||
|  |            | ||||||
|  |           <div class="won-wine"> | ||||||
|  |             <img :src="smallerWineImage(win.wine.image)"> | ||||||
|  |  | ||||||
|  |             <div class="won-wine-details"> | ||||||
|  |               <h3>{{ win.wine.name }}</h3> | ||||||
|  |               <a :href="win.wine.vivinoLink" class="vin-link no-margin"> | ||||||
|  |                 Les mer på vinmonopolet.no | ||||||
|  |               </a> | ||||||
|  |             </div> | ||||||
|  |  | ||||||
|  |             <div class="raffle-element small" :class="win.color + `-raffle`"></div> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </section> | ||||||
|  |  | ||||||
|  |       <h2 v-else-if="error" class="error"> | ||||||
|  |         {{ error }} | ||||||
|  |       </h2> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | import { getWinnerByName } from "@/api"; | ||||||
|  | import { humanReadableDate, daysAgo } from "@/utils"; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       winner: undefined, | ||||||
|  |       error: undefined, | ||||||
|  |       previousRoute: { | ||||||
|  |         default: true, | ||||||
|  |         name: "topplisten", | ||||||
|  |         path: "/highscore" | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   beforeRouteEnter(to, from, next) { | ||||||
|  |     next(vm => { | ||||||
|  |       if (from.name !== null) | ||||||
|  |         vm.previousRoute = from | ||||||
|  |     }) | ||||||
|  |   }, | ||||||
|  |   computed: { | ||||||
|  |     numberOfWins() { | ||||||
|  |       return this.winner.highscore.length | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   created() { | ||||||
|  |     const nameFromURL = this.$route.params.name; | ||||||
|  |     getWinnerByName(nameFromURL) | ||||||
|  |       .then(winner => this.setWinner(winner)) | ||||||
|  |       .catch(err => this.error = `Ingen med navn: "${nameFromURL}" funnet.`) | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     setWinner(winner) { | ||||||
|  |       this.winner = { | ||||||
|  |         name: winner.name, | ||||||
|  |         highscore: [], | ||||||
|  |         ...winner | ||||||
|  |       } | ||||||
|  |       this.winningColors = this.findWinningColors() | ||||||
|  |     }, | ||||||
|  |     smallerWineImage(image) { | ||||||
|  |       if (image && image.includes(`515x515`)) | ||||||
|  |         return image.replace(`515x515`, `175x175`) | ||||||
|  |       return image | ||||||
|  |     }, | ||||||
|  |     findWinningColors() { | ||||||
|  |       const colors = this.winner.highscore.map(win => win.color) | ||||||
|  |       const colorOccurences = {} | ||||||
|  |       colors.forEach(color => { | ||||||
|  |         if (colorOccurences[color] == undefined) { | ||||||
|  |           colorOccurences[color] = 1 | ||||||
|  |         } else { | ||||||
|  |           colorOccurences[color] += 1 | ||||||
|  |         } | ||||||
|  |       }) | ||||||
|  |       return colorOccurences | ||||||
|  |     }, | ||||||
|  |     winDateUrl(date) { | ||||||
|  |       const timestamp = new Date(date).getTime(); | ||||||
|  |       return `/history/${timestamp}` | ||||||
|  |     }, | ||||||
|  |     navigateBack() { | ||||||
|  |       if (this.previousRoute.default) { | ||||||
|  |         this.$router.push({ path: this.previousRoute.path }); | ||||||
|  |       } else { | ||||||
|  |         this.$router.go(-1); | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     humanReadableDate: humanReadableDate, | ||||||
|  |     daysAgo: daysAgo | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | @import "./src/styles/variables"; | ||||||
|  | @import "./src/styles/media-queries"; | ||||||
|  |  | ||||||
|  | $elementSpacing: 3rem; | ||||||
|  |  | ||||||
|  | .el-spacing { | ||||||
|  |   margin-bottom: $elementSpacing; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .navigate-back { | ||||||
|  |   font-weight: normal; | ||||||
|  |   font-size: 1.2rem; | ||||||
|  |   border-width: 2px; | ||||||
|  |   border-color: gray; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .container { | ||||||
|  |   width: 90vw;   | ||||||
|  |   margin: 3rem auto; | ||||||
|  |   margin-bottom: 0; | ||||||
|  |   padding-bottom: 3rem; | ||||||
|  |   max-width: 1200px; | ||||||
|  |  | ||||||
|  |   @include desktop { | ||||||
|  |     width: 80vw; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | h1 { | ||||||
|  |   font-size: 3rem; | ||||||
|  |   font-family: "knowit"; | ||||||
|  |   font-weight: normal; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .name { | ||||||
|  |   text-transform: capitalize; | ||||||
|  |   font-size: 3.5rem; | ||||||
|  |   font-family: "knowit"; | ||||||
|  |   font-weight: normal; | ||||||
|  |   margin: 2rem 0 1rem 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .error { | ||||||
|  |   font-size: 2.5rem; | ||||||
|  |   font-weight: normal; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .win-count { | ||||||
|  |   font-size: 1.5rem; | ||||||
|  |   margin-top: 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .raffle-container { | ||||||
|  |   display: flex; | ||||||
|  |   margin-top: 1rem; | ||||||
|  |  | ||||||
|  |   div:not(:last-of-type) { | ||||||
|  |     margin-right: 1.5rem; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | .raffle-element { | ||||||
|  |   width: 5rem; | ||||||
|  |   height: 4rem; | ||||||
|  |   display: flex; | ||||||
|  |   justify-content: center; | ||||||
|  |   align-items: center; | ||||||
|  |   font-size: 1.5rem; | ||||||
|  |   margin-top: 0; | ||||||
|  |  | ||||||
|  |   &.small { | ||||||
|  |     height: 40px; | ||||||
|  |     width: 40px; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .days-ago { | ||||||
|  |   color: $matte-text-color; | ||||||
|  |   border-bottom: 2px solid transparent; | ||||||
|  |  | ||||||
|  |   &:hover { | ||||||
|  |     border-color: $link-color; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .won-wine { | ||||||
|  |   --spacing: 1rem; | ||||||
|  |   background-color: white; | ||||||
|  |   margin: var(--spacing) 0 3rem 0; | ||||||
|  |   padding: var(--spacing); | ||||||
|  |  | ||||||
|  |   position: relative; | ||||||
|  |  | ||||||
|  |   @include desktop { | ||||||
|  |     flex-direction: row; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   img { | ||||||
|  |     margin: 0 3rem; | ||||||
|  |     height: 160px; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   &-details { | ||||||
|  |     vertical-align: top; | ||||||
|  |     display: inline-block; | ||||||
|  |  | ||||||
|  |     @include tablet { | ||||||
|  |       width: calc(100% - 160px - 80px); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     & > * { | ||||||
|  |       width: 100%; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     h3 { | ||||||
|  |       font-size: 1.5rem; | ||||||
|  |       font-weight: normal; | ||||||
|  |       color: $matte-text-color; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     a { | ||||||
|  |       font-size: 1.2rem; | ||||||
|  |       border-width: 2px; | ||||||
|  |       font-weight: normal; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .raffle-element { | ||||||
|  |     position: absolute; | ||||||
|  |     top: calc(var(--spacing) * 2); | ||||||
|  |     right: calc(var(--spacing) * 2); | ||||||
|  |     margin: 0; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | .backdrop { | ||||||
|  |   $background: rgb(244,244,244); | ||||||
|  |    | ||||||
|  |   --padding: 2rem; | ||||||
|  |   @include desktop { | ||||||
|  |     --padding: 5rem; | ||||||
|  |   } | ||||||
|  |   background-color: $background; | ||||||
|  |   padding: var(--padding); | ||||||
|  | } | ||||||
|  | </style> | ||||||
| @@ -18,7 +18,7 @@ | |||||||
|     <h2 v-if="winners.length > 0">Vinnere</h2> |     <h2 v-if="winners.length > 0">Vinnere</h2> | ||||||
|     <div class="winners" v-if="winners.length > 0"> |     <div class="winners" v-if="winners.length > 0"> | ||||||
|       <div class="winner" v-for="(winner, index) in winners" :key="index"> |       <div class="winner" v-for="(winner, index) in winners" :key="index"> | ||||||
|         <div :class="winner.color + '-ballot'" class="ballot-element"> |         <div :class="winner.color + '-raffle'" class="raffle-element"> | ||||||
|           <span>{{ winner.name }}</span> |           <span>{{ winner.name }}</span> | ||||||
|           <span>{{ winner.phoneNumber }}</span> |           <span>{{ winner.phoneNumber }}</span> | ||||||
|           <span>Rød: {{ winner.red }}</span> |           <span>Rød: {{ winner.red }}</span> | ||||||
| @@ -47,11 +47,11 @@ | |||||||
|           <span class="name">{{ attendee.name }}</span> |           <span class="name">{{ attendee.name }}</span> | ||||||
|           <span class="phoneNumber">{{ attendee.phoneNumber }}</span> |           <span class="phoneNumber">{{ attendee.phoneNumber }}</span> | ||||||
|         </div> |         </div> | ||||||
|         <div class="ballots-container"> |         <div class="raffles-container"> | ||||||
|           <div class="red-ballot ballot-element small">{{ attendee.red }}</div> |           <div class="red-raffle raffle-element small">{{ attendee.red }}</div> | ||||||
|           <div class="blue-ballot ballot-element small">{{ attendee.blue }}</div> |           <div class="blue-raffle raffle-element small">{{ attendee.blue }}</div> | ||||||
|           <div class="green-ballot ballot-element small">{{ attendee.green }}</div> |           <div class="green-raffle raffle-element small">{{ attendee.green }}</div> | ||||||
|           <div class="yellow-ballot ballot-element small">{{ attendee.yellow }}</div> |           <div class="yellow-raffle raffle-element small">{{ attendee.yellow }}</div> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
| @@ -140,7 +140,7 @@ export default { | |||||||
|       blue: 0, |       blue: 0, | ||||||
|       green: 0, |       green: 0, | ||||||
|       yellow: 0, |       yellow: 0, | ||||||
|       ballots: 0, |       raffles: 0, | ||||||
|       randomColors: false, |       randomColors: false, | ||||||
|       attendees: [], |       attendees: [], | ||||||
|       winners: [], |       winners: [], | ||||||
| @@ -197,7 +197,7 @@ export default { | |||||||
|         blue: this.blue, |         blue: this.blue, | ||||||
|         green: this.green, |         green: this.green, | ||||||
|         yellow: this.yellow, |         yellow: this.yellow, | ||||||
|         ballots: this.ballots |         raffles: this.raffles | ||||||
|       }); |       }); | ||||||
|  |  | ||||||
|       if (response == true) { |       if (response == true) { | ||||||
| @@ -354,7 +354,7 @@ hr { | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| .ballot-element { | .raffle-element { | ||||||
|   width: 140px; |   width: 140px; | ||||||
|   height: 150px; |   height: 150px; | ||||||
|   margin: 20px 0; |   margin: 20px 0; | ||||||
| @@ -378,19 +378,19 @@ hr { | |||||||
|     font-size: 1rem; |     font-size: 1rem; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   &.green-ballot { |   &.green-raffle { | ||||||
|     background-color: $light-green; |     background-color: $light-green; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   &.blue-ballot { |   &.blue-raffle { | ||||||
|     background-color: $light-blue; |     background-color: $light-blue; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   &.yellow-ballot { |   &.yellow-raffle { | ||||||
|     background-color: $light-yellow; |     background-color: $light-yellow; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   &.red-ballot { |   &.red-raffle { | ||||||
|     background-color: $light-red; |     background-color: $light-red; | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @@ -422,7 +422,7 @@ button { | |||||||
|   margin: 0 auto; |   margin: 0 auto; | ||||||
|  |  | ||||||
|   & .name-and-phone, |   & .name-and-phone, | ||||||
|   & .ballots-container { |   & .raffles-container { | ||||||
|     display: flex; |     display: flex; | ||||||
|     justify-content: center; |     justify-content: center; | ||||||
|   } |   } | ||||||
| @@ -431,7 +431,7 @@ button { | |||||||
|     flex-direction: column; |     flex-direction: column; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   & .ballots-container { |   & .raffles-container { | ||||||
|     flex-direction: row; |     flex-direction: row; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -12,6 +12,7 @@ import WinnerPage from "@/components/WinnerPage"; | |||||||
| import LotteryPage from "@/components/LotteryPage"; | import LotteryPage from "@/components/LotteryPage"; | ||||||
| import HistoryPage from "@/components/HistoryPage"; | import HistoryPage from "@/components/HistoryPage"; | ||||||
| import HighscorePage from "@/components/HighscorePage"; | import HighscorePage from "@/components/HighscorePage"; | ||||||
|  | import PersonalHighscorePage from "@/components/PersonalHighscorePage"; | ||||||
|  |  | ||||||
| import RequestWine from "@/components/RequestWine"; | import RequestWine from "@/components/RequestWine"; | ||||||
| import AllRequestedWines from "@/components/AllRequestedWines"; | import AllRequestedWines from "@/components/AllRequestedWines"; | ||||||
| @@ -57,6 +58,10 @@ const routes = [ | |||||||
|     path: "/history", |     path: "/history", | ||||||
|     component: HistoryPage |     component: HistoryPage | ||||||
|   }, |   }, | ||||||
|  |   { | ||||||
|  |     path: "/highscore/:name", | ||||||
|  |     component: PersonalHighscorePage | ||||||
|  |   }, | ||||||
|   { |   { | ||||||
|     path: "/highscore", |     path: "/highscore", | ||||||
|     component: HighscorePage |     component: HighscorePage | ||||||
|   | |||||||
| @@ -18,6 +18,10 @@ body { | |||||||
|   margin: 0; |   margin: 0; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | a { | ||||||
|  |   text-decoration: none; | ||||||
|  | } | ||||||
|  |  | ||||||
| .title { | .title { | ||||||
|   text-align: center; |   text-align: center; | ||||||
|   width: fit-content; |   width: fit-content; | ||||||
| @@ -242,7 +246,7 @@ textarea { | |||||||
|   margin: 0 !important; |   margin: 0 !important; | ||||||
| } | } | ||||||
|  |  | ||||||
| .ballot-element { | .raffle-element { | ||||||
|   margin: 20px 0; |   margin: 20px 0; | ||||||
|   -webkit-mask-image: url(/../../public/assets/images/lodd.svg); |   -webkit-mask-image: url(/../../public/assets/images/lodd.svg); | ||||||
|   background-repeat: no-repeat; |   background-repeat: no-repeat; | ||||||
| @@ -251,19 +255,19 @@ textarea { | |||||||
|   mask-repeat: no-repeat; |   mask-repeat: no-repeat; | ||||||
|   color: #333333; |   color: #333333; | ||||||
|  |  | ||||||
|   &.green-ballot { |   &.green-raffle { | ||||||
|     background-color: $light-green; |     background-color: $light-green; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   &.blue-ballot { |   &.blue-raffle { | ||||||
|     background-color: $light-blue; |     background-color: $light-blue; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   &.yellow-ballot { |   &.yellow-raffle { | ||||||
|     background-color: $light-yellow; |     background-color: $light-yellow; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   &.red-ballot { |   &.red-raffle { | ||||||
|     background-color: $light-red; |     background-color: $light-red; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,15 +1,15 @@ | |||||||
| .flex { | .flex { | ||||||
|   display: flex; |   display: flex; | ||||||
|  |  | ||||||
|   & .column { |   &.column { | ||||||
|     flex-direction: column; |     flex-direction: column; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   & .row { |   &.row { | ||||||
|     flex-direction: row; |     flex-direction: row; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   & .wrap { |   &.wrap { | ||||||
|     flex-wrap: wrap; |     flex-wrap: wrap; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -17,3 +17,5 @@ $red: #ef5878; | |||||||
| $dark-red: #ec3b61; | $dark-red: #ec3b61; | ||||||
|  |  | ||||||
| $link-color: #ff5fff; | $link-color: #ff5fff; | ||||||
|  |  | ||||||
|  | $matte-text-color: #333333; | ||||||
| @@ -4,10 +4,10 @@ | |||||||
|     <div class="attendees-container" ref="attendees"> |     <div class="attendees-container" ref="attendees"> | ||||||
|       <div class="attendee" v-for="(attendee, index) in flipList(attendees)" :key="index"> |       <div class="attendee" v-for="(attendee, index) in flipList(attendees)" :key="index"> | ||||||
|         <span class="attendee-name">{{ attendee.name }}</span> |         <span class="attendee-name">{{ attendee.name }}</span> | ||||||
|         <div class="red-ballot ballot-element small">{{ attendee.red }}</div> |         <div class="red-raffle ballot-element small">{{ attendee.red }}</div> | ||||||
|         <div class="blue-ballot ballot-element small">{{ attendee.blue }}</div> |         <div class="blue-raffle ballot-element small">{{ attendee.blue }}</div> | ||||||
|         <div class="green-ballot ballot-element small">{{ attendee.green }}</div> |         <div class="green-raffle ballot-element small">{{ attendee.green }}</div> | ||||||
|         <div class="yellow-ballot ballot-element small">{{ attendee.yellow }}</div> |         <div class="yellow-raffle ballot-element small">{{ attendee.yellow }}</div> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
| @@ -46,7 +46,7 @@ export default { | |||||||
|   width: 60%; |   width: 60%; | ||||||
| } | } | ||||||
|  |  | ||||||
| .ballot-element { | .raffle-element { | ||||||
|   font-size: 0.75rem; |   font-size: 0.75rem; | ||||||
|   width: 45px; |   width: 45px; | ||||||
|   height: 45px; |   height: 45px; | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ | |||||||
|           color.name + |           color.name + | ||||||
|             '-container ' + |             '-container ' + | ||||||
|             color.name + |             color.name + | ||||||
|             '-ballot inner-bought-container ballot-element' |             '-raffle inner-bought-container ballot-element' | ||||||
|         " |         " | ||||||
|         :key="color.name" |         :key="color.name" | ||||||
|       > |       > | ||||||
| @@ -24,7 +24,7 @@ | |||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|       <div class="inner-bought-container total-ballots"> |       <div class="inner-bought-container total-raffles"> | ||||||
|         <div class="total-container"> |         <div class="total-container"> | ||||||
|           Totalt  |           Totalt  | ||||||
|           <div> |           <div> | ||||||
| @@ -136,7 +136,7 @@ export default { | |||||||
|   align-items: center; |   align-items: center; | ||||||
| } | } | ||||||
|  |  | ||||||
| .ballot-element { | .raffle-element { | ||||||
|   width: 140px; |   width: 140px; | ||||||
|   height: 150px; |   height: 150px; | ||||||
|   margin: 20px 0; |   margin: 20px 0; | ||||||
| @@ -166,7 +166,7 @@ export default { | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| .total-ballots { | .total-raffles { | ||||||
|   width: 150px; |   width: 150px; | ||||||
|   height: 150px; |   height: 150px; | ||||||
|   margin: 20px 0; |   margin: 20px 0; | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ | |||||||
|     <div class="current-draw" v-if="drawing"> |     <div class="current-draw" v-if="drawing"> | ||||||
|       <h2>TREKKER</h2> |       <h2>TREKKER</h2> | ||||||
|       <div |       <div | ||||||
|         :class="currentColor + '-ballot'" |         :class="currentColor + '-raffle'" | ||||||
|         class="ballot-element center-new-winner" |         class="ballot-element center-new-winner" | ||||||
|         :style="{ transform: 'rotate(' + getRotation() + 'deg)' }" |         :style="{ transform: 'rotate(' + getRotation() + 'deg)' }" | ||||||
|       > |       > | ||||||
| @@ -19,7 +19,7 @@ | |||||||
|     <div class="current-draw" v-if="drawingDone"> |     <div class="current-draw" v-if="drawingDone"> | ||||||
|       <h2>VINNER</h2> |       <h2>VINNER</h2> | ||||||
|       <div |       <div | ||||||
|         :class="currentColor + '-ballot'" |         :class="currentColor + '-raffle'" | ||||||
|         class="ballot-element center-new-winner" |         class="ballot-element center-new-winner" | ||||||
|         :style="{ transform: 'rotate(' + getRotation() + 'deg)' }" |         :style="{ transform: 'rotate(' + getRotation() + 'deg)' }" | ||||||
|       > |       > | ||||||
| @@ -204,7 +204,7 @@ h2 { | |||||||
|   align-items: center; |   align-items: center; | ||||||
| } | } | ||||||
|  |  | ||||||
| .ballot-element { | .raffle-element { | ||||||
|   width: 140px; |   width: 140px; | ||||||
|   height: 140px; |   height: 140px; | ||||||
|   font-size: 1.2rem; |   font-size: 1.2rem; | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ | |||||||
|     <h2 v-if="winners.length > 0"> {{ title ? title : 'Vinnere' }}</h2> |     <h2 v-if="winners.length > 0"> {{ title ? title : 'Vinnere' }}</h2> | ||||||
|     <div class="winners" v-if="winners.length > 0"> |     <div class="winners" v-if="winners.length > 0"> | ||||||
|       <div class="winner" v-for="(winner, index) in winners" :key="index"> |       <div class="winner" v-for="(winner, index) in winners" :key="index"> | ||||||
|         <div :class="winner.color + '-ballot'" class="ballot-element">{{ winner.name }}</div> |         <div :class="winner.color + '-raffle'" class="ballot-element">{{ winner.name }}</div> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
| @@ -40,7 +40,7 @@ h2 { | |||||||
|   flex-wrap: wrap; |   flex-wrap: wrap; | ||||||
| } | } | ||||||
|  |  | ||||||
| .ballot-element { | .raffle-element { | ||||||
|   font-size: 1rem; |   font-size: 1rem; | ||||||
|   width: 145px; |   width: 145px; | ||||||
|   height: 145px; |   height: 145px; | ||||||
|   | |||||||
							
								
								
									
										16
									
								
								src/utils.js
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								src/utils.js
									
									
									
									
									
								
							| @@ -7,6 +7,18 @@ const dateString = (date) => { | |||||||
|   return `${ye}-${mo}-${da}` |   return `${ye}-${mo}-${da}` | ||||||
| } | } | ||||||
|  |  | ||||||
| module.exports = { | function humanReadableDate(date) { | ||||||
|   dateString |   const options = { year: 'numeric', month: 'long', day: 'numeric' }; | ||||||
|  |   return new Date(date).toLocaleDateString(undefined, options); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function daysAgo(date) { | ||||||
|  |   const day = 24 * 60 * 60 * 1000; | ||||||
|  |   return Math.round(Math.abs((new Date() - new Date(date)) / day)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | module.exports = { | ||||||
|  |   dateString, | ||||||
|  |   humanReadableDate, | ||||||
|  |   daysAgo | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user