Develop #8

Open
wazolab wants to merge 50 commits from develop into master
7 changed files with 329 additions and 29 deletions
Showing only changes of commit 51c57cb80c - Show all commits

View File

@@ -4,19 +4,26 @@
<main>
<router-view />
</main>
<Modal v-if="modalIsOpen" />
<Footer />
</div>
</template>
<script>
import { mapState } from "vuex";
import Footer from "@/components/Footer";
import Header from "@/components/Header";
import Modal from "@/components/Modal";
export default {
name: "App",
components: {
Footer,
Header
Header,
Modal
},
computed: {
...mapState(["modalIsOpen"])
}
};
</script>
@@ -40,27 +47,26 @@ a {
main {
padding: 2rem 1rem;
background-color: @color-2;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='88' height='88' viewBox='0 0 88 88'%3E%3Cg fill='%230c9696' fill-opacity='0.35'%3E%3Cpath fill-rule='evenodd' d='M29.42 29.41c.36-.36.58-.85.58-1.4V0h-4v26H0v4h28c.55 0 1.05-.22 1.41-.58h.01zm0 29.18c.36.36.58.86.58 1.4V88h-4V62H0v-4h28c.56 0 1.05.22 1.41.58zm29.16 0c-.36.36-.58.85-.58 1.4V88h4V62h26v-4H60c-.55 0-1.05.22-1.41.58h-.01zM62 26V0h-4v28c0 .55.22 1.05.58 1.41.37.37.86.59 1.41.59H88v-4H62zM18 36c0-1.1.9-2 2-2h10a2 2 0 1 1 0 4H20a2 2 0 0 1-2-2zm0 16c0-1.1.9-2 2-2h10a2 2 0 1 1 0 4H20a2 2 0 0 1-2-2zm16-26a2 2 0 0 1 2-2 2 2 0 0 1 2 2v4a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-4zm16 0a2 2 0 0 1 2-2 2 2 0 0 1 2 2v4a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-4zM34 58a2 2 0 0 1 2-2 2 2 0 0 1 2 2v4a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-4zm16 0a2 2 0 0 1 2-2 2 2 0 0 1 2 2v4a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-4zM34 78a2 2 0 0 1 2-2 2 2 0 0 1 2 2v6a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-6zm16 0a2 2 0 0 1 2-2 2 2 0 0 1 2 2v6a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-6zM34 4a2 2 0 0 1 2-2 2 2 0 0 1 2 2v6a2 2 0 0 1-2 2 2 2 0 0 1-2-2V4zm16 0a2 2 0 0 1 2-2 2 2 0 0 1 2 2v6a2 2 0 0 1-2 2 2 2 0 0 1-2-2V4zm-8 82a2 2 0 1 1 4 0v2h-4v-2zm0-68a2 2 0 1 1 4 0v10a2 2 0 1 1-4 0V18zM66 4a2 2 0 1 1 4 0v8a2 2 0 1 1-4 0V4zm0 72a2 2 0 1 1 4 0v8a2 2 0 1 1-4 0v-8zm-48 0a2 2 0 1 1 4 0v8a2 2 0 1 1-4 0v-8zm0-72a2 2 0 1 1 4 0v8a2 2 0 1 1-4 0V4zm24-4h4v2a2 2 0 1 1-4 0V0zm0 60a2 2 0 1 1 4 0v10a2 2 0 1 1-4 0V60zm14-24c0-1.1.9-2 2-2h10a2 2 0 1 1 0 4H58a2 2 0 0 1-2-2zm0 16c0-1.1.9-2 2-2h10a2 2 0 1 1 0 4H58a2 2 0 0 1-2-2zm-28-6a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm8 26a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm16 0a2 2 0 1 0 0-4 2 2 0 0 0 0 4zM36 20a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm16 0a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm-8-8a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm0 68a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm16-34a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm16-12a2 2 0 1 0 0 4 6 6 0 1 1 0 12 2 2 0 1 0 0 4 10 10 0 1 0 0-20zm-64 0a2 2 0 1 1 0 4 6 6 0 1 0 0 12 2 2 0 1 1 0 4 10 10 0 1 1 0-20zm56-12a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm0 48a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm-48 0a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm0-48a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm24 32a10 10 0 1 1 0-20 10 10 0 0 1 0 20zm0-4a6 6 0 1 0 0-12 6 6 0 0 0 0 12zm36-36a6 6 0 1 1 0-12 6 6 0 0 1 0 12zm0-4a2 2 0 1 0 0-4 2 2 0 0 0 0 4zM10 44c0-1.1.9-2 2-2h8a2 2 0 1 1 0 4h-8a2 2 0 0 1-2-2zm56 0c0-1.1.9-2 2-2h8a2 2 0 1 1 0 4h-8a2 2 0 0 1-2-2zm8 24c0-1.1.9-2 2-2h8a2 2 0 1 1 0 4h-8a2 2 0 0 1-2-2zM3 68c0-1.1.9-2 2-2h8a2 2 0 1 1 0 4H5a2 2 0 0 1-2-2zm0-48c0-1.1.9-2 2-2h8a2 2 0 1 1 0 4H5a2 2 0 0 1-2-2zm71 0c0-1.1.9-2 2-2h8a2 2 0 1 1 0 4h-8a2 2 0 0 1-2-2zm6 66a6 6 0 1 1 0-12 6 6 0 0 1 0 12zm0-4a2 2 0 1 0 0-4 2 2 0 0 0 0 4zM8 86a6 6 0 1 1 0-12 6 6 0 0 1 0 12zm0-4a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm0-68A6 6 0 1 1 8 2a6 6 0 0 1 0 12zm0-4a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm36 36a2 2 0 1 0 0-4 2 2 0 0 0 0 4z'/%3E%3C/g%3E%3C/svg%3E");
background-attachment: fixed;
background-color: rgba(0, 0, 0, 0.06);
min-height: calc(100vh - @headerHeight - @footerHeight);
}
h2 {
margin: 0 auto 2em auto;
h2 {
margin: 0 auto 1em auto;
width: max-content;
padding: 0.85rem 2rem 0.75rem 2rem;
border-radius: 25px;
font-size: 28px;
text-align: center;
background-color: #ffffff;
}
}
h3 {
h3 {
font-size: 22px;
font-weight: lighter;
text-transform: capitalize;
margin-top: 0;
}
}
</style>

View File

@@ -4,6 +4,8 @@
<img src="https://www.cycloid.io/themes/cycloid/images/owl_logo.png" alt="Cycloid.io" />
<h1>Fruits</h1>
</router-link>
<button class="action-btn" @click="() => $store.commit('toggleModal')">+</button>
</header>
</template>
@@ -18,10 +20,6 @@ header {
background-color: @color-2;
box-shadow: 0 0px 14px 0px #cecece;
> a {
padding: 0.5rem;
}
h1 {
margin: 0;
font-size: 28px;
@@ -33,15 +31,17 @@ header {
top: 0;
left: 0;
right: 0;
transition: all 0.3s ease-in-out;
width: 100%;
height: @headerHeight;
z-index: 15;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 1rem;
height: @headerHeight;
transition: all 0.3s ease-in-out;
a {
display: flex;
align-items: center;
}
img {
width: 35px;
@@ -49,5 +49,18 @@ header {
margin-right: 1rem;
}
}
.action-btn {
width: 42px;
height: 42px;
border: none;
border-radius: 50%;
background-color: @color-1;
color: @color-2;
font-size: 28px;
font-weight: lighter;
text-align: center;
}
}
}
</style>

185
src/components/Modal.vue Normal file
View File

@@ -0,0 +1,185 @@
<template>
<div class="modal">
<h2>New Fruit</h2>
<form id="new-fruit">
<!-- name -->
<p>
<input type="text" v-model="fruit.name" placeholder="Name" />
</p>
<!-- image finder (with Unsplash API) -->
<div class="image-uploader">
<label for="fruit-thumbnail">Image</label>
<!-- Loading placeholder -->
<Skeleton v-if="loading" width="100%" height="180px" />
<!-- Image preview -->
<img
class="preview"
v-if="!loading && imagePreview"
:src="imagePreview.urls.regular"
:alt="imagePreview.alt_description"
/>
<!-- API error messages -->
<p class="error" v-if="error">{{ error }}</p>
<!-- Search input -->
<div class="search-box">
<input
id="fruit-thumbnail"
type="search"
placeholder="Search: strawberry, orange ..."
@change="handleSearch"
/>
</div>
</div>
<!-- taste -->
<p>
<input type="text" v-model="fruit.taste" placeholder="Taste" />
</p>
<!-- color -->
<p>
<input type="color" v-model="fruit.color" placeholder="Color" />
</p>
<!-- price -->
<p>
<input type="number" v-model="fruit.price" placeholder="Price" />
</p>
<!-- expires -->
<p>
<input type="date" v-model="fruit.expires" placeholder="Expiry date" />
</p>
<!-- description -->
<p>
<textarea v-model="fruit.description" placeholder="Message ..." />
</p>
</form>
</div>
</template>
<script>
import Skeleton from "./Skeleton";
export default {
name: "Modal",
components: { Skeleton },
data() {
return {
loading: false,
fruit: {},
error: null,
imagePreview: null
};
},
methods: {
handleUpload(e) {
console.log(e);
},
async handleSearch(e) {
this.error = null;
if (e.target.value != "") {
this.loading = true;
this.$store
.dispatch("getImageFromUnsplash", e.target.value)
.then(res => (this.imagePreview = res))
.catch(err => (this.error = err.message))
.finally(() => (this.loading = false));
} else this.imagePreview = null;
}
}
};
</script>
<style lang="less" scoped>
.modal {
position: absolute;
top: @headerHeight;
left: 0;
right: 0;
bottom: 0;
height: 100%;
background-color: @color-2;
padding: 1rem;
z-index: 14;
form {
border: 2px solid @color-1;
border-radius: 10px;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='88' height='88' viewBox='0 0 88 88'%3E%3Cg fill='%230c9696' fill-opacity='0.35'%3E%3Cpath fill-rule='evenodd' d='M29.42 29.41c.36-.36.58-.85.58-1.4V0h-4v26H0v4h28c.55 0 1.05-.22 1.41-.58h.01zm0 29.18c.36.36.58.86.58 1.4V88h-4V62H0v-4h28c.56 0 1.05.22 1.41.58zm29.16 0c-.36.36-.58.85-.58 1.4V88h4V62h26v-4H60c-.55 0-1.05.22-1.41.58h-.01zM62 26V0h-4v28c0 .55.22 1.05.58 1.41.37.37.86.59 1.41.59H88v-4H62zM18 36c0-1.1.9-2 2-2h10a2 2 0 1 1 0 4H20a2 2 0 0 1-2-2zm0 16c0-1.1.9-2 2-2h10a2 2 0 1 1 0 4H20a2 2 0 0 1-2-2zm16-26a2 2 0 0 1 2-2 2 2 0 0 1 2 2v4a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-4zm16 0a2 2 0 0 1 2-2 2 2 0 0 1 2 2v4a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-4zM34 58a2 2 0 0 1 2-2 2 2 0 0 1 2 2v4a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-4zm16 0a2 2 0 0 1 2-2 2 2 0 0 1 2 2v4a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-4zM34 78a2 2 0 0 1 2-2 2 2 0 0 1 2 2v6a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-6zm16 0a2 2 0 0 1 2-2 2 2 0 0 1 2 2v6a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-6zM34 4a2 2 0 0 1 2-2 2 2 0 0 1 2 2v6a2 2 0 0 1-2 2 2 2 0 0 1-2-2V4zm16 0a2 2 0 0 1 2-2 2 2 0 0 1 2 2v6a2 2 0 0 1-2 2 2 2 0 0 1-2-2V4zm-8 82a2 2 0 1 1 4 0v2h-4v-2zm0-68a2 2 0 1 1 4 0v10a2 2 0 1 1-4 0V18zM66 4a2 2 0 1 1 4 0v8a2 2 0 1 1-4 0V4zm0 72a2 2 0 1 1 4 0v8a2 2 0 1 1-4 0v-8zm-48 0a2 2 0 1 1 4 0v8a2 2 0 1 1-4 0v-8zm0-72a2 2 0 1 1 4 0v8a2 2 0 1 1-4 0V4zm24-4h4v2a2 2 0 1 1-4 0V0zm0 60a2 2 0 1 1 4 0v10a2 2 0 1 1-4 0V60zm14-24c0-1.1.9-2 2-2h10a2 2 0 1 1 0 4H58a2 2 0 0 1-2-2zm0 16c0-1.1.9-2 2-2h10a2 2 0 1 1 0 4H58a2 2 0 0 1-2-2zm-28-6a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm8 26a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm16 0a2 2 0 1 0 0-4 2 2 0 0 0 0 4zM36 20a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm16 0a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm-8-8a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm0 68a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm16-34a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm16-12a2 2 0 1 0 0 4 6 6 0 1 1 0 12 2 2 0 1 0 0 4 10 10 0 1 0 0-20zm-64 0a2 2 0 1 1 0 4 6 6 0 1 0 0 12 2 2 0 1 1 0 4 10 10 0 1 1 0-20zm56-12a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm0 48a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm-48 0a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm0-48a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm24 32a10 10 0 1 1 0-20 10 10 0 0 1 0 20zm0-4a6 6 0 1 0 0-12 6 6 0 0 0 0 12zm36-36a6 6 0 1 1 0-12 6 6 0 0 1 0 12zm0-4a2 2 0 1 0 0-4 2 2 0 0 0 0 4zM10 44c0-1.1.9-2 2-2h8a2 2 0 1 1 0 4h-8a2 2 0 0 1-2-2zm56 0c0-1.1.9-2 2-2h8a2 2 0 1 1 0 4h-8a2 2 0 0 1-2-2zm8 24c0-1.1.9-2 2-2h8a2 2 0 1 1 0 4h-8a2 2 0 0 1-2-2zM3 68c0-1.1.9-2 2-2h8a2 2 0 1 1 0 4H5a2 2 0 0 1-2-2zm0-48c0-1.1.9-2 2-2h8a2 2 0 1 1 0 4H5a2 2 0 0 1-2-2zm71 0c0-1.1.9-2 2-2h8a2 2 0 1 1 0 4h-8a2 2 0 0 1-2-2zm6 66a6 6 0 1 1 0-12 6 6 0 0 1 0 12zm0-4a2 2 0 1 0 0-4 2 2 0 0 0 0 4zM8 86a6 6 0 1 1 0-12 6 6 0 0 1 0 12zm0-4a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm0-68A6 6 0 1 1 8 2a6 6 0 0 1 0 12zm0-4a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm36 36a2 2 0 1 0 0-4 2 2 0 0 0 0 4z'/%3E%3C/g%3E%3C/svg%3E");
background-attachment: scroll;
background-color: rgba(0, 0, 0, 0.06);
box-shadow: 0 1px 4px 1px #d2d2f2;
padding: 1rem;
label {
display: block;
font-weight: bold;
margin-bottom: 0.5rem;
}
input,
.image-uploader {
box-sizing: border-box;
width: 100%;
border-radius: 4px;
border: 1px solid #cecece;
background-color: @color-2;
padding: 0.75rem 1rem;
}
.image-uploader {
.preview,
.skeleton {
margin-bottom: 1rem;
}
.preview {
width: 100%;
object-fit: scale-down;
height: 180px;
}
.error {
color: #d63031;
text-align: center;
}
.search-box {
position: relative;
z-index: 0;
&:focus-within::after {
opacity: 1;
}
&::after {
content: url("data:image/svg+xml,%3Csvg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='0 0 350.439 350.439' style='enable-background:new 0 0 350.439 350.439;' xml:space='preserve'%3E%3Cg%3E%3Cpath d='M312.856,36.464H202.507c-4.87,0-8.83,3.961-8.83,8.832v138.422H52.371c-4.87,0-8.832,3.973-8.832,8.843v108.023 c0,4.87,3.961,8.831,8.832,8.831l260.474-0.071c4.873,0,8.838-3.962,8.838-8.843l0.011-255.211 C321.701,40.425,317.737,36.464,312.856,36.464z M175.216,253.617c0,5.724-4.635,10.353-10.356,10.353h-57.814 c-0.392,0-0.775-0.022-1.155-0.065v0.887l8.294,21.002c0.281,0.7,0.057,1.51-0.551,1.965c-0.301,0.229-0.658,0.344-1.013,0.344 c-0.364,0-0.722-0.12-1.025-0.355l-41.251-31.715c-0.416-0.318-0.657-0.816-0.657-1.33c0-0.531,0.241-1.023,0.657-1.345 l41.251-31.712c0.6-0.463,1.433-0.463,2.038-0.011c0.603,0.459,0.827,1.258,0.551,1.958l-7.776,19.694 c0.21-0.011,0.418-0.033,0.637-0.033h47.462v-34.715c0-5.724,4.637-10.353,10.352-10.353c5.721,0,10.356,4.629,10.356,10.353 V253.617z M332.215,7.41H174.623c-10.062,0-18.221,8.159-18.221,18.225v126.679H18.223C8.159,152.314,0,160.479,0,170.541v154.267 c0,10.063,8.159,18.222,18.223,18.222h157.588c0.346,0,0.667-0.087,1.008-0.099h155.375c10.065,0,18.228-8.164,18.228-18.232 V180.074c0-0.066,0.018-0.121,0.018-0.176V25.635C350.439,15.569,342.273,7.41,332.215,7.41z M329.535,300.512 c0,9.205-7.483,16.69-16.69,16.69H204.052c-0.078,0.043-0.383,0.076-0.705,0.076H52.371c-9.202,0-16.688-7.485-16.688-16.689 V192.571c0-9.209,7.485-16.689,16.688-16.689h133.453V45.295c0-9.201,7.485-16.687,16.684-16.687h110.349 c9.204,0,16.689,7.486,16.689,16.687L329.535,300.512z'/%3E%3C/g%3E%3C/svg%3E");
position: absolute;
z-index: 1;
top: 50%;
right: 0.3rem;
width: 24px;
height: 24px;
transform: translateY(-50%);
opacity: 0.4;
}
[type="search"] {
padding-right: 2.35rem;
border: none;
background-color: lighten(#cecece, 15%);
}
}
[type="file"] {
border: none;
}
}
}
}
</style>

View File

@@ -0,0 +1,77 @@
<template>
<span :style="{ height, width: computedWidth }" class="skeleton" />
</template>
<script>
export default {
name: "Skeleton",
props: {
maxWidth: {
// The default maxiumum width is 100%.
default: 100,
type: Number
},
minWidth: {
// Lines have a minimum width of 80%.
default: 80,
type: Number
},
height: {
// Make lines the same height as text.
default: "1em",
type: String
},
width: {
// Make it possible to define a fixed
// width instead of using a random one.
default: null,
type: String
}
},
computed: {
computedWidth() {
// Either use the given fixed width or
// a random width between the given min
// and max values.
return (
this.width ||
`${Math.floor(Math.random() * (this.maxWidth - this.minWidth) + this.minWidth)}%`
);
}
}
};
</script>
<style lang="less">
.skeleton {
display: inline-block;
position: relative;
overflow: hidden;
vertical-align: middle;
background-color: darken(#dddbdd, 8%);
&::after {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
transform: translateX(-100%);
background-image: linear-gradient(
90deg,
rgba(#fff, 0) 0,
rgba(#fff, 0.2) 20%,
rgba(#fff, 0.5) 60%,
rgba(#fff, 0)
);
animation: shimmer 5s infinite;
content: "";
}
@keyframes shimmer {
100% {
transform: translateX(100%);
}
}
}
</style>

View File

@@ -8,5 +8,20 @@ export default {
getFruit: async ({ commit }, id) => {
const response = await axios.get(`http://localhost:3000/fruit/${id}`);
commit("setFruit", response.data);
},
getImageFromUnsplash: async (_, keyword) => {
const response = await axios.get(
`https://api.unsplash.com/search/photos?query=${keyword}&w=800&h=400`,
{
headers: {
Authorization: "Client-ID NjFwGYsnksnzH2uh12W55aobUpTe0h06oSVACd5cWt0",
"X-Total": 1
}
}
);
if (response.data.total === 0) throw new Error("0 found.");
return response.data.results[0];
}
};

View File

@@ -6,7 +6,8 @@ import getters from "./getters";
export const state = {
fruits: [],
fruit: {}
fruit: {},
modalIsOpen: false
};
Vue.use(Vuex);

View File

@@ -12,5 +12,8 @@ export default {
},
removeFruit: (state, id) => {
state.fruits = state.fruits.filter(item => item.id != id);
},
toggleModal: state => {
state.modalIsOpen = !state.modalIsOpen;
}
};