Pulled feature branch up-stream.

This commit is contained in:
2020-09-08 09:19:56 +02:00
20 changed files with 909 additions and 63 deletions

View File

@@ -9,10 +9,17 @@ platform:
steps:
- name: frontend_install
image: node:13.6.0
image: node:14
commands:
- node -v
- yarn --version
- name: backend_build
image: node:14
commands:
- node -v
- yarn --version
- yarn
- yarn build
- name: deploy
image: appleboy/drone-ssh
pull: true

69
api/request.js Normal file
View File

@@ -0,0 +1,69 @@
const express = require("express");
const path = require("path");
const RequestedWine = require(path.join(
__dirname + "/../schemas/RequestedWine"
));
const Wine = require(path.join(
__dirname + "/../schemas/Wine"
));
const deleteRequestedWineById = async (req, res) => {
const { id } = req.params;
if(id == null){
return res.json({
message: "Id er ikke definert",
success: false
})
}
await RequestedWine.deleteOne({wineId: id})
return res.json({
message: `Slettet vin med id: ${id}`,
success: true
});
}
const getAllRequestedWines = async (req, res) => {
const allWines = await RequestedWine.find({}).populate("wine");
return res.json(allWines);
}
const requestNewWine = async (req, res) => {
const {wine} = req.body
let thisWineIsLOKO = await Wine.findOne({id: wine.id})
if(thisWineIsLOKO == undefined){
thisWineIsLOKO = new Wine({
name: wine.name,
vivinoLink: wine.vivinoLink,
rating: null,
occurences: null,
image: wine.image,
id: wine.id
});
await thisWineIsLOKO.save()
}
let requestedWine = await RequestedWine.findOne({ "wineId": wine.id})
if(requestedWine == undefined){
requestedWine = new RequestedWine({
count: 1,
wineId: wine.id,
wine: thisWineIsLOKO
})
} else {
requestedWine.count += 1;
}
await requestedWine.save()
return res.send(requestedWine);
}
module.exports = {
requestNewWine,
getAllRequestedWines,
deleteRequestedWineById
};

View File

@@ -1,6 +1,4 @@
const express = require("express");
const path = require("path");
const router = express.Router();
const mongoose = require("mongoose");
mongoose.connect("mongodb://localhost:27017/vinlottis", {
useNewUrlParser: true

View File

@@ -3,9 +3,11 @@ const path = require("path");
// Middleware
const mustBeAuthenticated = require(__dirname + "/../middleware/mustBeAuthenticated");
const setAdminHeaderIfAuthenticated = require(__dirname + "/../middleware/setAdminHeaderIfAuthenticated");
const update = require(path.join(__dirname + "/update"));
const retrieve = require(path.join(__dirname + "/retrieve"));
const request = require(path.join(__dirname + "/request"));
const subscriptionApi = require(path.join(__dirname + "/subscriptions"));
const loginApi = require(path.join(__dirname + "/login"));
const wineinfo = require(path.join(__dirname + "/wineinfo"));
@@ -18,6 +20,12 @@ const lottery = require(path.join(__dirname + "/lottery"));
const router = express.Router();
router.get("/wineinfo/search", wineinfo.wineSearch);
router.get("/request/all", setAdminHeaderIfAuthenticated, request.getAllRequestedWines);
router.post("/request/new-wine", request.requestNewWine);
router.delete("/request/:id", request.deleteRequestedWineById);
router.get("/wineinfo/schema", mustBeAuthenticated, update.schema);
router.get("/wineinfo/:ean", wineinfo.byEAN);

View File

@@ -1,5 +1,51 @@
const fetch = require('node-fetch')
const path = require('path')
const config = require(path.join(__dirname + "/../config/env/lottery.config"));
const convertToOurWineObject = wine => {
if(wine.basic.ageLimit === "18"){
return {
name: wine.basic.productShortName,
vivinoLink: "https://www.vinmonopolet.no/p/" + wine.basic.productId,
rating: wine.basic.alcoholContent,
occurences: 0,
id: wine.basic.productId,
image: `https://bilder.vinmonopolet.no/cache/500x500-0/${wine.basic.productId}-1.jpg`,
price: wine.prices[0].salesPrice.toString(),
country: wine.origins.origin.country
}
}
}
const wineSearch = async (req, res) => {
const {query} = req.query
let url = new URL(`https://apis.vinmonopolet.no/products/v0/details-normal?productShortNameContains=test&maxResults=15`)
url.searchParams.set('productShortNameContains', query)
const vinmonopoletResponse = await fetch(url, {
headers: {
"Ocp-Apim-Subscription-Key": config.vinmonopoletToken
}
})
.then(resp => resp.json())
.catch(err => console.error(err))
if (vinmonopoletResponse.errors != null) {
return vinmonopoletResponse.errors.map(error => {
if (error.type == "UnknownProductError") {
return res.status(404).json({
message: error.message
})
} else {
return next()
}
})
}
const winesConverted = vinmonopoletResponse.map(convertToOurWineObject).filter(Boolean)
return res.send(winesConverted);
}
const byEAN = async (req, res) => {
const vinmonopoletResponse = await fetch("https://app.vinmonopolet.no/vmpws/v2/vmp/products/barCodeSearch/" + req.params.ean)
@@ -21,5 +67,6 @@ const byEAN = async (req, res) => {
};
module.exports = {
byEAN
byEAN,
wineSearch
};

View File

@@ -6,5 +6,6 @@ module.exports = {
date: 5,
hours: 15,
apiUrl: undefined,
gatewayToken: undefined
};
gatewayToken: undefined,
vinmonopoletToken: undefined
};

View File

@@ -0,0 +1,6 @@
const setAdminHeaderIfAuthenticated = (req, res, next) => {
res.set("Vinlottis-Admin", req.isAuthenticated());
return next();
};
module.exports = setAdminHeaderIfAuthenticated;

View File

@@ -6,7 +6,7 @@
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node server.js",
"dev": "cross-env NODE_ENV=development webpack-dev-server --progress",
"dev": "cross-env NODE_ENV=development webpack-dev-server",
"build": "cross-env NODE_ENV=production webpack --progress --hide-modules"
},
"author": "",

13
schemas/RequestedWine.js Normal file
View File

@@ -0,0 +1,13 @@
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const RequestedWine = new Schema({
count: Number,
wineId: String,
wine: {
type: Schema.Types.ObjectId,
ref: "Wine"
}
});
module.exports = mongoose.model("RequestedWine", RequestedWine);

View File

@@ -9,7 +9,6 @@ const User = require(path.join(__dirname + "/schemas/User"));
const apiRouter = require(path.join(__dirname + "/api/router.js"));
const loginApi = require(path.join(__dirname + "/api/login"));
const virtualApi = require(path.join(__dirname + "/api/virtualLottery"));
const subscriptionApi = require(path.join(__dirname + "/api/subscriptions"));
//This is required for the chat to work

View File

@@ -1,6 +1,6 @@
<template>
<div class="app-container">
<banner />
<banner :routes="routes"/>
<router-view />
<UpdateToast
v-if="showToast"
@@ -24,7 +24,29 @@ export default {
return {
showToast: false,
toastText: null,
refreshToast: false
refreshToast: false,
routes: [
{
name: "Dagens viner",
route: "/dagens/"
},
{
name: "Historie",
route: "/history/"
},
{
name: "Lotteriet",
route: "/lottery/game/"
},
{
name: "Foreslå vin",
route: "/request"
},
{
name: "Foreslåtte viner",
route: "/requested-wines"
},
]
};
},
mounted() {
@@ -45,7 +67,7 @@ export default {
methods: {
closeToast: function() {
this.showToast = false;
}
},
}
};
</script>

View File

@@ -1,3 +1,5 @@
import fetch from "node-fetch";
const BASE_URL = __APIURL__ || window.location.origin;
const statistics = () => {
@@ -24,6 +26,16 @@ const overallWineStatistics = () => {
return fetch(url.href).then(resp => resp.json());
};
const allRequestedWines = () => {
const url = new URL("/api/request/all", BASE_URL);
return fetch(url.href)
.then(resp => {
const isAdmin = resp.headers.get("vinlottis-admin") || false;
return Promise.all([resp.json(), Boolean(isAdmin)]);
});
};
const chartWinsByColor = () => {
const url = new URL("/api/purchase/statistics/color", BASE_URL);
@@ -108,6 +120,21 @@ const winners = () => {
return fetch(url.href).then(resp => resp.json());
};
const deleteRequestedWine = wineToBeDeleted => {
const url = new URL("api/request/"+ wineToBeDeleted._id, BASE_URL);
const options = {
headers: {
"Content-Type": "application/json"
},
method: "DELETE",
body: JSON.stringify(wineToBeDeleted)
};
return fetch(url.href, options).then(resp => resp.json())
}
const deleteWinners = () => {
const url = new URL("/api/virtual/winner/all", BASE_URL);
@@ -140,6 +167,23 @@ const attendees = () => {
return fetch(url.href).then(resp => resp.json());
};
const requestNewWine = (wine) => {
const options = {
body: JSON.stringify({
wine: wine
}),
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
method: "post"
}
const url = new URL("/api/request/new-wine", BASE_URL)
return fetch(url.href, options).then(resp => resp.json())
}
const logWines = wines => {
const url = new URL("/api/log/wines", BASE_URL);
@@ -174,6 +218,21 @@ const barcodeToVinmonopolet = id => {
});
};
const searchForWine = searchString => {
const url = new URL("/api/wineinfo/search?query=" + searchString, BASE_URL);
return fetch(url.href).then(async resp => {
if (!resp.ok) {
if (resp.status == 404) {
throw await resp.json();
}
} else {
return resp.json();
}
});
};
const handleErrors = async resp => {
if ([400, 409].includes(resp.status)) {
throw await resp.json();
@@ -287,6 +346,9 @@ export {
logWines,
wineSchema,
barcodeToVinmonopolet,
searchForWine,
requestNewWine,
allRequestedWines,
login,
register,
addAttendee,
@@ -297,6 +359,7 @@ export {
winnersSecure,
deleteWinners,
deleteAttendees,
deleteRequestedWine,
getChatHistory,
finishedDraw,
getAmIWinner,

View File

@@ -0,0 +1,52 @@
<template>
<main>
<h1>
Alle foreslåtte viner
</h1>
<section class="requested-wines-container">
<p v-if="wines == undefined || wines.length == 0">Ingen har foreslått noe enda!</p>
<RequestedWineCard v-for="requestedEl in wines" :key="requestedEl.id" :requestedElement="requestedEl" @wineDeleted="filterOutDeletedWine" :showDeleteButton="isAdmin"/>
</section>
</main>
</template>
<script>
import { allRequestedWines } from "@/api";
import RequestedWineCard from "@/ui/RequestedWineCard";
export default {
components: {
RequestedWineCard
},
data(){
return{
wines: undefined,
canRequest: true,
isAdmin: false
}
},
methods: {
filterOutDeletedWine(wine){
this.wines = this.wines.filter(item => item.wine._id !== wine._id)
},
async refreshData(){
[this.wines, this.isAdmin] = await allRequestedWines() || [[], false]
}
},
mounted() {
this.refreshData()
}
}
</script>
<style lang="scss" scoped>
h1{
text-align: center;
}
.requested-wines-container{
display: flex;
justify-content: space-around;
flex-flow: row wrap;
align-items: stretch
}
</style>

View File

@@ -0,0 +1,195 @@
<template>
<main>
<h1>
Foreslå en vin!
</h1>
<Modal
v-if="showModal"
modalText="Ønsket ditt har blitt lagt til"
:buttons="modalButtons"
@click="emitFromModalButton"
></Modal>
<section>
<section class="search-section">
<input type="text" v-model="searchString" @keyup.enter="fetchWineFromVin()" placeholder="Søk etter en vin du liker her!🍷">
<button :disabled="!searchString" @click="fetchWineFromVin()" class="vin-button">Søk</button>
</section>
<section v-for="(wine, index) in this.wines" :key="index" class="search-results-container">
<img
v-if="wine.image"
:src="wine.image"
class="wine-image"
:class="{ 'fullscreen': fullscreen }"
/>
<img v-else class="wine-placeholder" alt="Wine image" />
<section class="wine-info">
<h2 v-if="wine.name">{{ wine.name }}</h2>
<h2 v-else>(no name)</h2>
<div class="__details">
<span v-if="wine.rating">{{ wine.rating }}%</span>
<span v-if="wine.price">{{ wine.price }} NOK</span>
<span v-if="wine.country">{{ wine.country }}</span>
</div>
</section>
<section class="buttons">
<button class="vin-button" @click="request(wine)">Foreslå denne</button>
<a
v-if="wine.vivinoLink"
:href="wine.vivinoLink"
class="wine-link"
>Les mer polet</a>
</section>
</section>
<p v-if="this.wines && this.wines.length == 0">
Fant ingen viner med det navnet!
</p>
</section>
</main>
</template>
<script>
import { searchForWine, requestNewWine } from "@/api";
import Wine from "@/ui/Wine";
import Modal from "@/ui/Modal";
export default {
components: {
Wine,
Modal
},
data() {
return {
searchString: undefined,
wines: undefined,
showModal: false,
modalButtons: [
{
text: "Legg til flere viner",
action: "stay"
},
{
text: "Se alle viner",
action: "move"
}
]
}
},
methods: {
fetchWineFromVin(){
if(this.searchString){
this.wines = []
let localSearchString = this.searchString.replace(/ /g,"_");
searchForWine(localSearchString)
.then(res => this.wines = res)
}
},
request(wine){
requestNewWine(wine)
.then(() => this.showModal = true)
},
emitFromModalButton(action){
if(action == "stay"){
this.showModal = false
} else {
this.$router.push("/requested-wines");
}
}
},
}
</script>
<style lang="scss" scoped>
@import "./src/styles/media-queries";
@import "./src/styles/global";
@import "./src/styles/variables";
main{
margin: auto;
width: 80%;
text-align: center;
z-index: 0;
}
input[type="text"] {
width: 100%;
color: black;
border-radius: 4px;
padding: 0.5rem 1rem;
border: 1px solid black;
max-width: 80%;
}
.search-section{
display: flex;
justify-content: space-around;
flex-flow: row;
}
.search-results-container{
display: flex;
padding: 3px;
border-radius: 1px;
box-shadow: 0px 0px 0px 1px rgba(0,0,0,0.3);
margin: 1rem 0;
justify-content: space-around;
flex-flow: row wrap;
align-items: stretch;
.wine-image {
height: 100px;
}
.wine-placeholder {
height: 100px;
width: 70px;
}
.wine-info{
display: flex;
flex-direction: column;
.__details{
display: flex;
flex-direction: column;
}
}
.wine-link {
color: #333333;
font-family: Arial;
text-decoration: none;
font-weight: bold;
border-bottom: 1px solid #ff5fff;
width: fit-content;
}
.buttons{
display: flex;
align-items: center;
order: 2;
justify-content: space-between;
width: 40%;
margin-right: 1rem;
}
@include mobile {
display: flex;
flex-direction: column;
.wine-image {
height: 100px;
width: 50px;
align-self: center;
}
.buttons{
display: flex;
flex-direction: column;
align-self: center;
margin: 1em;
.wine-link{
margin-top: 1em;
}
}
}
}
</style>

View File

@@ -13,6 +13,9 @@ import LotteryPage from "@/components/LotteryPage";
import HistoryPage from "@/components/HistoryPage";
import HighscorePage from "@/components/HighscorePage";
import RequestWine from "@/components/RequestWine";
import AllRequestedWines from "@/components/AllRequestedWines";
const routes = [
{
path: "*",
@@ -57,6 +60,14 @@ const routes = [
{
path: "/highscore",
component: HighscorePage
},
{
path: "/request",
component: RequestWine
},
{
path: "/requested-wines",
component: AllRequestedWines
}
];

147
src/styles/banner.scss Normal file
View File

@@ -0,0 +1,147 @@
@import "./media-queries.scss";
// https://codepen.io/erikterwan/pen/EVzeRP
@include mobile{
#menuToggle
{
display: block;
position: relative;
margin: 7px;
z-index: 1;
-webkit-user-select: none;
user-select: none;
}
#menuToggle a
{
text-decoration: none;
color: #333333;
transition: color 0.3s ease;
}
#menuToggle input
{
display: block;
width: 40px;
height: 32px;
position: absolute;
top: -7px;
left: -5px;
cursor: pointer;
opacity: 0; /* hide this */
z-index: 2; /* and place it over the hamburger */
-webkit-touch-callout: none;
}
/*
* Just a quick hamburger
*/
#menuToggle span
{
display: block;
width: 33px;
height: 4px;
margin-bottom: 5px;
position: relative;
background: #333333;
border-radius: 3px;
z-index: 1;
transform-origin: 4px 0px;
transition: transform 0.5s cubic-bezier(0.77,0.2,0.05,1.0),
background 0.5s cubic-bezier(0.77,0.2,0.05,1.0),
opacity 0.55s ease;
}
#menuToggle span:first-child
{
transform-origin: 0% 0%;
}
#menuToggle span:nth-last-child(2)
{
transform-origin: 0% 100%;
}
/*
* Transform all the slices of hamburger
* into a crossmark.
*/
#menuToggle input:checked ~ span
{
opacity: 1;
transform: rotate(45deg) translate(-2px, -1px);
background: #232323;
}
/*
* But let's hide the middle one.
*/
#menuToggle input:checked ~ span:nth-last-child(3)
{
opacity: 0;
transform: rotate(0deg) scale(0.2, 0.2);
}
/*
* Ohyeah and the last one should go the other direction
*/
#menuToggle input:checked ~ span:nth-last-child(2)
{
transform: rotate(-45deg) translate(0, -1px);
}
/*
* Make this absolute positioned
* at the top left of the screen
*/
#menu
{
position: absolute;
width: 100vw;
margin: -100px 0 0 -50px;
padding-bottom: 10px;
padding-top: 125px;
background-color: $primary;
list-style-type: none;
-webkit-font-smoothing: antialiased;
/* to stop flickering of text in safari */
transform-origin: 0% 0%;
transform: translate(-100%, 0);
transition: transform 0.5s cubic-bezier(0.77,0.2,0.05,1.0);
}
#menu li
{
padding: 10px 0;
font-size: 22px;
}
/*
* And let's slide it in from the left
*/
#menuToggle input:checked ~ ul
{
transform: none;
}
}
@include desktop{
#menuToggle{
display: none;
}
}

View File

@@ -144,13 +144,17 @@ textarea {
0 16px 32px rgba(0, 0, 0, 0.07), 0 32px 64px rgba(0, 0, 0, 0.07);
}
&:hover {
&:hover:not(:disabled) {
transform: scale(1.02) translateZ(0);
&::after {
opacity: 1;
}
}
&:disabled{
opacity: 0.25;
cursor: not-allowed;
}
}

View File

@@ -1,18 +1,36 @@
<template>
<router-link to="/" class="link">
<div class="top-banner">
<img src="/public/assets/images/knowit.svg" alt="knowit logo" />
<div class="clock">
<h2 v-if="!fiveMinutesLeft || !tenMinutesOver">
<span v-if="days > 0">{{ pad(days) }}:</span>
<span>{{ pad(hours) }}</span>:
<span>{{ pad(minutes) }}</span>:
<span>{{ pad(seconds) }}</span>
</h2>
<h2 v-if="twoMinutesLeft || tenMinutesOver">Lotteriet er i gang!</h2>
</div>
<div class="top-banner">
<!-- Mobile -->
<div id="menuToggle" >
<input type="checkbox" />
<span></span>
<span></span>
<span></span>
<ul id="menu">
<router-link v-for="(route, index) in routes" :key="index" :to="route.route">
<li>{{route.name}}</li>
</router-link>
</ul>
</div>
</router-link>
<router-link to="/">
<img src="/public/assets/images/knowit.svg" alt="knowit logo" />
</router-link>
<div v-for="(route, index) in routes" :key="index" class="desktop">
<router-link :to="route.route" class="routes">
{{route.name}}
</router-link>
</div>
<div class="clock">
<h2 v-if="!fiveMinutesLeft || !tenMinutesOver">
<span v-if="days > 0">{{ pad(days) }}:</span>
<span>{{ pad(hours) }}</span>:
<span>{{ pad(minutes) }}</span>:
<span>{{ pad(seconds) }}</span>
</h2>
<h2 v-if="twoMinutesLeft || tenMinutesOver">Lotteriet er i gang!</h2>
</div>
</div>
</template>
<script>
@@ -25,12 +43,15 @@ export default {
minutes: 0,
seconds: 0,
distance: 0,
enabled: false,
code: "38384040373937396665",
codeDone: "",
interval: null
interval: null,
};
},
props: {
routes: {
required: true,
type: Array
}
},
mounted() {
this.initialize(), this.countdown();
},
@@ -55,19 +76,6 @@ export default {
}
return num;
},
listenerFunction: function(event) {
this.codeDone += event.keyCode;
if (this.code.substring(0, this.codeDone.length) == this.codeDone) {
if (this.code == this.codeDone && !this.enabled) {
this.enabled = true;
this.initialize();
this.countdown();
this.codeDone = "";
}
} else {
this.codeDone = "";
}
},
initialize: function() {
let d = new Date();
let dayOfLottery = __DATE__;
@@ -115,7 +123,7 @@ export default {
this.initialize();
}
this.interval = setTimeout(this.countdown, 500);
}
},
}
};
</script>
@@ -123,41 +131,56 @@ export default {
<style lang="scss" scoped>
@import "../styles/media-queries.scss";
@import "../styles/variables.scss";
@import "../styles/banner.scss";
.link {
text-decoration: none;
@include mobile {
.desktop {
display: none;
}
}
@include desktop {
.top-banner{
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
width: calc(100% - 20px);
.routes {
text-decoration: none;
color: #333333;
}
}
}
.top-banner {
text-align: center;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
width: calc(100% - 80px);
margin-top: 0px;
padding: 0px 40px;
width: calc(100% - 20px);
padding: 5px 10px;
background-color: $primary;
-webkit-box-shadow: 0px 0px 22px -8px rgba(0, 0, 0, 0.65);
-moz-box-shadow: 0px 0px 22px -8px rgba(0, 0, 0, 0.65);
box-shadow: 0px 0px 22px -8px rgba(0, 0, 0, 0.65);
@include mobile {
padding: 0px 40px;
> img {
height: 23px;
.clock {
text-decoration: none;
color: #333333;
display: flex;
font-family: Arial;
margin-right: 2rem;
@include mobile {
font-size: 0.8em;
margin-right: 1rem;
}
h2 {
display: flex;
}
}
}
.clock {
text-decoration: none;
color: #333333;
display: flex;
font-family: Arial;
h2 {
display: flex;
}
}
</style>

101
src/ui/Modal.vue Normal file
View File

@@ -0,0 +1,101 @@
<template>
<transition name="modal-fade">
<main class="modal-backdrop">
<section class="modal">
<header class="modal-header" v-if="headerText">
{{headerText}}
</header>
<section class="modal-body">
<p>
{{modalText}}
</p>
<section class="button-container">
<button v-for="(button, index) in buttons" :key="index" @click="modalButtonClicked(button.action)" class="vin-button">
{{button.text}}
</button>
</section>
</section>
</section>
</main>
</transition>
</template>
<script>
export default {
props: {
headerText: {
type: String,
required: false
},
modalText: {
type: String,
required: true
},
buttons: {
type: Array,
required: true
},
},
methods:{
modalButtonClicked(action){
this.$emit('click', action)
}
}
}
</script>
<style lang="scss" scoped>
@import "../styles/global.scss";
.modal-fade-enter,
.modal-fade-leave-active {
opacity: 0;
}
.modal-fade-enter-active,
.modal-fade-leave-active {
transition: opacity .5s ease
}
.modal-backdrop {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.3);
display: flex;
justify-content: center;
align-items: center;
z-index: 1;
width: 100vw;
height: 100vh;
}
.modal {
background: #FFFFFF;
-webkit-box-shadow: 0px 0px 22px 1px rgba(0, 0, 0, 0.65);
-moz-box-shadow: 0px 0px 22px 1px rgba(0, 0, 0, 0.65);
box-shadow: 0px 0px 22px 1px rgba(0, 0, 0, 0.65);
overflow-x: auto;
display: flex;
flex-direction: column;
}
.modal-header {
padding: 15px;
display: flex;
}
.modal-header {
border-bottom: 1px solid #eeeeee;
color: #4AAE9B;
justify-content: space-between;
}
.modal-body {
position: relative;
padding: 20px 10px;
}
</style>

View File

@@ -0,0 +1,80 @@
<template>
<div class="requested-wine">
<img
v-if="wine.image"
:src="wine.image"
class="wine-image"
:class="{ 'fullscreen': fullscreen }"
/>
<img v-else class="wine-placeholder" alt="Wine image" />
<section class="wine-info">
<h3 v-if="wine.name">{{ wine.name }}</h3>
<h3 v-else>(no name)</h3>
<p>Antall ganger denne har blitt foreslått: {{requestedElement.count}}</p>
<section class="buttons">
<button class="vin-button" @click="request(wine)" v-if="!locallyRequested">Foreslå denne</button>
<a
v-if="wine.vivinoLink"
:href="wine.vivinoLink"
class="wine-link"
>Les mer polet</a>
</section>
<button @click="deleteWine(wine)" v-if="showDeleteButton == true">
Slett vinen
</button>
</section>
</div>
</template>
<script>
import { deleteRequestedWine, requestNewWine } from "@/api";
export default {
data(){
return {
wine: this.requestedElement.wine,
locallyRequested: false
}
},
props: {
requestedElement: {
required: true,
type: Object
},
showDeleteButton: {
required: false,
type: Boolean,
default: false
}
},
methods: {
request(wine){
this.locallyRequested = true
this.requestedElement.count = this.requestedElement.count +1
requestNewWine(wine)
},
async deleteWine(wine) {
if (window.confirm("Er du sikker på at du vil slette vinen?")) {
let response = await deleteRequestedWine(wine);
if (response['success'] == true) {
this.$emit('wineDeleted', wine);
} else {
alert("Klarte ikke slette vinen");
}
}
},
},
}
</script>
<style lang="scss" scoped>
.requested-wine{
padding: 20px;
border-radius: 1px;
margin: 1rem 0;
-webkit-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.65);
-moz-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.65);
box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.65);
}
</style>