New admin pages with better isolated functionality.
Draw winner page has: - Better user feedback - Knows how many wines are left - Separat button for starting sms prize distribution. - Edit, update and delete wine by id. RegisterWinePage: - Import wine by url. - Edit, update and delete wine by id.
This commit is contained in:
356
frontend/components/admin/DrawWinnerPage.vue
Normal file
356
frontend/components/admin/DrawWinnerPage.vue
Normal file
@@ -0,0 +1,356 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<h1>Trekk vinnere</h1>
|
||||
|
||||
<div class="draw-winner-container">
|
||||
<div v-if="drawingWinner == false" class="draw-container">
|
||||
<input type="number" v-model="winnersToDraw" />
|
||||
<button class="vin-button no-margin" @click="startDrawingWinners">Trekk vinnere</button>
|
||||
</div>
|
||||
|
||||
<div v-if="wines.length" class="wines-left">
|
||||
<span>Antall vin igjen: {{ winnersToDraw }} av {{ wines.length }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="drawingWinner == true">
|
||||
<p>Trekker vinner {{ winners.length }} av {{ wines.length }}.</p>
|
||||
<p>Neste trekning om {{ secondsLeft }} sekunder av {{ drawTime }}</p>
|
||||
|
||||
<div class="button-container draw-winner-actions">
|
||||
<button class="vin-button danger" @click="stopDraw">
|
||||
Stopp trekning
|
||||
</button>
|
||||
<button
|
||||
class="vin-button"
|
||||
:class="{ 'pulse-button': secondsLeft == 0 }"
|
||||
:disabled="secondsLeft > 0"
|
||||
@click="drawWinner"
|
||||
>
|
||||
Trekk neste
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="prize-distribution">
|
||||
<h2>Prisutdeling</h2>
|
||||
|
||||
<div class="button-container">
|
||||
<button class="vin-button" @click="startPrizeDistribution">Start automatisk prisutdeling med SMS</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 v-if="winners.length > 0">Vinnere</h2>
|
||||
<div class="winners" v-if="winners.length > 0">
|
||||
<div :class="winner.color + '-raffle'" class="raffle-element" v-for="(winner, index) in winners" :key="index">
|
||||
<span>{{ winner.name }}</span>
|
||||
<span>Phone: {{ winner.phoneNumber }}</span>
|
||||
<span>Rød: {{ winner.red }}</span>
|
||||
<span>Blå: {{ winner.blue }}</span>
|
||||
<span>Grønn: {{ winner.green }}</span>
|
||||
<span>Gul: {{ winner.yellow }}</span>
|
||||
|
||||
<div class="button-container">
|
||||
<button class="vin-button small" @click="editingWinner = editingWinner == winner ? false : winner">
|
||||
{{ editingWinner == winner ? "Lukk" : "Rediger" }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="editingWinner == winner" class="edit">
|
||||
<div class="label-div" v-for="key in Object.keys(winner)" :key="key">
|
||||
<label>{{ key }}</label>
|
||||
<input type="text" v-model="winner[key]" :placeholder="key" />
|
||||
</div>
|
||||
|
||||
<div v-if="editingWinner == winner" class="button-container column">
|
||||
<button class="vin-button small" @click="notifyWinner(winner)">
|
||||
Send SMS
|
||||
</button>
|
||||
<button class="vin-button small warning" @click="updateWinner(winner)">
|
||||
Oppdater
|
||||
</button>
|
||||
<button class="vin-button small danger" @click="deleteWinner(winner)">
|
||||
Slett
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button-container margin-md" v-if="winners.length > 0">
|
||||
<button class="vin-button danger" v-if="winners.length > 0" @click="deleteAllWinners">
|
||||
Slett virtuelle vinnere
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
wines: [],
|
||||
drawingWinner: false,
|
||||
secondsLeft: 20,
|
||||
drawTime: 20,
|
||||
winners: [],
|
||||
editingWinner: undefined
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.fetchLotterWines();
|
||||
this.fetchLotterWinners();
|
||||
},
|
||||
computed: {
|
||||
winnersToDraw() {
|
||||
if (this.wines.length == undefined || this.winners.length == undefined) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return this.wines.length - this.winners.length;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
winners(val) {
|
||||
this.$emit("counter", val.length);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fetchLotterWines() {
|
||||
return fetch("/api/lottery/wines")
|
||||
.then(resp => resp.json())
|
||||
.then(response => (this.wines = response.wines));
|
||||
},
|
||||
fetchLotterWinners() {
|
||||
return fetch("/api/lottery/winners")
|
||||
.then(resp => resp.json())
|
||||
.then(response => (this.winners = response.winners));
|
||||
},
|
||||
countdown() {
|
||||
if (this.drawingWinner == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.secondsLeft > 0) {
|
||||
this.secondsLeft -= 1;
|
||||
|
||||
setTimeout(_ => {
|
||||
this.countdown();
|
||||
}, 1000);
|
||||
} else {
|
||||
if (this.winners.length == this.wines.length) {
|
||||
this.drawingWinner = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
startDrawingWinners() {
|
||||
if (window.confirm("Er du sikker på at du vil trekke vinnere?")) {
|
||||
this.drawWinner();
|
||||
}
|
||||
},
|
||||
drawWinner() {
|
||||
if (this.winnersToDraw <= 0) {
|
||||
this.$toast.error({ title: "No more wines to draw" });
|
||||
return;
|
||||
}
|
||||
this.secondsLeft = this.drawTime;
|
||||
this.drawingWinner = true;
|
||||
|
||||
fetch("/api/lottery/draw")
|
||||
.then(resp => resp.json())
|
||||
.then(response => {
|
||||
const { winner, color, success, message } = response;
|
||||
|
||||
if (success == false) {
|
||||
this.$toast.error({ title: message });
|
||||
return;
|
||||
}
|
||||
|
||||
winner.color = color;
|
||||
this.winners.push(winner);
|
||||
this.countdown();
|
||||
})
|
||||
.catch(error => {
|
||||
if (error) {
|
||||
this.$toast.error({ title: error.message });
|
||||
}
|
||||
this.drawingWinner = false;
|
||||
});
|
||||
},
|
||||
stopDraw() {
|
||||
this.drawingWinner = false;
|
||||
this.secondsLeft = this.drawTime;
|
||||
},
|
||||
startPrizeDistribution() {
|
||||
if (!window.confirm("Er du sikker på at du vil starte prisutdeling?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.drawingWinner = false;
|
||||
|
||||
const options = { method: "POST" };
|
||||
fetch(`/api/lottery/prize-distribution/start`, options)
|
||||
.then(resp => resp.json())
|
||||
.then(response => {
|
||||
if (response.success) {
|
||||
this.$toast.info({
|
||||
title: `Startet prisutdeling. SMS'er sendt ut!`
|
||||
});
|
||||
} else {
|
||||
this.$toast.error({
|
||||
title: `Klarte ikke starte prisutdeling`,
|
||||
description: response.message
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
notifyWinner(winner) {
|
||||
const options = { method: "POST" };
|
||||
|
||||
fetch(`/api/lottery/messages/winner/${winner.id}`, options)
|
||||
.then(resp => resp.json())
|
||||
.then(response => {
|
||||
if (response.success) {
|
||||
this.$toast.info({
|
||||
title: `Sendte sms til vinner ${winner.name}.`
|
||||
});
|
||||
} else {
|
||||
this.$toast.error({
|
||||
title: `Klarte ikke sende sms til vinner ${winner.name}`,
|
||||
description: response.message
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
updateWinner(winner) {
|
||||
const options = {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ winner })
|
||||
};
|
||||
|
||||
fetch(`/api/lottery/winner/${winner.id}`, options)
|
||||
.then(resp => resp.json())
|
||||
.then(response => {
|
||||
if (response.success) {
|
||||
this.$toast.info({
|
||||
title: `Oppdaterte vinner ${winner.name}.`
|
||||
});
|
||||
} else {
|
||||
this.$toast.error({
|
||||
title: `Klarte ikke oppdatere vinner ${winner.name}`,
|
||||
description: response.message
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
deleteWinner(winner) {
|
||||
if (winner._id != null && window.confirm(`Er du sikker på at du vil slette vinner ${winner.name}?`)) {
|
||||
const options = { method: "DELETE" };
|
||||
|
||||
fetch(`/api/lottery/winner/${winner.id}`, options)
|
||||
.then(resp => resp.json())
|
||||
.then(response => {
|
||||
if (response.success) {
|
||||
this.winners = this.winners.filter(w => w.id != winner.id);
|
||||
|
||||
this.$toast.info({
|
||||
title: `Slettet vinner ${winner.name}.`
|
||||
});
|
||||
} else {
|
||||
this.$toast.error({
|
||||
title: `Klarte ikke slette vinner ${winner.name}`,
|
||||
description: response.message
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
deleteAllWinners() {
|
||||
if (window.confirm("Er du sikker på at du vil slette alle vinnere?")) {
|
||||
const options = { method: "DELETE" };
|
||||
|
||||
fetch("/api/lottery/winners", options)
|
||||
.then(resp => resp.json())
|
||||
.then(response => {
|
||||
if (response.success) {
|
||||
this.winners = [];
|
||||
this.$toast.info({
|
||||
title: "Slettet alle vinnere."
|
||||
});
|
||||
} else {
|
||||
this.$toast.error({
|
||||
title: "Klarte ikke slette vinnere",
|
||||
description: response.message
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.wines-left {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 1rem;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.draw-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
input {
|
||||
font-size: 1.7rem;
|
||||
padding: 7px;
|
||||
margin: 0;
|
||||
width: 10rem;
|
||||
height: 3rem;
|
||||
border: 1px solid rgba(#333333, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.button-container {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.draw-winner-actions {
|
||||
justify-content: left;
|
||||
}
|
||||
|
||||
.winners {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
|
||||
.raffle-element {
|
||||
width: 220px;
|
||||
height: 100%;
|
||||
min-height: 250px;
|
||||
font-size: 1.1rem;
|
||||
padding: 1rem;
|
||||
font-weight: 500;
|
||||
// text-align: center;
|
||||
|
||||
-webkit-mask-size: cover;
|
||||
-moz-mask-size: cover;
|
||||
mask-size: cover;
|
||||
flex-direction: column;
|
||||
|
||||
span:first-of-type {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
span.active {
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
.edit {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
59
frontend/components/admin/PushPage.vue
Normal file
59
frontend/components/admin/PushPage.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<h1>Send push melding</h1>
|
||||
|
||||
<div class="notification-element">
|
||||
<div class="label-div">
|
||||
<label for="notification">Melding</label>
|
||||
<textarea id="notification" type="text" rows="3" v-model="pushMessage" placeholder="Push meldingtekst" />
|
||||
</div>
|
||||
<div class="label-div">
|
||||
<label for="notification-link">Push åpner lenke</label>
|
||||
<input id="notification-link" type="text" v-model="pushLink" placeholder="Push-click link" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-container margin-top-sm">
|
||||
<button class="vin-button" @click="sendPush">Send push</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
pushMessage: "",
|
||||
pushLink: "/"
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
sendPush: async function() {
|
||||
const options = {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message: this.pushMessage,
|
||||
link: this.pushLink
|
||||
})
|
||||
};
|
||||
|
||||
return fetch("/subscription/send-notification", options)
|
||||
.then(resp => resp.json())
|
||||
.then(response => {
|
||||
if (response.success) {
|
||||
this.$toast.info({
|
||||
title: "Sendt!"
|
||||
});
|
||||
} else {
|
||||
this.$toast.error({
|
||||
title: "Noe gikk galt!",
|
||||
description: response.message
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
308
frontend/components/admin/RegisterWinePage.vue
Normal file
308
frontend/components/admin/RegisterWinePage.vue
Normal file
@@ -0,0 +1,308 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Register vin</h1>
|
||||
|
||||
<ScanToVinmonopolet @wine="wineFromVinmonopoletScan" v-if="showCamera" />
|
||||
|
||||
<div class="button-container">
|
||||
<button class="vin-button" @click="showCamera = !showCamera">
|
||||
{{ showCamera ? "Skjul camera" : "Legg til vin med camera" }}
|
||||
</button>
|
||||
|
||||
<button class="vin-button" @click="manualyFillInnWine">
|
||||
Legg til en vin manuelt
|
||||
</button>
|
||||
|
||||
<button class="vin-button" @click="showImportLink = !showImportLink">
|
||||
{{ showImportLink ? "Skjul importer fra link" : "Importer fra link" }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="showImportLink" class="import-from-link">
|
||||
<label>Importer vin fra vinmonopolet link:</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Vinmonopol lenke"
|
||||
ref="vinmonopoletLinkInput"
|
||||
autocapitalize="none"
|
||||
@input="addWineByUrl"
|
||||
/>
|
||||
|
||||
<div v-if="linkError" class="error">
|
||||
{{ linkError }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="wines.length > 0" class="wine-edit-container">
|
||||
<h2>Dagens registrerte viner</h2>
|
||||
|
||||
<div>
|
||||
<button class="vin-button" @click="sendWines">Send inn dagens viner</button>
|
||||
</div>
|
||||
|
||||
<div class="wines">
|
||||
<wine v-for="wine in wines" :key="wine.id" :wine="wine">
|
||||
<template v-slot:default>
|
||||
<div v-if="editingWine == wine" class="wine-edit">
|
||||
<div class="label-div" v-for="key in Object.keys(wine)" :key="key">
|
||||
<label>{{ key }}</label>
|
||||
<input type="text" v-model="wine[key]" :placeholder="key" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:bottom>
|
||||
<div class="button-container row small">
|
||||
<button v-if="editingWine == wine && wine._id" class="vin-button warning" @click="updateWine(wine)">
|
||||
Oppdater vin
|
||||
</button>
|
||||
|
||||
<button class="vin-button" @click="editingWine = editingWine == wine ? false : wine">
|
||||
{{ editingWine == wine ? "Lukk" : "Rediger" }}
|
||||
</button>
|
||||
|
||||
<button class="danger vin-button" @click="deleteWine(wine)">
|
||||
Slett
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</wine>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button-container" v-if="wines.length > 0"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ScanToVinmonopolet from "@/ui/ScanToVinmonopolet";
|
||||
import Wine from "@/ui/Wine";
|
||||
|
||||
export default {
|
||||
components: { ScanToVinmonopolet, Wine },
|
||||
data() {
|
||||
return {
|
||||
wines: [],
|
||||
editingWine: undefined,
|
||||
showCamera: false,
|
||||
showImportLink: false,
|
||||
linkError: undefined
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
wines() {
|
||||
this.$emit("counter", this.wines.length);
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.fetchLotterWines();
|
||||
},
|
||||
methods: {
|
||||
fetchLotterWines() {
|
||||
fetch("/api/lottery/wines")
|
||||
.then(resp => resp.json())
|
||||
.then(response => (this.wines = response.wines));
|
||||
},
|
||||
wineFromVinmonopoletScan(wineResponse) {
|
||||
if (this.wines.map(wine => wine.name).includes(wineResponse.name)) {
|
||||
this.toastText = "Vinen er allerede lagt til.";
|
||||
this.showToast = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.toastText = "Fant og la til vin:<br>" + wineResponse.name;
|
||||
this.showToast = true;
|
||||
|
||||
this.wines.unshift(wineResponse);
|
||||
},
|
||||
manualyFillInnWine() {
|
||||
fetch("/api/lottery/wine/schema")
|
||||
.then(resp => resp.json())
|
||||
.then(response => response.schema)
|
||||
.then(wineSchema => {
|
||||
this.editingWine = wineSchema;
|
||||
this.wines.unshift(wineSchema);
|
||||
});
|
||||
},
|
||||
addWineByUrl(event) {
|
||||
const url = event.target.value;
|
||||
this.linkError = null;
|
||||
|
||||
if (!url.includes("vinmonopolet.no")) {
|
||||
this.linkError = "Dette er ikke en gydlig vinmonopolet lenke.";
|
||||
return;
|
||||
}
|
||||
const id = url.split("/").pop();
|
||||
|
||||
fetch(`/api/vinmonopolet/wine/by-id/${id}`)
|
||||
.then(resp => resp.json())
|
||||
.then(response => {
|
||||
const { wine } = response;
|
||||
this.wines.unshift(wine);
|
||||
this.$refs.vinmonopoletLinkInput.value = "";
|
||||
});
|
||||
},
|
||||
sendWines() {
|
||||
const filterOutExistingWines = wine => wine["_id"] == null;
|
||||
|
||||
const options = {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
wines: this.wines.filter(filterOutExistingWines)
|
||||
})
|
||||
};
|
||||
|
||||
fetch("/api/lottery/wines", options).then(resp => {
|
||||
try {
|
||||
if (resp.ok == false) {
|
||||
throw resp;
|
||||
}
|
||||
|
||||
resp.json().then(response => {
|
||||
if (response.success == false) {
|
||||
throw response;
|
||||
} else {
|
||||
this.$toast.info({
|
||||
title: "Viner sendt inn!",
|
||||
timeout: 4000
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
this.$toast.error({
|
||||
title: "Feil oppsto ved innsending!",
|
||||
description: error.message,
|
||||
timeout: 4000
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
updateWine(updatedWine) {
|
||||
const options = {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ wine: updatedWine })
|
||||
};
|
||||
|
||||
fetch(`/api/lottery/wine/${updatedWine._id}`, options)
|
||||
.then(resp => resp.json())
|
||||
.then(response => {
|
||||
this.editingWine = null;
|
||||
|
||||
if (response.success) {
|
||||
this.$toast.info({
|
||||
title: response.message
|
||||
});
|
||||
} else {
|
||||
this.$toast.error({
|
||||
title: response.message
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
deleteWine(deletedWine) {
|
||||
this.wines = this.wines.filter(wine => wine.name != deletedWine.name);
|
||||
|
||||
if (deletedWine._id == null) return;
|
||||
|
||||
const options = { method: "DELETE" };
|
||||
fetch(`/api/lottery/wine/${deletedWine._id}`, options)
|
||||
.then(resp => resp.json())
|
||||
.then(response => {
|
||||
this.editingWine = null;
|
||||
|
||||
this.$toast.info({
|
||||
title: response.message
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/media-queries.scss";
|
||||
@import "@/styles/variables.scss";
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
margin: 1.5rem 0 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.row {
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
.import-from-link {
|
||||
width: 70%;
|
||||
max-width: 800px;
|
||||
margin: 1.5rem auto 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
font-size: 1rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
input {
|
||||
font-size: 1.5rem;
|
||||
min-height: 2rem;
|
||||
line-height: 2rem;
|
||||
border: none;
|
||||
border-bottom: 1px solid black;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.error {
|
||||
margin-top: 0.5rem;
|
||||
padding: 1.25rem;
|
||||
background-color: $light-red;
|
||||
color: $red;
|
||||
font-size: 1.3rem;
|
||||
|
||||
@include mobile {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wine-edit-container {
|
||||
max-width: 1500px;
|
||||
padding: 2rem;
|
||||
margin: 0 auto;
|
||||
|
||||
.wines {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
|
||||
> div {
|
||||
margin: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
margin-top: 0.7rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
margin-top: 1rem;
|
||||
|
||||
button:not(:last-child) {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user