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(async (lotteries) => { | ||||
|       const lottery = lotteries[date]; | ||||
|       let highscoreWithResolvedWines = await resolveWineReferences(lottery) | ||||
|       highscoreWithResolvedWines = highscoreWithResolvedWines.reverse() | ||||
|  | ||||
|       if (lottery != null) { | ||||
|         return res.send({ | ||||
|           message: `Lottery for date: ${dateString}`, | ||||
|           lottery: await resolveWineReferences(lottery) | ||||
|           lottery: highscoreWithResolvedWines | ||||
|         }) | ||||
|       } else { | ||||
|         return res.status(404).send({ | ||||
| @@ -93,15 +95,18 @@ const byEpochDate = (req, res) => { | ||||
|  | ||||
| const byName = (req, res) => { | ||||
|   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) => { | ||||
|       highscore = highscore[0] | ||||
|       if (highscore) { | ||||
|         const highscoreWithResolvedWines = await resolveWineReferences(highscore.wins) | ||||
|         let highscoreWithResolvedWines = await resolveWineReferences(highscore.wins) | ||||
|         highscoreWithResolvedWines = highscoreWithResolvedWines.reverse() | ||||
|  | ||||
|         return res.send({ | ||||
|           message: `Lottery winnings by name: ${name}`, | ||||
|           name: name, | ||||
|           highscore: highscoreWithResolvedWines | ||||
|          }) | ||||
|       } 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 { | ||||
|   statistics, | ||||
|   colorStatistics, | ||||
| @@ -364,5 +377,6 @@ export { | ||||
|   finishedDraw, | ||||
|   getAmIWinner, | ||||
|   postWineChosen, | ||||
|   historyAll | ||||
|   historyAll, | ||||
|   getWinnerByName | ||||
| }; | ||||
|   | ||||
| @@ -1,56 +1,24 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <h1 class="text-center">Vinlottis highscore 🏆🍷</h1> | ||||
|   <div class="container"> | ||||
|     <h1>Vinlottis highscore</h1> | ||||
|  | ||||
|     <div class="content-container"> | ||||
|       <div class="highscore">         | ||||
|         <div> | ||||
|           <h3 >Finn ditt navn:</h3> | ||||
|           <input type="text" v-model="highscoreFilter" placeholder="Filtrer på navn" class="margin-bottom-sm" /> | ||||
|     <div class="filter flex el-spacing"> | ||||
|       <input type="text" v-model="filterInput" placeholder="Filtrer på navn" /> | ||||
|       <button v-if="filterInput" @click="resetFilter" class="vin-button auto-height margin-left-sm"> | ||||
|         Reset | ||||
|       </button> | ||||
|     </div> | ||||
|  | ||||
|         <ol v-if="highscore.length > 0"> | ||||
|           <li v-for="person in highscore" :key="person" @click="selectWinner(person)"> | ||||
|             {{ person.name }} - {{ person.wins.length }} | ||||
|     <p class="highscore-header margin-bottom-md"><b>Plassering.</b> Navn - Antall vinn</p> | ||||
|  | ||||
|     <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> | ||||
|     </ol> | ||||
|  | ||||
|         <div v-if="highscore.length != highscoreResponse.length" class="flex justify-center align-center"> | ||||
|           <i @click="resetFilter" @keyup.space="resetFilter" | ||||
|              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 class="center desktop-only"> | ||||
|       👈 Se dine vin(n), trykk på navnet ditt | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| @@ -58,16 +26,15 @@ | ||||
| <script> | ||||
|  | ||||
| import { highscoreStatistics } from "@/api"; | ||||
| import { humanReadableDate, daysAgo } from "@/utils"; | ||||
| import Wine from "@/ui/Wine"; | ||||
|  | ||||
| export default { | ||||
|   components: { Wine }, | ||||
|   data() { | ||||
|     return { | ||||
|       highscoreResponse: [], | ||||
|       highscore: [], | ||||
|       highscoreFilter: '', | ||||
|       selectedWinner: null | ||||
|       filterInput: '' | ||||
|     } | ||||
|   }, | ||||
|   async mounted() { | ||||
| @@ -78,177 +45,128 @@ export default { | ||||
|     response = response.filter( | ||||
|       person => person.name != null && person.name != "" | ||||
|     ); | ||||
|     this.highscoreResponse = response | ||||
|     this.highscore = this.highscoreResponse; | ||||
|     this.highscore = this.generateScoreBoard(response); | ||||
|   }, | ||||
|   watch: { | ||||
|     highscoreFilter(val) { | ||||
|   computed: { | ||||
|     filteredResults() { | ||||
|       let highscore = this.highscore; | ||||
|       let val = this.filterInput; | ||||
|  | ||||
|       if (val.length) { | ||||
|         val = val.toLowerCase(); | ||||
|         this.highscore = this.highscoreResponse.filter(person => person.name.toLowerCase().includes(val)) | ||||
|       } else { | ||||
|         this.highscore = this.highscoreResponse | ||||
|         const nameIncludesString = (person) => person.name.toLowerCase().includes(val); | ||||
|         highscore = highscore.filter(nameIncludesString) | ||||
|       } | ||||
|  | ||||
|       return highscore | ||||
|     } | ||||
|   }, | ||||
|   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() { | ||||
|       this.highscore = this.highscoreResponse; | ||||
|       this.highscoreFilter = ''; | ||||
|       this.filterInput = ''; | ||||
|       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) { | ||||
|       if (this.selectedWinner != null && this.selectedWinner["name"] == winner["name"]) { | ||||
|         this.selectedWinner = null; | ||||
|       } else { | ||||
|         let newestFirst = winner.wins.sort((a, b) => a.date < b.date); | ||||
|         winner.wins = newestFirst; | ||||
|         this.selectedWinner = { ...winner }; | ||||
|       } | ||||
|       const path = "/highscore/" + encodeURIComponent(winner.name) | ||||
|       this.$router.push(path); | ||||
|     }, | ||||
|     getRotation: function() { | ||||
|       let num = Math.floor(Math.random() * 12.5); | ||||
|       let neg = Math.floor(Math.random() * 2); | ||||
|       return neg == 0 ? -num : num; | ||||
|     } | ||||
|     humanReadableDate: humanReadableDate, | ||||
|     daysAgo: daysAgo | ||||
|   } | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| @import "../styles/media-queries.scss"; | ||||
| @import "../styles/variables.scss"; | ||||
| @import "./src/styles/media-queries.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 { | ||||
|   text-align: center; | ||||
|   font-size: 3rem; | ||||
|   font-family: "knowit"; | ||||
|   color: $matte-text-color; | ||||
|   font-weight: normal; | ||||
| } | ||||
|  | ||||
| input[type="text"] { | ||||
|     width: 100%; | ||||
|     color: black; | ||||
|     border-radius: 4px; | ||||
|     padding: 1rem 1rem; | ||||
|     border: 1px solid black; | ||||
|     max-width: 200px; | ||||
| .filter input { | ||||
|   font-size: 1rem; | ||||
|   width: 30%; | ||||
|   border-color: black; | ||||
|   border-width: 1.5px; | ||||
|   padding: 0.75rem 1rem; | ||||
| } | ||||
|  | ||||
| .date-won { | ||||
| .highscore-header { | ||||
|   margin-bottom: 2rem; | ||||
|   font-size: 1.3rem; | ||||
|   margin-top: 2rem; | ||||
|   color: $matte-text-color; | ||||
| } | ||||
|  | ||||
| .color-box { | ||||
|   width: 100px; | ||||
|   height: 100px; | ||||
|   margin: 10px; | ||||
|   -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; | ||||
| .highscore-list { | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   padding-left: 0; | ||||
|  | ||||
|   @include mobile { | ||||
|     width: 60px; | ||||
|     height: 60px; | ||||
|     margin: 10px; | ||||
|   li { | ||||
|     width: intrinsic; | ||||
|     display: inline-block; | ||||
|     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 { | ||||
|   border-bottom: 1px solid rgb(237, 237, 237); | ||||
|   .left { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|   } | ||||
| } | ||||
| .center { | ||||
|   position: absolute; | ||||
|   top: 40%; | ||||
|   right: 10vw; | ||||
|   max-width: 50vw; | ||||
|  | ||||
| .green { | ||||
|   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 { | ||||
|   font-size: 2.5rem; | ||||
|   background-color: $primary; | ||||
|       padding: 0.5rem 1rem; | ||||
|   padding: 1rem 1rem; | ||||
|   border-radius: 8px; | ||||
|   font-style: italic; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .winning-colors { | ||||
|     display: flex; | ||||
|   } | ||||
|  | ||||
|   @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; | ||||
|     } | ||||
|   @include widescreen { | ||||
|     right: 20vw; | ||||
|   } | ||||
| } | ||||
| </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> | ||||
|     <div class="winners" v-if="winners.length > 0"> | ||||
|       <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.phoneNumber }}</span> | ||||
|           <span>Rød: {{ winner.red }}</span> | ||||
| @@ -47,11 +47,11 @@ | ||||
|           <span class="name">{{ attendee.name }}</span> | ||||
|           <span class="phoneNumber">{{ attendee.phoneNumber }}</span> | ||||
|         </div> | ||||
|         <div class="ballots-container"> | ||||
|           <div class="red-ballot ballot-element small">{{ attendee.red }}</div> | ||||
|           <div class="blue-ballot ballot-element small">{{ attendee.blue }}</div> | ||||
|           <div class="green-ballot ballot-element small">{{ attendee.green }}</div> | ||||
|           <div class="yellow-ballot ballot-element small">{{ attendee.yellow }}</div> | ||||
|         <div class="raffles-container"> | ||||
|           <div class="red-raffle raffle-element small">{{ attendee.red }}</div> | ||||
|           <div class="blue-raffle raffle-element small">{{ attendee.blue }}</div> | ||||
|           <div class="green-raffle raffle-element small">{{ attendee.green }}</div> | ||||
|           <div class="yellow-raffle raffle-element small">{{ attendee.yellow }}</div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
| @@ -140,7 +140,7 @@ export default { | ||||
|       blue: 0, | ||||
|       green: 0, | ||||
|       yellow: 0, | ||||
|       ballots: 0, | ||||
|       raffles: 0, | ||||
|       randomColors: false, | ||||
|       attendees: [], | ||||
|       winners: [], | ||||
| @@ -197,7 +197,7 @@ export default { | ||||
|         blue: this.blue, | ||||
|         green: this.green, | ||||
|         yellow: this.yellow, | ||||
|         ballots: this.ballots | ||||
|         raffles: this.raffles | ||||
|       }); | ||||
|  | ||||
|       if (response == true) { | ||||
| @@ -354,7 +354,7 @@ hr { | ||||
|   } | ||||
| } | ||||
|  | ||||
| .ballot-element { | ||||
| .raffle-element { | ||||
|   width: 140px; | ||||
|   height: 150px; | ||||
|   margin: 20px 0; | ||||
| @@ -378,19 +378,19 @@ hr { | ||||
|     font-size: 1rem; | ||||
|   } | ||||
|  | ||||
|   &.green-ballot { | ||||
|   &.green-raffle { | ||||
|     background-color: $light-green; | ||||
|   } | ||||
|  | ||||
|   &.blue-ballot { | ||||
|   &.blue-raffle { | ||||
|     background-color: $light-blue; | ||||
|   } | ||||
|  | ||||
|   &.yellow-ballot { | ||||
|   &.yellow-raffle { | ||||
|     background-color: $light-yellow; | ||||
|   } | ||||
|  | ||||
|   &.red-ballot { | ||||
|   &.red-raffle { | ||||
|     background-color: $light-red; | ||||
|   } | ||||
| } | ||||
| @@ -422,7 +422,7 @@ button { | ||||
|   margin: 0 auto; | ||||
|  | ||||
|   & .name-and-phone, | ||||
|   & .ballots-container { | ||||
|   & .raffles-container { | ||||
|     display: flex; | ||||
|     justify-content: center; | ||||
|   } | ||||
| @@ -431,7 +431,7 @@ button { | ||||
|     flex-direction: column; | ||||
|   } | ||||
|  | ||||
|   & .ballots-container { | ||||
|   & .raffles-container { | ||||
|     flex-direction: row; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -12,6 +12,7 @@ import WinnerPage from "@/components/WinnerPage"; | ||||
| import LotteryPage from "@/components/LotteryPage"; | ||||
| import HistoryPage from "@/components/HistoryPage"; | ||||
| import HighscorePage from "@/components/HighscorePage"; | ||||
| import PersonalHighscorePage from "@/components/PersonalHighscorePage"; | ||||
|  | ||||
| import RequestWine from "@/components/RequestWine"; | ||||
| import AllRequestedWines from "@/components/AllRequestedWines"; | ||||
| @@ -57,6 +58,10 @@ const routes = [ | ||||
|     path: "/history", | ||||
|     component: HistoryPage | ||||
|   }, | ||||
|   { | ||||
|     path: "/highscore/:name", | ||||
|     component: PersonalHighscorePage | ||||
|   }, | ||||
|   { | ||||
|     path: "/highscore", | ||||
|     component: HighscorePage | ||||
|   | ||||
| @@ -18,6 +18,10 @@ body { | ||||
|   margin: 0; | ||||
| } | ||||
|  | ||||
| a { | ||||
|   text-decoration: none; | ||||
| } | ||||
|  | ||||
| .title { | ||||
|   text-align: center; | ||||
|   width: fit-content; | ||||
| @@ -242,7 +246,7 @@ textarea { | ||||
|   margin: 0 !important; | ||||
| } | ||||
|  | ||||
| .ballot-element { | ||||
| .raffle-element { | ||||
|   margin: 20px 0; | ||||
|   -webkit-mask-image: url(/../../public/assets/images/lodd.svg); | ||||
|   background-repeat: no-repeat; | ||||
| @@ -251,19 +255,19 @@ textarea { | ||||
|   mask-repeat: no-repeat; | ||||
|   color: #333333; | ||||
|  | ||||
|   &.green-ballot { | ||||
|   &.green-raffle { | ||||
|     background-color: $light-green; | ||||
|   } | ||||
|  | ||||
|   &.blue-ballot { | ||||
|   &.blue-raffle { | ||||
|     background-color: $light-blue; | ||||
|   } | ||||
|  | ||||
|   &.yellow-ballot { | ||||
|   &.yellow-raffle { | ||||
|     background-color: $light-yellow; | ||||
|   } | ||||
|  | ||||
|   &.red-ballot { | ||||
|   &.red-raffle { | ||||
|     background-color: $light-red; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,15 +1,15 @@ | ||||
| .flex { | ||||
|   display: flex; | ||||
|  | ||||
|   & .column { | ||||
|   &.column { | ||||
|     flex-direction: column; | ||||
|   } | ||||
|  | ||||
|   & .row { | ||||
|   &.row { | ||||
|     flex-direction: row; | ||||
|   } | ||||
|  | ||||
|   & .wrap { | ||||
|   &.wrap { | ||||
|     flex-wrap: wrap; | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -17,3 +17,5 @@ $red: #ef5878; | ||||
| $dark-red: #ec3b61; | ||||
|  | ||||
| $link-color: #ff5fff; | ||||
|  | ||||
| $matte-text-color: #333333; | ||||
| @@ -4,10 +4,10 @@ | ||||
|     <div class="attendees-container" ref="attendees"> | ||||
|       <div class="attendee" v-for="(attendee, index) in flipList(attendees)" :key="index"> | ||||
|         <span class="attendee-name">{{ attendee.name }}</span> | ||||
|         <div class="red-ballot ballot-element small">{{ attendee.red }}</div> | ||||
|         <div class="blue-ballot ballot-element small">{{ attendee.blue }}</div> | ||||
|         <div class="green-ballot ballot-element small">{{ attendee.green }}</div> | ||||
|         <div class="yellow-ballot ballot-element small">{{ attendee.yellow }}</div> | ||||
|         <div class="red-raffle ballot-element small">{{ attendee.red }}</div> | ||||
|         <div class="blue-raffle ballot-element small">{{ attendee.blue }}</div> | ||||
|         <div class="green-raffle ballot-element small">{{ attendee.green }}</div> | ||||
|         <div class="yellow-raffle ballot-element small">{{ attendee.yellow }}</div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| @@ -46,7 +46,7 @@ export default { | ||||
|   width: 60%; | ||||
| } | ||||
|  | ||||
| .ballot-element { | ||||
| .raffle-element { | ||||
|   font-size: 0.75rem; | ||||
|   width: 45px; | ||||
|   height: 45px; | ||||
|   | ||||
| @@ -8,7 +8,7 @@ | ||||
|           color.name + | ||||
|             '-container ' + | ||||
|             color.name + | ||||
|             '-ballot inner-bought-container ballot-element' | ||||
|             '-raffle inner-bought-container ballot-element' | ||||
|         " | ||||
|         :key="color.name" | ||||
|       > | ||||
| @@ -24,7 +24,7 @@ | ||||
|         </div> | ||||
|       </div> | ||||
|  | ||||
|       <div class="inner-bought-container total-ballots"> | ||||
|       <div class="inner-bought-container total-raffles"> | ||||
|         <div class="total-container"> | ||||
|           Totalt  | ||||
|           <div> | ||||
| @@ -136,7 +136,7 @@ export default { | ||||
|   align-items: center; | ||||
| } | ||||
|  | ||||
| .ballot-element { | ||||
| .raffle-element { | ||||
|   width: 140px; | ||||
|   height: 150px; | ||||
|   margin: 20px 0; | ||||
| @@ -166,7 +166,7 @@ export default { | ||||
|   } | ||||
| } | ||||
|  | ||||
| .total-ballots { | ||||
| .total-raffles { | ||||
|   width: 150px; | ||||
|   height: 150px; | ||||
|   margin: 20px 0; | ||||
|   | ||||
| @@ -3,7 +3,7 @@ | ||||
|     <div class="current-draw" v-if="drawing"> | ||||
|       <h2>TREKKER</h2> | ||||
|       <div | ||||
|         :class="currentColor + '-ballot'" | ||||
|         :class="currentColor + '-raffle'" | ||||
|         class="ballot-element center-new-winner" | ||||
|         :style="{ transform: 'rotate(' + getRotation() + 'deg)' }" | ||||
|       > | ||||
| @@ -19,7 +19,7 @@ | ||||
|     <div class="current-draw" v-if="drawingDone"> | ||||
|       <h2>VINNER</h2> | ||||
|       <div | ||||
|         :class="currentColor + '-ballot'" | ||||
|         :class="currentColor + '-raffle'" | ||||
|         class="ballot-element center-new-winner" | ||||
|         :style="{ transform: 'rotate(' + getRotation() + 'deg)' }" | ||||
|       > | ||||
| @@ -204,7 +204,7 @@ h2 { | ||||
|   align-items: center; | ||||
| } | ||||
|  | ||||
| .ballot-element { | ||||
| .raffle-element { | ||||
|   width: 140px; | ||||
|   height: 140px; | ||||
|   font-size: 1.2rem; | ||||
|   | ||||
| @@ -3,7 +3,7 @@ | ||||
|     <h2 v-if="winners.length > 0"> {{ title ? title : 'Vinnere' }}</h2> | ||||
|     <div class="winners" v-if="winners.length > 0"> | ||||
|       <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> | ||||
| @@ -40,7 +40,7 @@ h2 { | ||||
|   flex-wrap: wrap; | ||||
| } | ||||
|  | ||||
| .ballot-element { | ||||
| .raffle-element { | ||||
|   font-size: 1rem; | ||||
|   width: 145px; | ||||
|   height: 145px; | ||||
|   | ||||
							
								
								
									
										16
									
								
								src/utils.js
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								src/utils.js
									
									
									
									
									
								
							| @@ -7,6 +7,18 @@ const dateString = (date) => { | ||||
|   return `${ye}-${mo}-${da}` | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|   dateString | ||||
| function humanReadableDate(date) { | ||||
|   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